Skip to content

Palindrome Patterns

Palindromes are a favourite interview topic because the same word covers three genuinely different difficulty tiers:

  1. Verify one string is a palindrome — two pointers, O(n)O(n), Easy.
  2. Find palindromic substrings inside a string — expand around centre, O(n2)O(n^2), Medium.
  3. Build a palindrome by deleting or inserting — interval DP, O(n2)O(n^2), Medium to Hard.

The mistake that costs the most time is reaching for tier 3 machinery on a tier 2 problem. This page separates them, and gives you the cue for each.

What you’ll learn

  • The two-pointer verification template, and the “at most one deletion” variant.
  • Expand around centre: why there are 2n - 12n - 1 centres, not nn.
  • Why “substring” means expand-around-centre and “subsequence” means DP — the single most useful distinction on this page.
  • Three real LeetCode problems solved in the browser: 5, 647, 680.

The cue

Tier 1 — verify with two pointers

is_palindrome.py
def is_palindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True
 
 
print(is_palindrome("racecar"))   # True
print(is_palindrome("abca"))      # False
is_palindrome.py
def is_palindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True
 
 
print(is_palindrome("racecar"))   # True
print(is_palindrome("abca"))      # False

O(n)O(n) time, O(1)O(1) space. s == s[::-1]s == s[::-1] is also O(n)O(n) but allocates a reversed copy, so it is O(n)O(n) space — fine to mention, and fine to use unless asked for constant space.

For LC 125 the only additions are skipping non-alphanumeric characters and lower-casing:

valid_palindrome_125.py
def is_palindrome_alnum(s):
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():    # skip junk
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True
 
 
print(is_palindrome_alnum("A man, a plan, a canal: Panama"))   # True
valid_palindrome_125.py
def is_palindrome_alnum(s):
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():    # skip junk
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True
 
 
print(is_palindrome_alnum("A man, a plan, a canal: Panama"))   # True

Tier 2 — expand around centre

Every palindrome has a centre. Grow outward from each possible centre while the characters match; each expansion step is O(1)O(1) and finds a new palindrome.

The detail that catches people: a palindrome of even length has its centre between two characters. So there are 2n - 12n - 1 centres — nn single-character centres and n - 1n - 1 gaps.

expand_around_centre.py
def longest_palindrome(s):
    if not s:
        return ""
    best_left, best_len = 0, 1
 
    def expand(left, right):
        nonlocal best_left, best_len
        while left >= 0 and right < len(s) and s[left] == s[right]:
            if right - left + 1 > best_len:
                best_left, best_len = left, right - left + 1
            left -= 1
            right += 1
 
    for i in range(len(s)):
        expand(i, i)          # odd-length centre: a single character
        expand(i, i + 1)      # even-length centre: the gap after i
 
    return s[best_left:best_left + best_len]
 
 
print(longest_palindrome("babad"))   # "bab"  ("aba" is equally correct)
print(longest_palindrome("cbbd"))    # "bb"
expand_around_centre.py
def longest_palindrome(s):
    if not s:
        return ""
    best_left, best_len = 0, 1
 
    def expand(left, right):
        nonlocal best_left, best_len
        while left >= 0 and right < len(s) and s[left] == s[right]:
            if right - left + 1 > best_len:
                best_left, best_len = left, right - left + 1
            left -= 1
            right += 1
 
    for i in range(len(s)):
        expand(i, i)          # odd-length centre: a single character
        expand(i, i + 1)      # even-length centre: the gap after i
 
    return s[best_left:best_left + best_len]
 
 
print(longest_palindrome("babad"))   # "bab"  ("aba" is equally correct)
print(longest_palindrome("cbbd"))    # "bb"
ApproachTimeSpace
Check every substringO(n3)O(n^3)O(1)O(1)
DP table is_pal[i][j]is_pal[i][j]O(n2)O(n^2)O(n2)O(n^2)
Expand around centreO(n2)O(n^2)O(1)O(1)
Manacher’s algorithmO(n)O(n)O(n)O(n)

Expand-around-centre is the interview answer: same time as the DP with O(1)O(1) space, and far less code. Manacher’s is the theoretically optimal O(n)O(n) solution — know that it exists and is the answer to “can you do better than O(n2)O(n^2)”, but nobody sane expects you to derive it live.

Tier 3 — when it becomes DP

Once you may skip characters (subsequence) or modify the string (insert/delete), centres stop helping and you need interval DP over (i, j)(i, j) ranges.

The one identity worth memorising:

min insertions to make s a palindrome=nLPS(s)\text{min insertions to make } s \text{ a palindrome} = n - \text{LPS}(s)

where LPS\text{LPS} is the longest palindromic subsequence — and LPS(s)=LCS(s,reverse(s))\text{LPS}(s) = \text{LCS}(s, \text{reverse}(s)). So LC 1312 reduces to a longest-common-subsequence you already know how to write. These live in Classic DP and DP on Grids and Intervals.

Practice — real LeetCode problems

LC 5 — Longest Palindromic Substring · Medium

Problem. Given a string ss, return the longest palindromic substring in ss.

Constraints. 1 <= len(s) <= 10001 <= len(s) <= 1000, digits and English letters.

Examples. "babad""babad" gives "bab""bab""aba""aba" is also accepted · "cbbd""cbbd" gives "bb""bb"

Editorial — approach, complexity, follow-ups

Every palindromic substring is determined by its centre and its radius. Try all 2n - 12n - 1 centres and expand each while the characters match; the longest one found is the answer.

Time O(n2)O(n^2)2n - 12n - 1 centres, each expanding at most O(n)O(n). Space O(1)O(1) beyond the output.

Track (best_left, best_len)(best_left, best_len) instead of building strings inside the loop. Slicing on every improvement turns the inner work into O(n)O(n) and the whole thing into O(n3)O(n^3) in the worst case (a long run of identical characters).

Follow-ups you should expect:

  • “Can you do better than O(n2)O(n^2)?” Yes — Manacher’s algorithm is O(n)O(n). It transforms the string (interleaving separators so every palindrome is odd-length) and reuses previously computed radii via a mirror argument. Naming it and sketching why it works is the expected depth; deriving it live is not.
  • “Count them instead of finding the longest?” LC 647 — same expansion, increment a counter on every successful step.
  • “Longest palindromic subsequence?” Different problem entirely — interval DP, LC 516. "bbbab""bbbab" gives 44, not 33.
  • “Why is the DP table worse?” Same O(n2)O(n^2) time but O(n2)O(n^2) space, and more code. It is only preferable when you need the full is_pal[i][j]is_pal[i][j] table for something else — which is exactly what LC 131 Palindrome Partitioning wants.

LC 647 — Palindromic Substrings · Medium

Problem. Given a string ss, return the number of palindromic substrings in it. Substrings at different positions count separately even if they are identical.

Constraints. 1 <= len(s) <= 10001 <= len(s) <= 1000, lowercase English letters.

Examples. "abc""abc" gives 33 ("a""a", "b""b", "c""c") · "aaa""aaa" gives 66 ("a""a"×3, "aa""aa"×2, "aaa""aaa")

Editorial — approach, complexity, follow-ups

This is LC 5 with the accumulator swapped. Each time an expansion succeeds, the current [left, right][left, right] window is itself a palindromic substring, so increment.

Time O(n2)O(n^2). Space O(1)O(1).

Worth checking "abba""abba" by hand to see all six: "a""a", "b""b", "b""b", "a""a", "bb""bb", "abba""abba". The four single characters always count — every string of length nn has at least nn palindromic substrings, which is a useful sanity floor. "abc""abc" giving exactly 33 confirms you are counting singles.

Follow-ups you should expect: “Count distinct palindromic substrings?” — genuinely harder; you need to deduplicate, which means a set of strings (O(n2)O(n^2) space) or the Eertree / palindromic-tree structure for the efficient answer. “Longest instead of count?” — LC 5. “Count palindromic subsequences?” — interval DP, and the counting version (LC 730) is Hard.

LC 680 — Valid Palindrome II · Easy

Problem. Given a string ss, return TrueTrue if it can be made a palindrome by deleting at most one character.

Constraints. 1 <= len(s) <= 10^51 <= len(s) <= 10^5, lowercase English letters.

Examples. "aba""aba" gives TrueTrue (already one) · "abca""abca" gives TrueTrue (delete "c""c" or "b""b") · "abc""abc" gives FalseFalse

Editorial — approach, complexity, follow-ups

Walk inward. While characters match there is no decision to make. At the first mismatch, the deleted character must be one of s[left]s[left] or s[right]s[right] — no other deletion can fix this particular pair. So branch once: check s[left+1 .. right]s[left+1 .. right] and s[left .. right-1]s[left .. right-1] as plain palindromes.

Time O(n)O(n) — the main loop is O(n)O(n) and the branch happens at most once, costing another O(n)O(n). Space O(1)O(1).

Follow-ups you should expect: “At most kk deletions?” — the branching approach becomes exponential, so switch to DP: the answer is len(s) - LPS(s) <= klen(s) - LPS(s) <= k, i.e. LC 1312’s identity. “Return which character to delete?” — return the index from whichever branch succeeded. “Deletions from both strings to make them equal?” — LC 583, an edit-distance variant.

LeetCode problem set

#ProblemDifficultyThe twist
125Valid PalindromeEasyTwo pointers, skipping non-alphanumerics; guard the inner skip loops
680Valid Palindrome IIEasyOne deletion means one branch — try both sides
9Palindrome NumberEasyNo string conversion allowed: reverse only half the digits
234Palindrome Linked ListEasyFind the middle, reverse the second half, compare — O(1)O(1) space
5Longest Palindromic SubstringMediumExpand around all 2n - 12n - 1 centres
647Palindromic SubstringsMediumSame expansion, counting instead of maximising
131Palindrome PartitioningMediumBacktracking, ideally with a precomputed is_pal[i][j]is_pal[i][j] table
516Longest Palindromic SubsequenceMediumSubsequence, so interval DP — or LCS(s, reversed(s))LCS(s, reversed(s))
1312Minimum Insertion Steps to Make a String PalindromeHardn - LPS(s)n - LPS(s) — the identity does all the work

Interview follow-ups

They askWhat they’re checkingThe answer
“Substring or subsequence?”Careful readingSubstring is contiguous (centres); subsequence may skip (DP). "bbbab""bbbab" gives 3 vs 4
“Why 2n - 12n - 1 centres?”Whether you found the even casenn character centres plus n - 1n - 1 gaps, because even-length palindromes centre between characters
“Beat O(n2)O(n^2)?”BreadthManacher’s algorithm, O(n)O(n) — name it and sketch the mirror/radius-reuse idea
“Why not the DP table?”JudgementSame O(n2)O(n^2) time but O(n2)O(n^2) space; only worth it when you need the whole table (LC 131)
“At most kk deletions?”Knowing when branching diesBranching goes exponential; use len(s) - LPS(s) <= klen(s) - LPS(s) <= k
O(1)O(1) space on a linked list?”CompositionFast/slow to the middle, reverse the second half, compare — then consider restoring it
“Count distinct palindromic substrings?”DepthDeduplicate with a set (O(n2)O(n^2) space), or use a palindromic tree (Eertree)

Edge-case checklist

  • Single character — always a palindrome; the answer to LC 5 has length 11, and LC 647 counts 11.
  • Two characters"ac""ac" gives "a""a" (or "c""c"); "bb""bb" gives "bb""bb". The fastest check that you handled even centres.
  • All identical"aaaa""aaaa"; the worst case for expansion, and where slicing inside the loop turns O(n2)O(n^2) into O(n3)O(n^3).
  • No palindrome longer than 1"abc""abc"; make sure best_lenbest_len starts at 11, not 00.
  • Ties for longest — multiple valid answers; do not assume one exact string.
  • All non-alphanumeric (LC 125) — ".,"".," must return TrueTrue and must not run off the end of the string.
  • Mismatch needing the right-side deletion (LC 680) — "cbbcc""cbbcc"; catches one-sided solutions.
  • Empty string — vacuously a palindrome, though most of these problems guarantee at least one character.

Recap

  • Three tiers, three techniques. Verify with two pointers (O(n)O(n)), find substrings by expanding around centres (O(n2)O(n^2), O(1)O(1) space), build/modify with interval DP.
  • Substring vs. subsequence is the distinction that decides which tier you are in. Read the problem statement twice.
  • There are 2n - 12n - 1 centres — always expand both (i, i)(i, i) and (i, i + 1)(i, i + 1).
  • Track indices, not strings, inside expansion loops; slicing per improvement can add a factor of nn.
  • “At most one deletion” is one branch, not a counter — and you must try both sides.
  • min insertions = n - LPS(s)min insertions = n - LPS(s), and LPS(s) = LCS(s, reverse(s))LPS(s) = LCS(s, reverse(s)) — one identity that collapses several Hard problems into a known DP.
  • Manacher’s gives O(n)O(n); know it exists, and that expand-around-centre is what you are expected to write.

Next: Trie Patterns — the prefix tree, and the problems that are unreasonably easy once you have one.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did