Problem overview
The function longestPalindrome gets 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 first wins.
For "babad" the answer is "bab": "aba" has the same length, but "bab" starts further left. For "cbbd" it is "bb", an even-length palindrome. For "abacdfgdcaba" it is "aba", the first of its two occurrences. Uppercase and lowercase letters are different characters, so "Aa" is not a palindrome.
Approach
The brute force checks every substring: about n²/2 of them, each needing up to n comparisons, so O(n³). A palindrome gives a better handle. 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. Starting from a center and moving outward, the palindrome grows for as long as the two characters at the ends match.
So the plan is to visit every center and expand outward from it. A string of length n has n character centers and n - 1 gaps between characters. Each expansion takes at most about n / 2 steps, so the whole search is O(n²) time with only a few variables, which is O(1) extra space besides the returned string.
A helper does the expanding. expand(s, left, right) takes the two starting ends, moves them outward while the characters match, and returns the length of the palindrome it found. From a real run on "babad":
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
Here odd is expand(s, i, i) and even is expand(s, i, i + 1). The first length-3 palindrome appears at i = 1 ("bab"). At i = 2 the length is 3 again ("aba"), but it isn't larger than the best so far, so the first one stays. For "cbbd", only i = 1 gives something longer than 1: even=2, the gap between the two b's.
Step by step
- Remember the best palindrome. Two variables,
bestStartandbestLen, both 0, hold where the best palindrome starts and how long it is. Remembering these two numbers is enough; the substring is cut out once at the end. - Visit every position.
for (i in 0 until s.length). Eachigives two centers: the characters[i]and the gap right after it. - Odd palindromes.
expand(s, i, i)starts both ends on the same character, so it finds the longest palindrome withs[i]in the middle. Store it inodd. - Even palindromes.
expand(s, i, i + 1)starts the ends on neighbouring characters, so it finds the longest palindrome centered on the gap. If they differ, oriis the last index, the length is 0. Store it ineven. - Take the larger.
val length = maxOf(odd, even)is the longest palindrome centered ati. - Compare strictly.
if (length > bestLen). A palindrome just as long as the best starts further right, and the one that starts first must win, so ties don't replace it. - Compute the start.
bestStart = i - (length - 1) / 2. An odd palindrome has(length - 1) / 2characters before its middles[i]. An even one haslength / 2 - 1characters befores[i], its left middle. With integer division both equal(length - 1) / 2. For length 3 that is 1, and for length 4 it is also 1. - Save the length.
bestLen = length. - Cut out the answer. After the loop,
s.substring(bestStart, bestStart + bestLen). The end index ofsubstringis exclusive, so this takes exactlybestLencharacters. - Write
expand. It is a method of the same class, solongestPalindromecalls it by name alone. It takess: String,left: Intandright: Intand returns anInt. - Copy the parameters.
var left = leftandvar right = right. - Loop while the ends match. The condition has three parts joined by
&&:left >= 0,right < s.lengthands[left] == s[right]. The first two keep both ends inside the string, and&&stops at the first failure, so the characters are compared only when both indexes are valid. - Move outward.
left--andright++. - Return the length. The loop ends one step past the palindrome on both sides, so the palindrome runs from
left + 1toright - 1, which isright - left - 1characters. If the starting characters differ, the loop never runs and the result is 0.
The solution in Kotlin
class Solution {
fun longestPalindrome(s: String): String {
var bestStart = 0
var bestLen = 0
for (i in 0 until s.length) {
val odd = expand(s, i, i)
val even = expand(s, i, i + 1)
val length = maxOf(odd, even)
if (length > bestLen) {
bestStart = i - (length - 1) / 2
bestLen = length
}
}
return s.substring(bestStart, bestStart + bestLen)
}
fun expand(s: String, left: Int, right: Int): Int {
var left = left
var right = right
while (left >= 0 && right < s.length && s[left] == s[right]) {
left--
right++
}
return right - left - 1
}
}
Kotlin details worth knowing:
- Parameters are read-only. Writing
left--on a parameter is a compile error:'val' cannot be reassigned. That is why the function copies them intovarvariables first. A local variable with the same name hides the parameter for the rest of the function body, soleftandrightthere refer to the copies. s[left]reads aChar, and==on twoCharvalues compares them by value./on twoIntvalues drops the remainder. Herelength - 1is never negative, so dropping the remainder means rounding down.substring(start, end)excludesend.
Complexity
There are 2n - 1 centers, and each expansion moves at most about n / 2 steps outward, so the time is O(n²); the worst case is a string like "aaaa", where every expansion goes far. The code keeps a handful of integer variables, so the extra space is O(1), apart from the returned substring.
Edge cases and common mistakes
- Only odd centers. Without the
expand(s, i, i + 1)call,"cbbd"would give"b"instead of"bb". - Using
>=in the comparison. A later palindrome of the same length would replace the earlier one. For"babad"the result would be"aba", not"bab". - Checking the characters before the bounds.
s[left] == s[right]must come last in the condition; written first, it can read index-1ors.lengthand throw an exception. - Returning
right - leftfromexpand. That is off by one: the ends have stepped past the palindrome by one position on each side. - Single character.
"a"gives"a": the odd expansion returns 1, and the even one returns 0 becausei + 1is out of range. - No repeats.
"ab"gives"a", the first single character, because ties keep the first. - Case matters.
"Aa"gives"A".
Testing
Results from running the solution:
"babad":"bab""cbbd":"bb""abacdfgdcaba":"aba""a":"a""ab":"a""racecar":"racecar""aaaa":"aaaa""forgeeksskeegfor":"geeksskeeg""Aa":"A"
