-
Notifications
You must be signed in to change notification settings - Fork 0
133 — Longest Palindromic Substring
LeetCode 5 · Medium. Given a string s, return the longest substring of s that reads the same forwards and backwards.
Every palindrome has a center — either a single character (for odd-length palindromes like "aba") or a gap between two characters (for even-length palindromes like "abba"). Instead of asking "is this substring a palindrome?" for every one of the O(n^2) possible substrings, flip the question around: for every one of the 2n - 1 possible centers, how far can you expand outward before the two sides stop matching? That reframing turns "check a candidate" into "grow a candidate," and growing stops the instant it fails — no wasted comparisons on substrings that were never going to work.
"Longest substring that reads the same forwards and backwards" is the palindrome-substring tell. Whenever the question is about the longest (not just counting) palindromic substring, expand-around-center is almost always the efficient path: pick every possible center, grow outward symmetrically, and track the widest successful expansion.
Check every possible substring for the palindrome property directly, using a straightforward O(n) two-pointer check per candidate.
func longestPalindromeBruteForce(_ s: String) -> String {
let chars = Array(s)
let n = chars.count
guard n > 0 else { return "" }
func isPalindrome(_ left: Int, _ right: Int) -> Bool {
var l = left, r = right
while l < r {
if chars[l] != chars[r] { return false }
l += 1
r -= 1
}
return true
}
var bestStart = 0
var bestLength = 1
for start in 0..<n {
for end in start..<n {
let length = end - start + 1
if length > bestLength && isPalindrome(start, end) {
bestStart = start
bestLength = length
}
}
}
return String(chars[bestStart..<(bestStart + bestLength)])
}
// smoke test
print(longestPalindromeBruteForce("babad")) // "bab" (or "aba")
print(longestPalindromeBruteForce("cbbd")) // "bb"
print(longestPalindromeBruteForce("a")) // "a"Big-O: O(n^3) time — O(n^2) substrings, each checked with an O(n) palindrome scan in the worst case. O(n) space for the chars array.
For every possible center (both single-character and between-character centers), expand outward one step at a time while the two sides still match, and remember the widest successful expansion.
func longestPalindrome(_ s: String) -> String {
let chars = Array(s)
let n = chars.count
guard n > 0 else { return "" }
var bestStart = 0
var bestLength = 1
func expand(_ left: Int, _ right: Int) {
var l = left, r = right
while l >= 0 && r < n && chars[l] == chars[r] {
l -= 1
r += 1
}
// l, r overshot by one on both sides once the match broke (or a bound was hit)
let length = r - l - 1
if length > bestLength {
bestLength = length
bestStart = l + 1
}
}
for center in 0..<n {
expand(center, center) // odd-length palindromes, centered on one character
expand(center, center + 1) // even-length palindromes, centered on a gap
}
return String(chars[bestStart..<(bestStart + bestLength)])
}
// smoke test — same cases as the brute force
print(longestPalindrome("babad")) // "bab" (or "aba")
print(longestPalindrome("cbbd")) // "bb"
print(longestPalindrome("a")) // "a"Big-O: O(n^2) time — 2n - 1 centers, each expansion taking up to O(n) steps in the worst case (e.g. all identical characters). O(n) space for the chars array, O(1) extra beyond that.
The brute force treats "is this a palindrome?" as an independent question for every one of the O(n^2) substrings, paying a fresh O(n) scan each time even though most candidates fail almost immediately at their very outer edges. Expand-around-center flips the direction of the check: instead of validating a substring end-to-end, it grows a substring from the inside out and stops the instant a mismatch is found, so short-lived candidates cost almost nothing. Both approaches are ultimately bounded by O(n^2) total character comparisons in the worst case, but expand-around-center gets there by trying every center once (2n - 1 of them) rather than every substring once (O(n^2) of them, each independently re-scanned) — a smaller, non-redundant set of starting points doing the same job.
-
1D Dynamic Programming — "is
s[l...r]a palindrome?" has the classic 1-D-collapsed recurrenceisPal(l, r) = (s[l] == s[r]) && isPal(l+1, r-1); expand-around-center walks that recurrence outward from the base case instead of filling a 2-D table. -
Two Pointers —
expandis a textbook two-pointer walk, movinglandroutward in lockstep from a shared center until the matching condition breaks. -
Arrays and Strings — converting
stoArray(s)up front gives O(1) indexed character access, which both versions rely on throughout.
Longest Palindromic Substring is grown, not checked — plant a center (on a character or between two), expand outward while the edges keep matching, and remember the widest expansion that ever succeeded.
- Why does the loop call
expand(center, center)andexpand(center, center + 1)for every center, instead of just one of the two? - In
expand, after thewhileloop exits, why is the palindrome's lengthr - l - 1rather thanr - l + 1? - For
s = "cbbd", traceexpandatcenter = 1for both the odd and even calls. Which one produces the winning"bb", and why does the odd-centered call atcenter = 1fail to beat length 1?
⬅️ Previous: House Robber II · Next: Palindromic Substrings ➡️