Let's take a look at this "Palindrome Substrings" problem using the PEDAC approach.
// Given a string, write a function, `palindromeSubstrings`, that returns
// all the substrings from the given string that are palindromes and have
// a minimum length of two.
// Test cases:
console.log(palindromeSubstrings("supercalifragilisticexpialidocious"));
// should return: ["ili"]
console.log(palindromeSubstrings("abcDDcbA"));
// should return: ["bcDDcb", "cDDc", "DD"]
console.log(palindromeSubstrings("palindrome"));
// should log: []
console.log(palindromeSubstrings(""));
// should log: []
Understanding the problem has three steps.
After reading this problem description, some items may need clarification:
What is a palindrome? You might ask the interviewer to tell you what a palindrome is, and the interviewer would tell you that it is a word that reads the same forwards and backward.
What is a substring?
You might ask the interviewer to tell you what a substring is, and the interviewer would tell you that it's a contiguous sequence of characters within a string. For example, in a string "halo"
, some substrings are "ha"
, "alo"
, "lo"
etc.
Should the characters in the string remain the same if they already use uppercase?
Here, you can check the test cases. In the second test case, the input contains the substring "DD"
which remains in uppercase in the expected return value.
How should I deal with empty strings provided as input? The test cases frequently answer this question. Here, test case number 3 provides the answer. This is an implicit requirement that we can infer from the test cases. Given an empty string as input, we should return an empty array.
Can I assume that all inputs are strings? Our test cases don't show any non-string inputs, so you should ask whether the inputs can contain non-string values, and what you should do with these edge cases if so. In this problem, we can expect our argument to always be a string.
Should I consider letter case when deciding whether a word is a palindrome?
Our second test case showed us that we should maintain the case of a given string, but should the case determine if a substring is a palindrome. Again, our second test case gives us a hint. Our expected output doesn't include the substring "abcDDcbA"
but it does include "bcDDcb"
. It looks like the difference in case between our start "a"
and end "A"
means that this isn't a palindrome. We could also ask the interviewer if we didn't notice this implicit rule.
Always verify your assumptions either by looking at the test cases or by asking the interviewer. Some assumptions, like whether we should treat strings as case-sensitive or not, can be verified either by looking at the problem description, if that is mentioned there, or by checking the test cases. If you can't determine the answer with the test cases or problem description, you should ask the interviewer to clarify this for you.
We should note that this isn't an exhaustive list of questions. These questions apply to this challenge, but the types of questions you ask will vary greatly depending on the problem given. For example, if we were working with an array instead of a string, or rather, a mutable input instead of an immutable one, we should consider whether we can or should mutate our input. Knowing what questions you need answers to in order to solve a problem is an important part of the PEDAC process, and it's something that you will develop an intuition for with time and experience.
To conclude this part of the PEDAC process, you should make sure that you've written down all of the information you've acquired. The problem description should give you your input, output, and a list of explicit rules. Your exploration of the test cases and your own questions should give you a list of implicit rules:
Data structures influence your algorithm, and for that reason, these two steps are often paired. At this stage, deciding what data structure to use often feels intuitive to students, like when choosing between an array and a hash. Designing the right algorithm, on the other hand, is far more challenging. The biggest problem that students have when writing algorithms is providing sufficient detail.
What data structure could we use to solve this problem? The obvious choice seems to be an array since that's the desired output.
Now, we come to the algorithm part. Look at the algorithm written below.
result
variable and initialize it to an empty array
substrArray
that will contain all of the
substrings of the input string that are at least 2 characters long.
substrArray
array.
result
array
result
array
Does this algorithm look complete to you?
This algorithm is a "high-level" algorithm and it resembles those that we often see students write during interviews. It looks complete, but let's think about it for a moment: What is the hardest part of this problem? Is it looping through an array and pushing substrings that are palindromes in the result array? Is it determining whether a substring is a palindrome? Is it something else entirely?
Determining whether a word is a palindrome isn't that difficult for most students. Looping through the array and selecting all the palindromes is relatively easy as well. However, finding all the substrings for a given string can be challenging. The above algorithm doesn't tackle that issue. It lacks implementation details for the "hard" parts.
When a student starts writing code based on this algorithm, they soon realize that returning all the substrings won't be easy. Ideally, the student should return to the algorithm and try to come up with a way to find all the substrings from the input string. They might even create a new method named substrings
that returns the array of substrings. In practice, though, the time limitations often lead them to take a hack & slash approach instead. That almost always leads to spending more time than necessary on the problem or, worse yet, not solving it at all.
Let's now follow the correct approach. The student can use the "high-level" algorithm from above and first write the code for it. The code might look like this:
function palindromeSubstrings(str) {
let result = [];
let substringsArr = substrings(str);
substringsArr.forEach(substring => {
if (isPalindrome(substring)) {
result.push(substring);
}
});
return result;
}
Note that we are calling functions named substrings
and isPalindrome
. We haven't defined those functions yet. Instead of trying to write the code for these subproblems on the fly, let's return to our algorithm and flesh out their logic. To start, let's investigate our substrings
helper function.
To find a working algorithm, we can simplify the problem by using a small, concrete example to determine what we need to do. For instance, we can start with a short word like "halo"
and write all its substrings that are at least 2 characters in length. The resulting list is ["ha", "hal", "halo", "al", "alo", "lo"]
. Do you see a pattern here? It's clear that some sort of complex looping is going on.
The first loop - the outermost loop - iterates over the starting index for the substrings. With "halo"
as a starting string, we need to iterate over the letters "h"
, "a"
, and "l"
. (We don't need to iterate over "o"
since there are no substrings of at least 2 characters that start with "o"
.)
Within the first loop, we'll iterate over the possible ending indices of our substrings. The ending index needs to start at least two positions after the starting index. This is because JavaScript's slice method excludes the character at the provided ending index.
Consider the string "halo"
. Calling "halo".slice(0, 2)
returns "ha"
. Notice that the character at index 2 ("l"
) is not included in the result.
Here's that looping pattern written out in an algorithm:
Beginning with the first letter of the string at index 0, 'h'
, we first find all of the substrings that begin with that letter: ['ha', 'hal', 'halo']
. As you can see, we're showing the inner loop at work here:
'ha'
'hal'
'halo'
The loop, in this case, is one that starts with an index 2 and ends with an index of 4.
Next, we need to find the substrings that start at index 1 (a
). The loop, in this case, starts with an index of 3 and ends with an index of 4.
'al'
'alo'
Finally, we get all of the substrings that begin at index 2. This time, the loop starts and ends with an index of 4, so there is only one iteration:
'lo'
What would happen if the original string was, say, 7 characters in length, such as goalies
? In that case, we'd still have to go through the same process - an outer loop that iterates from index 0 (the letter g
) to index 5 (the letter e
), and an inner loop that starts with index startIdx + 2
and continues until 7:
Looking at these two examples, we can determine that the outer loop iterates over indices from 0 to the length of the next to the last index position (i.e., string.length - 2
). We can also see that the inner loop ranges from the starting index plus two (startIdx + 2
) to the original string length. We can use both of these facts in our algorithm. Let's go ahead and write the complete algorithm for this function:
substrings
Algorithmresult
that will contain all required substrings
startIdx
variable (value 0
) for the starting index of a substring
startIdx
to iterate over string
from 0 to the length of the string minus 2
endIdx
variable (value startIdx + 2
) for the ending index of a substring
endIdx
to iterate over string
from startIdx + 2
to string.length
startIdx
and endIdx
result
array
endIdx
variable by 1
startIdx
variable by 1
result
array
Here's some code that we might write for the substrings
function:
function substrings(str) {
let result = [];
let startIdx = 0;
while (startIdx <= str.length - 2) {
let endIdx = startIdx + 2;
while (endIdx <= str.length) {
let substring = str.slice(startIdx, endIdx);
result.push(substring);
endIdx += 1;
}
startIdx += 1;
}
return result;
}
Checking whether the string is a palindrome is easy enough. However, we can write a function for it to help make our code more readable. Let's include that function in our algorithm.
isPalindrome
function, check whether the string value is equal to its reversed value
You can use the Array.prototype.reverse
method along with split
and join
:
function isPalindrome(str) {
return str === str.split('').reverse().join('');
}
We've finished all of the steps necessary to create a complete PEDAC:
substrings
functionresult
that will contain all required substrings
startIdx
variable (value 0
) for the starting index of a substring
startIdx
to iterate over string
from 0 to the length of the string minus 2
endIdx
variable (value startIdx + 2
) for the ending index of a substring
endIdx
to iterate over string
from startIdx + 2
to string.length
startIdx
and endIdx
result
array
endIdx
variable by 1
startIdx
variable by 1
result
array
isPalindrome
functionisPalindrome
function, check whether the string value is equal to its reversed value
palindromeSubstrings
functionresult
variable and initialize it to an empty array
substrArray
that will contain all of the substrings of the input string that are at least 2 characters long.
substrArray
array.
result
array
result
array
The code for our main problem, palindromeSubstrings
, along with all of the helper functions:
function substrings(str) {
let result = [];
let startIdx = 0;
while (startIdx <= str.length - 2) {
let endIdx = startIdx + 2;
while (endIdx <= str.length) {
let substring = str.slice(startIdx, endIdx);
result.push(substring);
endIdx += 1;
}
startIdx += 1;
}
return result;
}
function isPalindrome(str) {
return str === str.split('').reverse().join('');
}
function palindromeSubstrings(str) {
let result = [];
let substringsArr = substrings(str);
substringsArr.forEach(substring => {
if (isPalindrome(substring)) {
result.push(substring);
}
});
return result;
}
console.log(palindromeSubstrings("supercalifragilisticexpialidocious")); // ["ili"]
console.log(palindromeSubstrings("abcddcbA")); // ["bcddcb", "cddc", "dd"]
console.log(palindromeSubstrings("palindrome")); // []
console.log(palindromeSubstrings("")); // []
Once again, we want to emphasize that you don't need to write all your pseudocode before you start coding. As you saw above, we first wrote the pseudocode for the palindromeSubstrings
function. We then wrote the corresponding JavaScript code before we returned to write the pseudocode for the other two functions. Afterwards, we wrote the corresponding code, and then returned to the two lower-level functions.
Finally, the main takeaway is that you should be able to write a plain English solution to the problem. If you can't do that, you won't be able to code it either. You also don't need any "fancy" functions to solve these problems.
In conclusion, we suggest you practice working through the PEDAC process while solving problems. It may be hard at first, and for simple problems, it might even seem unnecessary, but stick with it. In time, your process will improve, and you'll soon be able to solve difficult problems much more readily.