Given a string, find the longest substring that's also a palindrome. A palindrome is a string that reads the same, forwards and backwards.
Input: abc Output: a Input: bananas Output: anana
Solution | Time | Space |
---|---|---|
Brute force | O(n3) | O(1) |
Dynamic programming | O(n2) | O(n2) |
Central expansion | O(n2) | O(1) |
Manacher's algorithm | O(n) | O(n) |
Use two loops to iterate over all possible substrings:
For each substring, check if it's a palindrome.
Afterwards, we'll have the start and end indices of the longest palindrome.
def longest_palindromic_substring(s) n = s.length i_start = i_end = 0 (0 .. n-2).each do |i| (i+1 .. n-1).each do |j| substr = s[i .. j] if substr == substr.reverse && j - i > i_end - i_start i_start, i_end = i, j end end end s[i_start .. i_end] end
Time | O(n3) | Each of O(n2) substrings takes O(n) time to check |
Space | O(1) | The start/end indices of the longest palindrome |
This improves the runtime of the brute force solution by checking whether a substring is a palindrome in O(1) time instead of O(n) time.
It saves time at the expense of space by storing whether a substring is a palindrome in a 2D array, and referencing these values for each longer substring.
Let each value of is_pal[i][j] be
We can check if a substring s[i .. j] is a palindrome in O(1) time:
def longest_palindromic_substring(s) n = s.length return s if n <= 1 is_pal = Array.new(n) { Array.new(n) } i_start = i_end = 0 (n - 1).downto(0).each do |i| i.upto(n - 1).each do |j| if j - i < 3 is_pal[i][j] = s[i] == s[j] else is_pal[i][j] = s[i] == s[j] && is_pal[i + 1][j - 1] end if is_pal[i][j] && j - i > i_end - i_start i_start, i_end = i, j end end end s[i_start .. i_end] end
Time | O(n2) | Each of O(n2) substrings takes O(1) time to check |
Space | O(n2) | 2D array of booleans for O(n2) substrings |
This improves on the previous two solutions by iterating over all possible centers of palindromes (2n - 1) ~ O(n) instead of iterating over all possible substrings.
A center can be represented by a pair of string indices [i, j]
The center of a palindrome can begin either
For each center [i, j], check if the first and last letter match.
Afterwards, we'll have the start and end indices of the longest palindrome.
def longest_palindromic_substring(s) n = s.length return s if n <= 1 i_start = i_end = 0 (0 .. (2 * n - 1)).each do |i_center| i = j = i_center / 2 j += 1 if i_center % 2 == 1 while i >= 0 and i < n and j >= 0 and j < n break if s[i] != s[j] i -= 1 j += 1 end i += 1 j -= 1 if j - i > i_end - i_start i_start, i_end = i, j end end s[i_start .. i_end] end
Time | O(n2) | Each of 2n - 1 centers takes O(n) time to check |
Space | O(1) | The start/end indices of the longest palindrome |
This algorithm combines the ideas explored in solutions 2 and 3:
By using the fact that the left and right sides of a palindrome are mirror images of each other, it avoids re-checking palindromes that have already been found.
If a shorter palindrome is located within the left of the center of a longer palindrome, this shorter palindrome is guaranteed to exist on the right side of the longer palindrome as well. When reaching the center of the shorter palindrome on the right side, the central expansion can begin at the known ends of the palindrome rather from the center.
def longest_palindromic_substring(s) t = s.split("").map {|c| "##{c}" }.join + "#" t = "^#{t}$" p_lengths = Array.new(t.length, 0) center = right = 0 (1 .. t.length-1).each do |i| mirror = 2*center - i if i < right p_lengths[i] = [right - i, p_lengths[mirror]].min end while t[i - (1+p_lengths[i])] == t[i + (1+p_lengths[i])] p_lengths[i] += 1 end if p_lengths[i] + i > right center = i right = p_lengths[i] + i end end max_length = p_lengths.max center = p_lengths.index(max_length)/2 - 1 p_start = center - max_length/2 if max_length % 2 == 0 p_start += 1 end p_end = center + max_length/2 s[p_start .. p_end] end
Time | O(n) | Iterating over 2n centers |
Space | O(n) | 2n space for expanded string with boundary characters |