Strings: Longest Palindromic Substring
By: Danny Pham
Tags: Leetcode, Python, Software Engineer, Algorithms, Coding Interviews

Problem Description

Given a string s, return the longest palindromic substring in s.

Example 1: Input: s = "babad", Output: "bab", Note: "aba" is also a valid answer. Example 2: Input: s = "cbbd", Output: "bb" Example 3: Input: s = "a", Output: "a"

Brute Force Solution

The naive way to solve this problem is to find all substrings by considering every possible start and end position and verify that it’s a palindrome. In the end, return the longest palindrome you found. The time complexity of this solution is O(n³). Let’s see why.

Brute Force Solution

What does this code do?

For every start position, i , consider every end position, j , and extract the substring. Compare the substring to the reverse of itself to check whether the substring is a palindrome. If the substring is a palindrome, check to see if its length is longer than the previously captured palindromic substring. If the length of this new substring is longer, update longest_palindrome to this new substring.

To capture every start and position, this algorithm requires a double for-loop which will have an O(n²) time complexity. However, for each start and end position, we perform a comparison of the substring and its reversed version. In Python, to reverse a string, we do s[i:j][::-1] , which will result in O(n) time.

Time complexity: O(n³), space complexity: O(1)

Let’s see if we can do better.

Better Solution

A better solution would need to have a time complexity better than O(n³). One way to find palindromic substrings is to check them from the middle outwards. What does that mean?

We’ll loop through the string and for each character, we will consider that character as the “middle” of a possible palindromic substring. From there, we compare the character on its left side and right side. If those two characters match, we can continue “building the palindrome” by expanding both leftward and rightward.

Let’s look at an example where s = "aba" . When we consider the character "b" as the center, we see that the character to its left and right are equal, so we can continue expanding. Ultimately, we see that the longest palindromic substring when centered around "b" is 3 and ends up being the solution.

However, this algorithm only works on odd-length palindromes. Let’s look at when this algorithm fails for even-length palindromes; let’s consider s = "abba" . When we consider the first instance of "b" , we see that the character to its left and right are not equal, so the longest palindromic substring around that character is 1. This is the same case for the second instance of "b" . This results in an invalid solution because the algorithm will see that the longest palindromic substring for "abba" is only 1, whereas the correct solution is 4.

In order to get around this problem, we are going to consider each character as both the center character and the left-centered character when attempting to build a palindrome (left-centered is defined as the left middle character when the string is even).

Let’s run this new logic on "abba" again. For the first instance of "b" , if we consider the character as the center of a possible palindrome, we will get the same result above: the longest palindrome around this character is 1. However, things get interesting when we consider it as the left-centered character. If the first instance of "b" is considered the left-centered character, we must compare it to the right-centered character, which is the second instance of "b" . These two characters are equal and we can expand leftward and rightward. We compare the next two characters, which are "a" and "a" and see that they are equal. In the end, we see that the length of the longest substring is 4, which is indeed the correct answer.

Let’s code this solution up.

Better Solution

What is the time complexity of this solution?

As we loop through the given string, for each character, we attempt to “build a palindrome” centered (or left-centered) around the character which takes O(n) time.

Time complexity: O(n²), space complexity: O(1)

Can we do better than this…?

Optimized Solution

Let’s take a closer look at the expand_around_center helper function. The while loop to expand leftward and rightward takes O(n) time. This is unavoidable. However, when we return the palindromic substring, this takes O(n) time as well (or O(k) time where k = j — i). Can we avoid this additional incurred O(n) time operation?

Fortunately, yes. Why can’t we just return the length of the palindrome we built? For instance, referring back to the example where s = "abba" . If we were building a palindrome around the first instance of "b" , instead of returning "abba" from expand_around_center , we should just return 4.

By doing this, we would have to include two additional variables in our function: start and end. These two variables indicate the start and end indices of the longest palindromic substring to return.

Let’s look at this solution.

Here are the changes that were made to optimize the solution.

In expand_around_center , we are now returning the length of the palindrome we found instead of returning a substring of the palindrome. This eliminates the incurred cost of performing a slice in the string which was O(n) time.

, we are now returning the length of the palindrome we found instead of returning a substring of the palindrome. This eliminates the incurred cost of performing a slice in the string which was O(n) time. Now, since expand_around_center returns the length instead of the substring, we need to do some comparisons with the currently set start and end variables to see if this palindromic substring is longer than what we’ve already found. The length of the previously found palindromic substring can be found by just doing end — start . Simple.

returns the length instead of the substring, we need to do some comparisons with the currently set and variables to see if this palindromic substring is longer than what we’ve already found. The length of the previously found palindromic substring can be found by just doing . Simple. If this newly found palindromic substring is longer, we now have to update start and end . We know that the current character is the center (or left-centered) of the palindromic substring. That means, to find the start index, we need to subtract the (length of the substring subtracted by 1) divided by 2 from the current index (we subtract 1 to avoid being out of bounds). Similarly, for the end index, we can add the length of the substring divided by 2 from the current index.

Time complexity: O(n²), space complexity: O(1)

Note: although the time complexity of this solution is also O(n²), it is slightly more optimized.

There are more optimal solutions than this one, but I believe this will suffice for a technical interview. However, if you’re interested in reading about an O(n) solution to this problem, check out Manacher’s Algorithm.

Hopefully this helps and I’ll see you on the next problem!