Alisa Bajramovic

Posted on

# The Longest Palindromic Substring: Solving the Problem Using Constant Space

Today's algorithm of the day is the Longest Palindromic Substring:

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

For example, let's say you were given the string "prefer". The output of the function should be "refer", because that is the longest substring of "prefer" which is a palindrome.

A palindrome is a word that is the same forwards and backwards--for example, "kayak", "level", and "noon". A substring is a continuous series of characters in a string--for example, "flow" is a substring of "flower". This problem is asking you to find the longest substring which is a palindrome in a given string.

Like most algorithms, there's many ways to solve this problem, but today I'll be solving it using the "expand around the center" method. The advantage of this method is that it uses constant space (O(1)). Although it uses O(n^2) time, the very little space it takes up is really interesting to me, so I wanted to give this approach a try.

I'll start by going over the approach behind this problem. Then I'll go on to code the solution in JavaScript. Finally, I'll illustrate how it works with an example.

## Expanding Around the Center: Approaching the Problem

Let's say you're given the string "watt". To find the longest palindromic substring, you'd want to check all of the points in the string and see if the left and right of that point are identical. We can call all of those points "centers". You may think there are 4 centers in "watt", because it's 4 characters long--however, there are actually 7 centers in "watt", or `2n - 1` centers in a string of length `n`.

The reason why this is the case is that the space between each letter is also a "center"--that is, a substring may have an even number of characters, and so there is no single "middle" letter.

In the example of "watt", the longest substring is "tt", which means its center is the space between "t" and "t".

So in the expanding around the center approach, we'll iterate through each character in the given string, and will check not only the substring that has a center at each character, but also the substring that has a center between any two characters.

## Solving for the Longest Palindromic Substring

To start solving this problem, we can account for edge cases. If the given string is less than a character long, we can simply return an empty string--there's no "substring" of an empty string.

``````function longestPalindrome(s) {
if (s.length < 1) return "";
//...
}
``````

Now, we'll want to keep track of where the longest palindromic substring starts, and how long it is. We want to do this so that we can return that section of the inputted string at the end. We can set both of those values equal to 0 to start. We also can include a return statement at the bottom of the function to return the maximum substring. When called on a string, the method `.substr()` returns the the substring of a string. The first parameter passed in is the starting index of the substring you want to return, and the second (optional) parameter is the number of characters you want to return. Therefore, we can return the substring that starts at `maxSubStart` and is `maxSubLength` characters long.

``````function longestPalindrome(s) {
if (s.length < 1) return "";
let maxSubStart = 0;
let maxSubLength = 0;
//...
return s.substr(maxSubStart, maxSubLength);
}
``````

Now, we'll want to walk through each character in `s` and perform checks on the substring at each step, so this is a good time to use a for-loop.

At each character in `s`, we'll want to check the substring which has a center at that character, and the substring which has a center between that character and the following character. We will write a helper function, `expandAroundCenter` to do this. `expandAroundCenter` will take in the string, the left parameter, and the right parameter. So, inside the for loop, we can call `expandAroundCenter` twice: once where `left` and `right` both equal the character we're currently on, and once where `left` equals the character we're currently on and `right` equals the next character in `s`.

``````function longestPalindrome(s) {
if (s.length < 1) return "";
let maxSubStart = 0;
let maxSubLength = 0;
for (let i = 0; i < s.length; i++) {
const lengthCenteredAtChar = expandAroundCenter(s, i, i);
const lengthCenteredAtSpace = expandAroundCenter(s, i, i + 1);
//...
}
return s.substr(maxSubStart, maxSubLength);
}
``````

We'll get back to writing the helper function in a minute. For now, we can continue writing the function we're on. `expandAroundCenter` will return lengths, and we want to know which one is longer: the substring that's centered at the character, or the substring that's centered at the space. So, we can use Math.max() and pass in both of those lengths. Whichever one is longer, we can set equal to a variable, `longestSubAtChar`, which is the longest substring at each character.

Then, we'll want to see if the longest substring at the character we're on is longer than the maximum substring we've seen so far. To check this, we can write a conditional statement inside the for loop.

``````function longestPalindrome(s) {
if (s.length < 1) return "";
let maxSubStart = 0;
let maxSubLength = 0;
for (let i = 0; i < s.length; i++) {
const lengthCenteredAtChar = expandAroundCenter(s, i, i);
const lengthCenteredAtSpace = expandAroundCenter(s, i, i + 1);
const longestSubAtChar = Math.max(lengthCenteredAtChar, lengthCenteredAtSpace)
if (longestSubAtChar > maxSubLength) {
//...
}
}
return s.substr(maxSubStart, maxSubLength);
}
``````

If the current substring is longer than the maximum substring seen so far, we'll want to make the current substring the maximum. We'll do this by setting `maxSubLength` equal to `longestSubAtChar`.

We'll also want to change the starting point of the maximum substring so that we can return the correct substring at the end of the function. We can find the starting point by finding the halfway point of `longestSubAtChar`, and subtracting that from the character we're on.

In the example of "lava", the maximum substring is "ava", the center is "v" (index 2), and the start of that substring is "a" (index 1). In the example of "wattage", the maximum substring is "atta", the center is between "t" and "t" (index 2 and 3), and the start of that substring is "a" (index 1).

Finding half of the length of the substring means taking the length and subtracting 1, dividing that by 2, and performing Math.floor() on that calculation. Then, to find the start of the substring, subtract that number from `i`. (Note: you can see why you need to subtract 1 by looking at the example of "wattage". If we just divided 4 (the maxSubLength) by 2, we'd get 2. 2 (i) minus 2 is 0. The substring starts at 1, not 0. Subtracting one accounts for substrings of even lengths.)

``````function longestPalindrome(s) {
if (s.length < 1) return "";
let maxSubStart = 0;
let maxSubLength = 0;
for (let i = 0; i < s.length; i++) {
const lengthCenteredAtChar = expandAroundCenter(s, i, i);
const lengthCenteredAtSpace = expandAroundCenter(s, i, i + 1);
const longestSubAtChar = Math.max(lengthCenteredAtChar, lengthCenteredAtSpace)
if (longestSubAtChar > maxSubLength) {
maxSubLength = longestSubAtChar;
maxSubStart = i - Math.floor((maxSubLength - 1) / 2);
}
}
return s.substr(maxSubStart, maxSubLength);
}
``````

We're now done with `longestPalindrome()`, and we just need to write the function which checks the substring at each center, `expandAroundCenter()`. `expandAroundCenter()` will take in the string, a left index, and a right index. We'll want to keep checking the letters at each left and right index to see if they're equal to each other as long as we're within the bounds of the string--so left has to be greater than or equal to 0, and right has to be less than the length of the string. We'll want to have a while loop continue to run so long as the characters at the left and right index are equal to each other, and we're still within the bounds of the string.

``````function longestPalindrome(s) {
if (s.length < 1) return "";
let maxSubStart = 0;
let maxSubLength = 0;
for (let i = 0; i < s.length; i++) {
const lengthCenteredAtChar = expandAroundCenter(s, i, i);
const lengthCenteredAtSpace = expandAroundCenter(s, i, i + 1);
const longestSubAtChar = Math.max(lengthCenteredAtChar, lengthCenteredAtSpace)
if (longestSubAtChar > maxSubLength) {
maxSubLength = longestSubAtChar;
maxSubStart = i - Math.floor((maxSubLength - 1) / 2);
}
}
return s.substr(maxSubStart, maxSubLength);
}

function expandAroundCenter(s, left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
//...
}
//...
}
``````

Inside the while loop, all we'll want to do is to continue to expand left and right. That means the left pointer should get smaller (go more toward the left), and the right pointer should get larger (go more toward the right).

Finally, once we're done executing the while loop (we're either out letters in `s` to check, or we've come to a point where the substring is no longer a palindrome we'll want to return the distance between `left` and `right` back to `longestPalindrome()`. To do this, we can just return `right - left - 1`.

``````function longestPalindrome(s) {
if (s.length < 1) return "";
let maxSubStart = 0;
let maxSubLength = 0;
for (let i = 0; i < s.length; i++) {
const lengthCenteredAtChar = expandAroundCenter(s, i, i);
const lengthCenteredAtSpace = expandAroundCenter(s, i, i + 1);
const longestSubAtChar = Math.max(lengthCenteredAtChar, lengthCenteredAtSpace)
if (longestSubAtChar > maxSubLength) {
maxSubLength = longestSubAtChar;
maxSubStart = i - Math.floor((maxSubLength - 1) / 2);
}
}
return s.substr(maxSubStart, maxSubLength);
}

function expandAroundCenter(s, left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
left--;
right++;
}
return right - left - 1;
}
``````

## Checking the Code With an Example

With that, we're done writing the solution to this problem. To check how this all works, I like walking through an example. I'll use the string "ABA". Even though the string is short, there are a lot of steps in this algorithm, so walking through it will take a bit of time. Nonetheless, I think it's super valuable to see how an example plays out.

We start with "ABA", and the maxSubStart and maxSubLength both automatically equal 0.

Now, we'll enter the for loop, and begin checking the character at index 0. We'll call `expandAroundCenter()` twice, once with left and right both at 0, and once with left at 0 and right at 1.

First we'll call `expandAroundCenter` where both left and right equal 0. That means the center is the "A" at index 0. Since left is greater than or equal to 0, right is less than the length of the string, and the value at left and right are equal, we'll expand the center.

Now, left is -1 and right is 1. The while loop, however, is no longer true. That means that we won't enter the loop, and will return `right - left - 1`, which equals 1.

Now we'll call `expandAroundCenter` with left = 0 and right = 1. That means the center is between "A" and "B". Since the character at the left index does not equal the character at the right index, we won't enter the while loop, and will return 0.

We're back to our function. We can compare the return values of both calls to expandAroundCenter, and since 1 > 0, longestSubAtChar will equal 1. The current maximumSubLength is 0, and since 1 > 0, the maxSubLength will equal 1. We can set maxSubStart equal to 0, as that's the index that the maximum palindromic substring ("A") started at.

We can move on to checking "B" at index 1. We'll call expandAroundCenter twice, once where the center is the letter "B", and once where the center is the space between "B" and the next letter "A".

First we'll check where the center is "B". Left is 1 and right is 1, which are both inside the bounds of the string, and "B" === "B", so we can enter the while loop. We will expand from the center, decrementing left, and incrementing right.

Now left is 0 and right is 2. Both of these values are inside of the bounds of the string, and the characters at these values are equal to each other ("A" === "A"), so we can go through the while loop again.

Now left is -1 and right is 3. Since left is no longer greater than or equal too 0, we don't even have to check the rest of the conditional, because we know we can't enter the while loop. We will return 3 back to the function.

We'll check where the center is the space between "B" and "A". Left is 1 and right is 2. However, since "B" does not equal "A", we can't enter the while loop, so we'll return 0 to the function.

Now we can compare the return values of both calls to expandAroundCenter. Since 3 is larger than 0, it's the longestSubAtChar. Since 3 is larger than the previous maximum substring (1), 3 becomes the new maxSubLength, and the maxSubStart is 0.

We can move to the last letter of the string, "A", and i = 2. We'll again call "expandAroundCenter" twice, once for each potential "center".

First we'll look at the substring which is centered around A. Left = 2 and right = 2 (both inside the confines of the string), and "A" === "A", so we can enter the while loop and expand from the center.

Now left is 1 and right is 3. Even though left is greater than 0, right is outside the confines of the string, so we can't enter the while loop. We'll return 1 to the function.

We'll now call expandAroundCenter with left = 2 and right = 3. Since 3 is larger than the length of the string, we won't enter the while loop. We can return 0 to the function.

Back in the function, we can compare the two longest substrings at this index in the string. The longest one is 1 character long (the letter "A"). Since 1 is not larger than the existing maximum substring length, we won't change the maximum substring values.

Since we're done checking the characters of the string, we can return the maximum substring--it starts at index 0, and is three characters long, which is "ABA".

--

Please let me know if you have any questions or alternative solutions to this problem!

Hi. A really a nice approach! I was wondering about the time complexity. I sometimes find it difficult to determine the time complexity of an algo with certainty, even the ones i write. Could you please elaborate on that... I wrote a different algorithm, just trying to judge if this one has a better complexity than mine, because mine does not eally perform that efficiently:

``````/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
let len = s.length;
while (len>0) {
for (let i=0; i<s.length-len+1; i++) {
let str = s.slice(i, i+len);
if (isPalindrom(str))
return str;
}
len--;
}
return "";
};

const isPalindrom = function(arr) {
let i=0;
let j=arr.length-1;
while (i<=j) {
if (arr[i]!==arr[j])
return false;
i++; j--;
}
return true;
}
``````

Alisa Bajramovic

Hi Saloni,
In the algorithm I used above, the reason its time complexity is O(n^2) can be found by looking at the for loop. The for loop examines each element of `s`, which takes O(n) time (n being the length of `s`). Then, inside the for loop, I call `expandAroundCenter`. In `expandAroundCenter`, I'm again traversing the string (even though I may just be looking at one character, I could be looking at the entire string again, like in the case of checking the "B" in "ABA"). That would take, at most, O(n) time. Because expandAroundCenter is nested inside the for loop, that means it takes a total of O(n^2) time.

In your function, you have a while loop, which goes from the end of the string to the start. In other words, you could write `while (len>0) {...` as `for (let i = len; i > 0; i--) {`. So in that line, that's O(n). Then inside the loop, you have another for loop. If you think about the example of "AAAA", the first time through the while loop, len would = 4, and inside the for loop, str would = "AAAA". Then, `isPalindrom` would be called, and it'd start at either end, and end up checking every character of the string, which is also O(n). So, it looks like overall the time would be O(n^2).