Start learning free

LESSON 459 OF 541 · DYNAMIC PROGRAMMING

Longest Palindromic Substring in Swift: 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 15
    Ctrl + Enter to check

    SWIFT · DYNAMIC PROGRAMMING

    Longest Palindromic Substring in Swift, 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" and "ace" is not. When several palindromes share the greatest length, the one that starts furthest to the left wins.

    For "babad" the answer is "bab". The string "aba" is just as long, but "bab" starts first. For "cbbd" the answer is "bb", an even-length palindrome. For "abacdfgdcaba" the answer is "aba", which appears at the start and at the end, and the first one is returned. Letters are case-sensitive: "Aa" is not a palindrome, so its answer is "A".

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

    Approach: grow a palindrome from its middle

    Checking every substring separately costs O(n³): there are about n²/2 substrings and each takes up to n steps to check. 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 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. A helper called expand does exactly this and returns the length.

    For each index i the solution calls expand(chars, i, i) for the odd center on a character and expand(chars, i, i + 1) for the even center on the gap after it. In "babad", expand from index 1 on the character gives 3 (the palindrome "aba"), and from index 2 it also gives 3 ("bab"). For "cbbd" the odd expansion at index 1 gives 1 and the even one gives 2, the "bb".

    The solution remembers only the start and the length of the best palindrome so far. It replaces them only for a strictly longer one. Centers are visited from left to right, so among equally long palindromes the first one stays.

    Step by step

    1. Copy the characters into an array. A Swift String can't be indexed with an integer, but an [Character] can.
    2. Create bestStart and bestLen, both 0, with var. The substring itself is cut out once, at the end.
    3. Loop i over 0..<chars.count. Each i gives two centers: the character and the gap after it.
    4. Call expand(chars, i, i) and store it in odd. The helper is written last.
    5. Call expand(chars, i, i + 1) and store it in even. If the two characters differ, or i is the last index, the result is 0.
    6. Take max(odd, even) as length, the longest palindrome centered at i.
    7. Compare with length > bestLen. The test is strict so that the leftmost of equal palindromes is kept.
    8. Compute the start as i - (length - 1) / 2. An odd palindrome has (length - 1) / 2 characters before its middle. An even one has length / 2 - 1 before its left middle, which is the same number after integer division.
    9. Store the new bestLen.
    10. Return the substring: chars[bestStart..<bestStart + bestLen] turned back into a String.
    11. Write expand as a method of the same class. It takes the character array and two integers, all without argument labels, and returns an Int.
    12. Copy the parameters into variables of the same names, because Swift parameters can't be changed.
    13. Loop while both ends are in range and the characters match. The range checks come first, so chars[left] and chars[right] are only read when both indexes are valid.
    14. Move the ends outward with left -= 1 and right += 1.
    15. Return right - left - 1. The loop stops one step past the palindrome on both sides, so it spans left + 1 to right - 1.

    The solution in Swift

    class Solution {
        func longestPalindrome(_ s: String) -> String {
            let chars = Array(s)
            var bestStart = 0
            var bestLen = 0
            for i in 0..<chars.count {
                let odd = expand(chars, i, i)
                let even = expand(chars, i, i + 1)
                let length = max(odd, even)
                if length > bestLen {
                    bestStart = i - (length - 1) / 2
                    bestLen = length
                }
            }
            return String(chars[bestStart..<bestStart + bestLen])
        }
    
        func expand(_ chars: [Character], _ left: Int, _ right: Int) -> Int {
            var left = left
            var right = right
            while left >= 0 && right < chars.count && chars[left] == chars[right] {
                left -= 1
                right += 1
            }
            return right - left - 1
        }
    }
    

    The Swift details worth a closer look:

    • Shadowing parameters. var left = left declares a new local variable that hides the parameter of the same name. From that line on, left is the mutable copy. Without it, left -= 1 would not compile, because function parameters are constants.
    • Slicing back to a string. chars[a..<b] is an ArraySlice<Character>. Passing it to String(...) builds a String from the characters.
    • Unlabeled parameters. The _ before each name means callers write expand(chars, i, i) with no labels. Inside longestPalindrome the call is by name alone because both methods belong to the same class.
    • Short-circuit order. && stops at the first false. Reading chars[left] before checking left >= 0 would trap at run time when left reaches -1, and the program would crash.
    • Integer division. / on two Int values truncates toward zero. Here length - 1 is never negative, so it rounds down.
    • No ++ or --. Swift has no increment operators, so the code uses += 1 and -= 1.

    Complexity

    Time: O(n²). There are 2n - 1 centers, and each expansion takes at most about n / 2 steps. The final slice costs O(n).

    Space: O(1) extra, if you don't count the character array and the returned string. Beyond those, only a handful of integers are kept. A table dp[i][j] telling whether s[i..j] is a palindrome works in the same time but needs O(n²) space.

    Edge cases and common mistakes

    • Only odd centers. Without the call on (i, i + 1), "cbbd" would return "b" instead of "bb".
    • Wrong start for even palindromes. The start formula uses (length - 1) / 2 for both kinds. For "abba" the even center at index 1 gives length 4 and start 1 - 1 = 0, so the answer is "abba".
    • Ties. The comparison length > bestLen must be strict. With >=, "babad" would return "aba".
    • Single characters. "a" returns "a" and "ab" returns "a", because every single character is a palindrome of length 1.
    • Case. "Aa" returns "A": uppercase and lowercase letters differ.
    • Checking the range after the characters. Swapping the conditions in the while would index outside the array.

    Testing

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