3

I have a recursive function that checks if a string is a palindrome, but my assignment asks me to count the number of palindromes in a string (for example kayak has 2).

I'm really confused about how I can implement a recursive function that counts the number of palindromes. Here's my current code:

function isPalindrome(string) {
  if (string.length <= 1) {
    return true;
  }

  let [ firstLetter ] = string;
  let lastLetter = string[string.length - 1];

  if (firstLetter === lastLetter) {
    let stringWithoutFirstAndLastLetters = string.substring(1, string.length - 1);
    return isPalindrome(stringWithoutFirstAndLastLetters);
  } else {
    return false;
  }
}
customcommander
  • 17,580
  • 5
  • 58
  • 84
  • 1
    That's not a super trivial problem. Easiest implementation would be to iterate over all possible substrings of the input. Maybe there's something smarter. – CertainPerformance Oct 29 '20 at 17:18
  • oh ok thanks, do you have any idea on how I can get started with that? – 28space_junkiee Oct 29 '20 at 17:19
  • My brain went to the thought process of looking for a palindrome from index 0-(n-1). Then, look for a palindrome from index 1-(n-2), and so on and so forth – Taplar Oct 29 '20 at 17:21
  • I'm unsure about `if (string.length <= 1) { return true;`. If you want strings of length 1 to be counted, then shouldn't each individually character match as well? That is, 7 results, not just 2 results, for `kayak`? – CertainPerformance Oct 29 '20 at 17:22
  • Go over every letter, expand left and right, checking, whether the letters stay the same (obvious details like even/odd palindromes etc etc...) – ASDFGerte Oct 29 '20 at 17:24

4 Answers4

3

When the function gets a palindrome it is easy:

  1. Record the input
  2. Try again without the edges
  3. Stop when input is three characters or less
"kayak" -> "aya"

If the input isn't a palindrome try "both ends" recursively e.g. with "kayam" try with both "kaya" and "ayam" and keep going...

We stop the recursion when a string is 3 (or less) characters. A single character is not a palindrome and checking whether a two or three characters string is a palindrome is trivial.

                      kayam
                        |
                  +-------------+
                  |             |
                 kaya          ayam
                  |             |
              +-------+     +--------+
              |       |     |        |
             kay     aya   aya      yam

const reverse =
  ([...xs]) =>
    xs.reverse().join("");

const is_palindrome =
  a =>
      a.length === 1  ? false
    : a.length <= 3   ? a[0] === a[a.length-1]
                      : a === reverse(a);

const find_palindromes = str => {
  const scan =
    (x, xs = []) =>
        x.length <= 3     ? xs.concat(is_palindrome(x) ? x : [])
      : is_palindrome(x)  ? xs.concat
                              ( x
                              , scan(x.slice(1, -1))
                              )
                          : xs.concat
                              ( scan(x.slice(0, -1))
                              , scan(x.slice(1))
                              );

  return [...new Set(scan(str))];
};


console.log(find_palindromes("kayak").join());
console.log(find_palindromes("kayakkayak").join());
console.log(find_palindromes("kayakcanoe").join());
console.log(find_palindromes("kayam").join());
console.log(find_palindromes("appal").join());
console.log(find_palindromes("madamimadam").join());
console.log(find_palindromes("madamimadamkayak").join());
customcommander
  • 17,580
  • 5
  • 58
  • 84
  • It seems a little schizophrenic to count single-letter strings as palindromes, but not single letters inside words. And I can't really tell if that's the requirement. Regardless, this is a nice version. – Scott Sauyet Nov 09 '20 at 22:38
  • Well, I think I spoke too soon. I just tried `"madamimadam"` and got `5` as expected, but when I appended `"kayak"` to it, I got `0`. Adding characters to the end definitely shouldn't reduce the number of palidromic substrings. – Scott Sauyet Nov 09 '20 at 22:43
  • @customcommander this doesn't count the palindromes though? it just prints them out – 28space_junkiee Nov 10 '20 at 01:52
  • @ScottSauyet i was looking for something similar to chelmertz answer – 28space_junkiee Nov 10 '20 at 01:53
  • @28space_junkiee True but it shouldn’t be too hard to count the result ;) – customcommander Nov 10 '20 at 01:54
  • @customcommander haha for sure but my teacher was looking for it to return a number something similar to chelmertz answer – 28space_junkiee Nov 10 '20 at 01:56
  • This answer makes me notice an ambiguity I hadn't considered. Are we counting all occurrences of any palindromic substring or the unique palindromic strings that are substrings of the original? I was assuming the former. This answers the latter; either seems a reasonable interpretation of the question. – Scott Sauyet Nov 10 '20 at 13:09
  • @28space_junkiee: Getting the count is just a matter of replacing `return [...new Set(scan(str))]` with return `(new Set(scan(str))).size`. – Scott Sauyet Nov 10 '20 at 13:33
  • @ScottSauyet One thing in favour of _just_ counting (as opposed to finding then counting) is that once you have found a palindrome you can automatically workout how many sub palindromes it contains. So recursing within an already established palindrome is unnecessary if you just want the count. It may not improve performance as such but the difference would perhaps matter in an assignment. – customcommander Nov 10 '20 at 13:45
  • @customcommander: How "automatically"? `"madamimadam"` has sub-palindromes that are not centered in the middle of the string (`"madam"` and `"ada"`, each twice.) Is there a trick I'm missing to catch them? (BTW: you inspired me to try my own hand at this; my answer is quite different from this. I always like to see variety.) – Scott Sauyet Nov 10 '20 at 13:55
  • @ScottSauyet Very true. It is perhaps just safer to find them all then count. – customcommander Nov 10 '20 at 13:58
2

I think the accepted answer does not actually work. It will not count palindromes unless they are centered in the string and will count substrings that are not palindromes if as long as they start and end with the same letter. The answer from CertainPerformance would probably work but I think it would result in checking a lot of strings that don't need to be checked. Here's what I came up with, I think it works for the extra tests I've added.

function countPalindromes(string) {
    if (string.length <= 1) {
    return 0;
    }

    count = 0

    for ( var i = 0; i < string.length; i++  ) {
    count += countPalindromesCenteredAt(string, i)
    count += countPalindromesCenteredAfter(string, i)
    }

    return count
}

function countPalindromesCenteredAt(string, i) {
    count = 0
    for ( var j = 1; i-j>=0 && i+j < string.length; j++  ) {
    if (string.charAt(i-j) === string.charAt(i+j)) {
        count += 1
    }
    else {
        return count
    }
    }

    return count
}

function countPalindromesCenteredAfter(string, i) {
    count = 0
    
    for ( var j = 1; i-j>=0 && i+j < string.length; j++  ) {
    if (string.charAt(i-j+1) === string.charAt(i+j)) {
        count += 1
    }
    else {
        return count
    }
    }

    return count
}

console.log(countPalindromes("kayak"));
console.log(countPalindromes("aya"));
console.log(countPalindromes("kayakcanoe"));
console.log(countPalindromes("kcanoek"));
biomiker
  • 3,108
  • 1
  • 29
  • 26
  • thank you for that clarification yes I noticed that too, but I'm not looking for that because my teacher just wants to come up with a palindrome and count the palindromes. I'll be choosing the palindrome so it would most likely be short just to save time and keep things simple. I accepted your answer because I know it can help others so thank you! – 28space_junkiee Oct 29 '20 at 18:32
  • 1
    Your original statement of the assignment: "count the number of palindromes in a string" is definitely subject to interpretation. What is the number of palindromes in the string "kayam"? What about "kayakkayak"? This is academic at this point, but so is your original question. :) – biomiker Oct 29 '20 at 19:15
  • do you know how I can implement this as an iterative? @biomiker – 28space_junkiee Nov 12 '20 at 01:58
  • do you know how I can use this as an iterative? – 28space_junkiee Nov 12 '20 at 02:01
  • @28space_junkiee My understanding is that it's already iterative. – biomiker Nov 12 '20 at 18:09
1

One method would be to first get all substrings, then validate each:

getAllSubstrings('kayak').filter(str => str.length >= 2 && isPalindrome(str))

function getAllSubstrings(str) {
  var i, j, result = [];

  for (i = 0; i < str.length; i++) {
      for (j = i + 1; j < str.length + 1; j++) {
          result.push(str.slice(i, j));
      }
  }
  return result;
}
function isPalindrome(string) {
  if (string.length <= 1) {
    return true;
  }

  let [ firstLetter ] = string;
  let lastLetter = string[string.length - 1];

  if (firstLetter === lastLetter) {
    let stringWithoutFirstAndLastLetters = string.substring(1, string.length - 1);
    return isPalindrome(stringWithoutFirstAndLastLetters);
  } else {
    return false;
  }
}
console.log(
  getAllSubstrings('kayak').filter(str => str.length >= 2 && isPalindrome(str))
);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
0

Here's an answer similar to that from CertainPerformance, but using recursion for the helper functions:

const getSubstrings = (str) =>
  str .length == 0 
    ? []
    : [
        ... str .split ('') .map ((_, i) => str .slice (0, str .length - i)),
        ... getSubstrings (str .slice (1))
      ]

const isPalindrome = (str) =>
  str .length < 2
    ? true
    : str [0] === str .slice (-1) [0] && isPalindrome (str .slice (1, -1))

const getPalindromicSubstrings = (str) =>
  getSubstrings (str) 
    .filter (s => s.length > 1) 
    .filter (isPalindrome)

const countPalindromicSubstrings = (str) =>
  getPalindromicSubstrings (str) .length

const countUniquePalindromicSubstrings = (str) =>
  new Set(getPalindromicSubstrings (str)) .size

console .log (getPalindromicSubstrings ('madamimadam'))
console .log (countPalindromicSubstrings ('madamimadam'))
console .log (countUniquePalindromicSubstrings ('madamimadam'))
.as-console-wrapper {max-height: 100% !important; top: 0}
  • getSubstrings does just what you'd expect. getSubstrings('abcd') returns ["abcd", "abc", "ab", "a", "bcd", "bc", "b", "cd", "c", "d"].

  • isPalindrome says that the empty string and single-character strings are automatically palindromes and that for another string we check that the two end characters match, recurring on the remainder.

  • getPalindromicSubstrings finds all the substrings that are palindromes, skipping those of length 1.

  • countPalindromicSubstrings returns a count of those.

  • countUniquePalindromicSubstrings uses a Set to filter out duplicates and returns that count.

We could also easily write a getUniquePalindromicSubstrings in a similar manner if needed.

getSubstrings is the only function with any complexity. It operates by repeatedly slicing our string from to a value varying from length down to 1, then recurring on the string starting with the second character, stopping when our input is empty.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • is there a way to simplify this answer something similar to chelmertz answer – 28space_junkiee Nov 12 '20 at 03:34
  • 1
    @28space_junkiee: I'm not sure I understand. You comment on chelmertz's answer to say it's incorrect, and then on this answer to ask me to make it more like that one. Hmm? I think this breakdown is quite simple. One function fetches substrings, another tests palindromes, a third finds all palindromic substrings, and either of the other two returns counts of them, depending on your requirements. Granted a non-recursive palindrome-check is probably simpler (`const isPalindrome = (str) => str.split('').reverse().join('') == str`) but you specifically asked for recursion. – Scott Sauyet Nov 12 '20 at 13:49