Start learning free

LESSON 454 OF 536 · DYNAMIC PROGRAMMING

Longest Palindromic Substring in Python: 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

    PYTHON · DYNAMIC PROGRAMMING

    Longest Palindromic Substring in Python, explained

    Problem overview

    The method longestPalindrome receives a string s 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 first wins. Uppercase and lowercase letters are different characters, so "Aa" is not a palindrome.

    For "babad" the answer is "bab". The substring "aba" is just as long, but it starts one position later. For "cbbd" the answer is "bb", a palindrome of even length. For "abacdfgdcaba" the answer is the first "aba", at the start of the string.

    The statement asks for O(n²) time and O(1) extra space besides the returned string.

    Approach: grow palindromes from their centers

    The brute force checks every substring. There are about n²/2 of them and each check reads up to n characters, so it costs O(n³).

    A better view starts from the middle. 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, on the center: the same character twice for an odd center, two neighbours for an even one. While both are inside the string and their characters are equal, move them one step outward. When they stop, the palindrome lies strictly between them and has right - left - 1 characters. Two matching ends on the outside of a palindrome make a longer palindrome, so the loop finds the longest one around that center.

    The helper expand does this and returns the length. For "babad" it gives these lengths, for the odd center at each index and for the even center right after it:

    iodd center (i, i)even center (i, i + 1)
    010
    130
    230
    310
    410

    The first 3 comes at i = 1 ("bab"), the second at i = 2 ("aba"). Because the centers go from left to right and a result replaces the best only when it is strictly longer, "bab" stays.

    Step by step

    1. Create bestStart and bestLen, both 0. The start and length are all that has to be remembered; the substring is cut out once at the end.
    2. Loop over i in range(len(s)). Each i gives two centers: the character s[i] and the gap after it.
    3. Call self.expand(s, i, i) and store it in odd: the longest palindrome of odd length centered on s[i]. The lesson writes the helper last, so the loop stays short.
    4. Call self.expand(s, i, i + 1) and store it in even. If the two characters differ, or i is the last index, the result is 0.
    5. Take length = max(odd, even), the longest palindrome of either kind around i.
    6. Test length > bestLen. The comparison is strict, so an equally long palindrome further right doesn't replace the first one.
    7. Set bestStart to i - (length - 1) // 2, where this palindrome begins. An odd palindrome has (length - 1) / 2 characters before its middle s[i]. An even one has length / 2 - 1 characters before its left middle s[i]. Both equal (length - 1) // 2.
    8. Set bestLen to length. This is the palindrome to beat from now on.
    9. Return the slice s[bestStart:bestStart + bestLen]. A slice leaves out its end index, so it takes exactly bestLen characters.
    10. Write the method expand in the same class, taking self, s, left and right. The calls above use self.expand for that reason.
    11. Write the while condition: left >= 0 and right < len(s) and s[left] == s[right].
    12. Move both ends outward inside the loop: left -= 1 and right += 1.
    13. Return right - left - 1. The loop stops one step beyond the palindrome on each side, so the palindrome runs from left + 1 to right - 1.

    The solution in Python

    class Solution:
        def longestPalindrome(self, s: str) -> str:
            bestStart = 0
            bestLen = 0
            for i in range(len(s)):
                odd = self.expand(s, i, i)
                even = self.expand(s, i, i + 1)
                length = max(odd, even)
                if length > bestLen:
                    bestStart = i - (length - 1) // 2
                    bestLen = length
            return s[bestStart:bestStart + bestLen]
    
        def expand(self, s, left, right):
            while left >= 0 and right < len(s) and s[left] == s[right]:
                left -= 1
                right += 1
            return right - left - 1
    

    Python details worth a closer look:

    • The condition's order protects the indexes. and stops at the first false check, so s[left] and s[right] are read only when both are in range. Dropping left >= 0 would not raise an error for a negative left; Python would read from the end of the string. With the check removed, expand("aa", 0, 0) returns 3, because s[-1] is the last "a". The correct answer is 1.
    • // floors. It divides and rounds down to a whole number. Here length - 1 is never negative, so this is the same as dropping the fraction. For length = 4, (4 - 1) // 2 is 1.
    • Slices don't raise errors. s[bestStart:bestStart + bestLen] is safe even at the end of the string.
    • Strings are immutable. The solution never builds substrings while searching, only two numbers, which is why the extra space stays constant. The single substring is created by the final slice.
    • A method calling another method. expand doesn't use self for anything, but it is still called as self.expand(...), because it belongs to the class.

    Complexity

    Time: O(n²). There are 2n - 1 centers, and each expansion moves outward at most about n / 2 steps. A string of one repeated letter, like "aaaa", is the worst case, because every expansion runs until it reaches an end.

    Space: O(1) extra. Only a handful of integers are kept, besides the returned string.

    A table dp[i][j] that records whether s[i..j] is a palindrome works in the same time but needs O(n²) space. Manacher's algorithm reaches O(n) time, which goes beyond this lesson.

    Edge cases and common mistakes

    • Only odd centers. Without the (i, i + 1) call, "cbbd" would return "b" instead of "bb".
    • Using >= instead of >. A later palindrome of equal length would replace the earlier one, and "babad" would return "aba".
    • A wrong start index. Using i - length // 2 works for odd lengths but is off by one for even ones. i - (length - 1) // 2 covers both.
    • Returning the length. The helper returns a length and the method returns a string. The cut happens once at the end.
    • Single character and no repeats. "a" returns "a". "ab" returns "a", the first of the two palindromes of length 1.
    • Case matters. "Aa" returns "A", because "A" and "a" are different characters.

    Testing

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