Start learning free

LESSON 461 OF 543 · DYNAMIC PROGRAMMING

Longest Palindromic Substring in Rust: O(n²) by Expanding Around Centers

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 14
    Ctrl + Enter to check

    RUST · DYNAMIC PROGRAMMING

    Longest Palindromic Substring in Rust, explained

    Problem overview

    The function longest_palindrome 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 furthest to the left wins.

    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 occurrence at the start. The string holds English letters and digits, and uppercase and lowercase are different characters, so "Aa" is not a palindrome.

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

    Approach: grow a palindrome from its middle

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

    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 two positions, left and right, on the same character (odd case) or on two neighbours (even case). While both are inside the string and the characters match, move them one step outward. When the loop stops, the palindrome sits strictly between the two positions, so it has right - left - 1 characters. A helper named expand does this and returns that length.

    The main function calls expand twice for each index i: with (i, i) for the odd center and with (i, i + 1) for the even center. For "babad" the results are:

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

    For "cbbd", the only index with a longer result is i=1, where odd=1 and even=2: the gap between the two b's is a center.

    The program keeps only the best start and the best length. A palindrome of length characters centered at i starts at i - (length - 1) / 2, with integer division. For "babad" at i=1 with length 3 that is index 0, which gives "bab". For "cbbd" at i=1 with length 2 it is index 1, which gives "bb".

    Step by step

    1. Take the bytes. Rust doesn't let you index a String by position, so s.as_bytes() gives a slice of bytes that can be indexed. Letters and digits are one byte each, so comparing bytes compares characters.
    2. Create best_start and best_len, both 0. The substring itself is cut out only once, at the end.
    3. Loop over every i from 0 up to the number of bytes, typed as i32. The positions are i32 because expand moves left below 0 when it passes the start, which a usize can't hold.
    4. Call expand(bytes, i, i) for the longest odd palindrome around bytes[i].
    5. Call expand(bytes, i, i + 1) for the longest even palindrome around the gap after i. When the two characters differ, or i is the last index, the length is 0.
    6. Take the larger of the two with odd.max(even).
    7. Compare with the best so far using a strict >. A palindrome of equal length starts further right, and the first one must win.
    8. Compute the start as i - (length - 1) / 2. An odd palindrome has (length - 1) / 2 characters before its middle, and an even one has length / 2 - 1 before its left middle character. Both equal (length - 1) / 2 with integer division.
    9. Record the length in best_len.
    10. Cut out the result after the loop: the slice of s from best_start to best_start + best_len, turned into a new String with to_string().
    11. Write expand with the bytes as &[u8] and two i32 parameters marked mut, returning an i32.
    12. Loop while left >= 0, right is below the length, and the two bytes are equal, joined with &&.
    13. Move both ends outward: left -= 1 and right += 1.
    14. Return right - left - 1. The loop stops one step past the palindrome on each side, so the palindrome runs from left + 1 to right - 1.

    The solution in Rust

    impl Solution {
        fn longest_palindrome(s: String) -> String {
            let bytes = s.as_bytes();
            let mut best_start = 0;
            let mut best_len = 0;
            for i in 0..bytes.len() as i32 {
                let odd = Self::expand(bytes, i, i);
                let even = Self::expand(bytes, i, i + 1);
                let length = odd.max(even);
                if length > best_len {
                    best_start = i - (length - 1) / 2;
                    best_len = length;
                }
            }
            s[best_start as usize..(best_start + best_len) as usize].to_string()
        }
    
        fn expand(bytes: &[u8], mut left: i32, mut right: i32) -> i32 {
            while left >= 0 && right < bytes.len() as i32 && bytes[left as usize] == bytes[right as usize] {
                left -= 1;
                right += 1;
            }
            right - left - 1
        }
    }
    

    The Rust details worth a closer look:

    • Casts and precedence. In 0..bytes.len() as i32, the cast binds tighter than the range operator, so it is the upper bound that becomes an i32. The loop variable i, and with it best_start and best_len, are then i32 too.
    • Short-circuit order in the while. The checks left >= 0 and right < bytes.len() as i32 come first, and && stops at the first one that fails. The bytes are indexed only when both positions are in range, so left as usize never sees a negative number.
    • mut on parameters. mut left lets expand change its own copy. The caller's i is unchanged.
    • Slicing a String. s[a..b] uses byte positions and needs usize bounds, hence the two casts. It works here because each character is one byte. A string with a character that takes several bytes, like "é" (2 bytes, 1 character), would panic if a cut fell inside it.
    • to_string(). The slice is a &str borrowed from s. The function must return an owned String, and to_string() copies the piece into one.
    • Self::expand. expand takes no self, so it is called by path, like a static method.

    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 until it reaches an end.

    Space: O(1) extra. Only a few integers are kept. The returned string is the only allocation, and it isn't counted as extra space. A table dp[i][j] that records whether s[i..j] is a palindrome gives the same time but takes O(n²) space, and Manacher's algorithm reaches O(n) time, which goes beyond this lesson.

    Edge cases and common mistakes

    • Forgetting even centers. With only expand(i, i), "cbbd" would return "b" instead of "bb".
    • Using >= instead of >. A later palindrome of the same length would replace the earlier one: "babad" would give "aba" instead of "bab".
    • A single character. For "a" the odd expansion returns 1 and the even one returns 0, since right is already out of range. The answer is "a".
    • No repeated characters. For "ab" every palindrome has length 1, and the first one, "a", stays.
    • Case matters. For "Aa" the bytes differ, so the answer is "A".
    • Using usize for left. When a palindrome reaches the start of the string, left becomes -1, which a usize can't hold: left -= 1 would overflow, panicking in a debug build and wrapping in an optimized one.
    • Off-by-one in the length. The loop stops after moving one step too far, so the length is right - left - 1, not right - left.

    Testing

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