Start learning free

LESSON 454 OF 536 · DYNAMIC PROGRAMMING

Longest Palindromic Substring in JavaScript: Expand Around Center

Practice O(n2) Read the full explanation

You are given a string s. Return its longest palindromic substring. If several substrings have that greatest length, return the one that starts first, furthest to the left.

A palindrome reads the same forward and backward, like "racecar" or "abba". A substring is a run of consecutive characters of s: "bcd" is a substring of "abcde", but "ace" is not.

 

Example 1:

Input: s = "babad"
Output: "bab"
Explanation: "aba" is a palindrome of length 3 too, but "bab" starts first.
No substring of length 4 or 5 is a palindrome.

Example 2:

Input: s = "cbbd"
Output: "bb"
Explanation: A palindrome of even length, like "bb", has its middle between two characters.

Example 3:

Input: s = "abacdfgdcaba"
Output: "aba"
Explanation: "aba" appears at the start and at the end, and the first one is returned.
"abacd" read backward is "dcaba", which also appears in s, but neither of them is a palindrome.

 

Constraints:

  • 1 <= s.length <= 1000
  • s is made of English letters and digits only. Uppercase and lowercase letters are different characters: "Aa" is not a palindrome.

Note:

Your algorithm should run in O(n2) time and use O(1) extra space besides the returned string, where n is the length of s.



Checking every substring on its own takes O(n3): about n2/2 substrings, each up to n characters long. Look at palindromes from the middle instead. Every palindrome has a center: its middle character when its length is odd, or the gap between its two middle characters when the length is even. A string of n characters has 2n - 1 centers: n characters and n - 1 gaps.

From a center, expand: start with two positions left and right (the same character, or two neighbours), and move them one step outward while both are inside s and their characters are equal. When they stop, the palindrome between them has right - left - 1 characters, and it is the longest one around that center. A helper that does this and returns the length, called for the centers (i, i) and (i, i + 1) of every i, finds the longest palindrome at each center.
A palindrome of length length centered at i starts at i - (length - 1) / 2, rounded down. Keep the best start and length, and replace them only by a strictly longer palindrome: the centers go from left to right, so the first of the longest ones stays.

There are 2n - 1 centers, and each expansion takes at most n / 2 steps: O(n2) time. Only a few numbers are kept: O(1) extra space, besides the returned string.
A table dp[i][j] telling whether s[i..j] is a palindrome works too, in the same time but with O(n2) space. Manacher's algorithm even finds the answer in O(n), but it goes beyond this lesson.
STEP 1 OF 13
    Ctrl + Enter to check

    JAVASCRIPT · DYNAMIC PROGRAMMING

    Longest Palindromic Substring in JavaScript, explained

    Problem overview

    The method longestPalindrome(s) receives a string and returns its longest palindromic substring. A palindrome reads the same forward and backward, like "racecar" or "abba". A substring is a run of consecutive characters, so "bcd" is a substring of "abcde" but "ace" is not. When several palindromes share the greatest length, the one that starts furthest to the left wins. Uppercase and lowercase letters are different characters, so "Aa" is not a palindrome.

    For "babad" the answer is "bab": "aba" has the same length, but "bab" starts first. For "cbbd" it is "bb", a palindrome of even length. For "abacdfgdcaba" it is "aba", the first of its two appearances.

    The lesson asks for O(n²) time and O(1) extra space besides the returned string, where n is the length of s.

    Approach: grow palindromes from their centers

    Checking every substring on its own takes O(n³): about n²/2 substrings, each up to n characters long. Looking at palindromes from the middle is much cheaper.

    Every palindrome has a center: its middle character when the length is odd, or the gap between its two middle characters when the length is even. A string of n characters has 2n - 1 centers, n characters and n - 1 gaps. From a center, expand: put two positions left and right there, and move them one step outward while both are inside the string and their characters are equal. When they stop, the palindrome between them is the longest one around that center, and it has right - left - 1 characters.

    A helper does this and returns the length. For each index i it is called twice: with (i, i) for the odd center on s[i], and with (i, i + 1) for the even center in the gap after it. For "babad" the results are:

    i  odd  even  start
    0  1    0     0
    1  3    0     0
    2  3    0     1
    3  1    0     3
    4  1    0     4

    At i = 1 the longest palindrome has length 3 and starts at index 0: "bab". At i = 2 there is another one of length 3, starting at index 1: "aba". It isn't longer than the best so far, so it doesn't replace it, and the leftmost one stays.

    Step by step

    1. Remember the best palindrome with two variables, bestStart and bestLen, both 0. Its start and length are all that is needed. The substring is cut out once, at the end.
    2. Visit every position i from 0 to s.length - 1. Each one gives two centers, which together cover all 2n - 1 (the extra even center after the last character finds nothing).
    3. Odd center. Call this.expand(s, i, i) and keep the result in odd. The helper is written afterwards; the main loop only needs to know what it returns.
    4. Even center. Call this.expand(s, i, i + 1) and keep the result in even. It is 0 when the two characters differ or when i is the last index.
    5. Take the larger of the two with Math.max, in length.
    6. Compare strictly: length > bestLen. A palindrome equally long starts further right, so it must not replace the one found first.
    7. Compute the start. An odd palindrome of length characters has (length - 1) / 2 characters before its middle s[i]. An even one has length / 2 - 1 characters before s[i], its left middle. Rounded down, both equal (length - 1) / 2, so the start is i - Math.floor((length - 1) / 2).
    8. Update bestLen to length.
    9. Cut out the answer with s.substring(bestStart, bestStart + bestLen).
    10. Write expand as a second method of the class, taking s, left and right.
    11. Loop while the ends are valid and equal: left >= 0, right < s.length and s[left] === s[right]. The checks run left to right and stop at the first false one, so the characters are compared only when both ends are inside the string.
    12. Move outward: left-- and right++.
    13. Return right - left - 1. The loop stops one step past the palindrome on both sides, so the palindrome runs from left + 1 to right - 1, which is that many characters. If the first two characters already differ, the result is 0.

    The solution in JavaScript

    class Solution {
        longestPalindrome(s) {
            let bestStart = 0;
            let bestLen = 0;
            for (let i = 0; i < s.length; i++) {
                const odd = this.expand(s, i, i);
                const even = this.expand(s, i, i + 1);
                const length = Math.max(odd, even);
                if (length > bestLen) {
                    bestStart = i - Math.floor((length - 1) / 2);
                    bestLen = length;
                }
            }
            return s.substring(bestStart, bestStart + bestLen);
        }
    
        expand(s, left, right) {
            while (left >= 0 && right < s.length && s[left] === s[right]) {
                left--;
                right++;
            }
            return right - left - 1;
        }
    }
    

    JavaScript details worth knowing:

    • this.expand. Methods in a class don't see each other by bare name. Inside longestPalindrome, the helper must be called as this.expand(...).
    • Division is not integer division. (length - 1) / 2 is 1.5 when length is 4, which is why Math.floor is needed. Without it, bestStart would be a fraction.
    • Reading past the end gives undefined. s[right] with right equal to s.length does not throw. The range checks come first anyway, so the comparison never depends on that.
    • Parameters are copies. expand changes its own left and right; the caller's variables are unaffected, because numbers are passed by value.
    • s[i] indexes UTF-16 code units. That is fine for this lesson, whose strings contain only English letters and digits.

    Complexity

    Time: O(n²). There are 2n - 1 centers, and each expansion takes at most about n / 2 steps. A string like "aaaa…a" is the worst case, because every expansion runs to an end of the string.

    Space: O(1) extra. Only a few numbers are kept. The returned substring is the only new string.

    A table dp[i][j] that records whether s[i..j] is a palindrome gives the same time but costs O(n²) space. Manacher's algorithm finds the answer in O(n), but it goes beyond this lesson.

    Edge cases and common mistakes

    • Only odd centers. Skipping this.expand(s, i, i + 1) misses palindromes like "bb". For "cbbd" the answer would be "c" instead of "bb".
    • Using >= instead of >. A later palindrome of the same length would replace the first one. For "babad" the result would be "aba" instead of "bab".
    • Checking s[left] === s[right] before the bounds. The bounds checks must come first in the condition.
    • Single characters. "a" returns "a", and "ac" returns "a", the first of two palindromes of length 1.
    • Case matters. "Aa" returns "A", because "A" and "a" are different characters.
    • A start that drifts for even palindromes. The formula i - Math.floor((length - 1) / 2) works for both parities. For "cbbd" at i = 1 the even palindrome has length 2 and starts at 1.

    Testing

    • "babad" gives "bab".
    • "cbbd" gives "bb".
    • "abacdfgdcaba" gives "aba".
    • "a" gives "a".
    • "ac" gives "a".
    • "Aa" gives "A".
    • "aaaa" gives "aaaa".
    • "forgeeksskeegfor" gives "geeksskeeg".
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free