Issue With Slicing In Javascript Recursion
Solution 1:
Problems with implementation
I'm afraid there are at least three separate problems with this implementation.
First, it forgets to same-case the characters, and the initial capital 'A'
from 'Anne'
won't match the final lower-case 'a'
from 'Vienna'
.
Second, it properly alters the string
input to remove non-alphanumeric characters, but then when it recurs, it does so on this original string
rather than on the new str
.
Third, and most fundamentally, it doesn't check the most important fact before it recurs: it doesn't test whether the first character matches the last character.
Getting initial version working
We can fix all those and get a version that looks like this:
functionpalindrome(string) {
let str = string.replace(/[^A-Za-z0-9]/g, '') .toLowerCase ().split("") // Issue 1 -- case// console.log( str.join('') )if (str.length === 3 && str[0] === str[2] || str.length === 1) {
returntrue
} elseif (str.length === 2 && str[0] !== str[1]) { // Issue 3 -- compare first and lastreturnfalse
} elseif (str [0] !== str [str .length - 1]) {
returnfalse
} else {
returnpalindrome(str.slice(1, -1).join('')) // Issue 2 -- recur on correct item
}
returnfalse
}
console .log (palindrome ("Anne I vote more cars race Rome to Vienna")) //=> trueconsole .log (palindrome ("Anne I vote more cars X race Rome to Vienna")) //=> falseconsole .log (palindrome ("A man, a plan, a canal: Panama!")) //=> trueconsole .log (palindrome ("A man, a plan, a banal palindrome.")) //=> false
Remaining problems
But even here, there is some real ugliness. We convert back and forth between strings and arrays on every call. We run the string clean-up on every call, when it shouldn't be needed after the first one. And it contains many more separate cases than are really necessary.
Don't keep converting
The first fix is trivial. We simply don't need arrays here. We can do all our tests on strings. The characters in a string can be indexed exactly like array items, and they have an equivalent length
property.
Initialize only once
The second issue, that of rerunning the initialization on each call is usually fixed in one of two ways: adding extra parameters which are defaulted on the initial call, or adding a helper functions. We'll choose the latter because there are some real potential problems with the former (although it's often enough still worth considering.) This breaks down into two styles. We can nest the helper function inside our main function and then call it from inside. Or we can make it an external function that we just call. The answer from MisterJoJo aptly demonstrates the first style, so we can look at the second. Here we can write an internal recursive _isPalindrome
function that takes a string of lower-case alphanumeric characters and returns a boolean. And then we could write a public wrapper function that accepts any string, normalizes it by removing all non-alphanumeric characters and lower-casing it, passing the result to that internal function and returning its result. It might look something like this:
constisPalindrome = (string) =>
_isPalindrome (string .replace (/\W/g, '') .toLowerCase())
const_isPalindrome = (cs) => /* ... recursive version here ... */
In these days of easy-to-use JS modules, that internal function can be encapsulated in the module, leaving only the main function publicly visible.
Handle fewer cases
For the third issue, we need to really look at what it means to be a palindrome. Fundamentally, it simply means that the string reads the same forward and backward. The intuition represented by this answer is that this means that the first and last characters are the same and the remaining string between them is also a palindrome.
For a recursive function, we need at least one base case. When can we no longer check whether the first and last character are the same? The answer is pretty clearly: when we don't have separate first and last characters.. So our base case is when there's zero or one characters in a string. A one-character string is clearly a palindrome. So is a zero-character string; if this sounds odd, please look up vacuous truths. So we now have a simple base case, and a simple recursive case, and we can just write our function:
constisPalindrome = (string) =>
_isPalindrome (string .replace (/\W/g, '') .toLowerCase ())
const_isPalindrome = (cs) =>
cs .length < 2
? true
: cs [0] == cs [cs .length - 1] && _isPalindrome (cs .slice (1, -1))
console .log (isPalindrome ("Anne I vote more cars race Rome to Vienna")) //=> trueconsole .log (isPalindrome ("Anne I vote more cars X race Rome to Vienna")) //=> falseconsole .log (isPalindrome ("A man, a plan, a canal: Panama!")) //=> trueconsole .log (isPalindrome ("A man, a plan, a banal palindrome.")) //=> false
Using utility functions
We could easily stop here. But there is still some ugliness in that code. All the index manipulations take some reading to understand. There is a strong argument to move that ugliness into well-named helper functions. I usually have laying around the helpers last
and first
, and it's easy enough to also write a middle
. These take the first or last characters of a string or array, or, for middle
, take everything between the first and last characters. Using those can make our main function more readable:
constfirst = (xs) => xs [0]
constlast = (xs) => xs [xs .length - 1]
constmiddle = (xs) => xs .slice (1, -1)
constisPalindrome = (string) =>
_isPalindrome (string .replace (/\W/g, '') .toLowerCase ())
const_isPalindrome = (cs) =>
cs .length < 2
? true
: first (cs) == last (cs) && _isPalindrome (middle (cs))
Generic processing
We accidentally wrote a more generic version of the function than we were trying to write! Note that we can use this same internal function on arrays:
_isPalindrome (['foo', 'bar', 'qux', 'bar', 'foo']) //=> true
_isPalindrome (['foo', 'bar', 'qux', 'foo']) //=> false
The semantics are simple. An array is a palindrome if it has fewer than two elements or if the first and last element are the same and the remaining elements form a palindrome.
This was mostly a happy accident. When I noticed it, I changed the parameter name that I originally wrote, letters
to the more generic cs
, which just might bring to mind characters
but doesn't insist on it.
Noting the generic nature of this implementation, I would probably change middle
so that we could carry this to even more types, to any finite iterable type. middle
won't handle this right now because not all such iterables have slice
methods. But all of them can be converted arrays, and then we can use the array slice
method.
Here is a simple update to middle
to make this function still more generic:
constmiddle = (xs) => [...xs] .slice (1, -1)
As I said, this was a happy accident. I didn't notice it while writing the code, only while typing up this post.
Much simpler isPalindrome
Finally, the question noted that this problem is probably not best solved with recursion. That's quite true. We glossed by it before, but did mention that a string is a palindrome if it reads the same forward and backward. We can write that directly:
constisPalindrome = (string) =>
_isPalindrome (string .replace (/\W/g, '') .toLowerCase ())
const_isPalindrome = (s) =>
s == s .split ('') .reverse () .join ('')
console .log (isPalindrome ("Anne I vote more cars race Rome to Vienna")) //=> trueconsole .log (isPalindrome ("Anne I vote more cars X race Rome to Vienna")) //=> falseconsole .log (isPalindrome ("A man, a plan, a canal: Panama!")) //=> trueconsole .log (isPalindrome ("A man, a plan, a banal palindrome.")) //=> false
While String
does not have a reverse
method, we can easily convert to an array, reverse the array, and join the result back into a string. Then we simply test whether the reversed string is the same as the original.
This version is not generic like the last one, but it is definitely a more direct translation of the idea of a palindrome directly into code.
Note that the regular expression /\W/g
is precisely equivalent to /[^A-Za-z0-9]/g
. It's the converse of \w
, which represents [a-zA-z0-9]
, but is simpler to type.
We can also note that if the first and last characters are the same, that is, if our string is only one character long, we could use the same process. All that would change is whether we test for length less than 2
or less than 1
.
first
is often called head
for reasons not worth discussing here.
Well, again, the finite ones
It's probably worth extracting a reverseString
helper function from this, for the same reason we did first
and last
above. It's a genuinely reusable simple function. That's left as an exercise.
Solution 2:
You do the .slice(1, -1)
on the wrong element because it keeps the spaces and these do not match in the palindrome.
And your code just tests the last letters in the center, and forgets all the others around.
I'd rather see the code this way
console.log(palindrome("Anne I vote more cars race Rome to Vienna"))
functionpalindrome( str )
{
returnrecussiv( str.replace(/[^A-Za-z0-9]/g, '').toLowerCase() )
functionrecussiv(s)
{
console.log('-->', s )
if (s.length > 1)
{
return (s[0] === s[s.length-1]) ? recussiv(s.slice(1, -1)) : false
}
elsereturntrue
}
}
.as-console-wrapper { max-height: 100%!important; top: 0
Post a Comment for "Issue With Slicing In Javascript Recursion"