Expand around the center
Find palindromes by starting at every middle point and growing outward while both sides match.
O(n^2)Space O(1)5 practice problemsStep-through pictureThe idea
A palindrome is text that reads the same forward and backward, like level or abba. Every palindrome has a middle. For level the middle is the letter v. For abba the middle sits between the two b letters.
So instead of checking every possible piece of text, pick each possible middle and grow outward. Put a left pointer and a right pointer at the middle. While the characters under them are equal, move left one step left and right one step right. When they differ, or you hit an edge, the palindrome around that middle is as big as it gets.
A text of length n has 2n - 1 middles: n on letters (odd-length palindromes) and n - 1 between letters (even-length ones). Trying them all finds the longest palindrome in O(n^2) time with no extra memory.
An everyday example
Folding a paper strip
Write a word on a long paper strip and fold it at some point. Look at the letters that land on top of each other, starting next to the fold and moving outward. As long as each pair matches, the folded part reads the same both ways. The first pair that doesn't match tells you how far the mirror goes. You try a fold at every letter and between every two letters, and keep the longest mirror you saw.
| In the example | In the code |
|---|---|
| Where you fold the strip | center, giving the start of left and right |
| A fold on a letter or between two letters | center % 2 is 0 or 1 |
| Checking the next pair outward | text[left] === text[right], then left-- and right++ |
| The first pair that doesn't match | The while loop stops |
| The longest mirror so far | bestStart and bestLength |
From the slow way to expand around the center
- The most obvious way checks every substring. There are about
n^2 / 2substrings, and checking one takes up tonsteps, so it costsO(n^3). For 1,000 characters that's hundreds of millions of steps. - Much of that work repeats. If
ollois a palindrome, checkinglolloneeds only one more comparison: its outer pair,lando, which differ. Every palindrome is a smaller palindrome with one matching pair added around it, so palindromes that share a middle can be checked together. - So organize the search by middle instead of by substring. A middle is a letter, for odd lengths, or the gap between two letters, for even lengths. From each middle, grow outward one pair at a time while the pair matches. The first mismatch ends every palindrome with that middle, because any longer one would contain that same bad pair.
- To visit all
2n - 1middles with one loop, number them withcenter: even numbers sit on letters and odd numbers on gaps, andleft = Math.floor(center / 2)withright = left + (center % 2)turns the number into two pointers. Grow, measureright - left - 1, keep the longest: that loop is expand around the center.
How it works
- Keep the best palindrome found so far as
bestStartandbestLength, both starting at0. - Loop
centerfrom0to2 * text.length - 2. Even values sit on a letter and odd values sit between two letters. - Set
left = Math.floor(center / 2)andright = left + (center % 2). For a letter they are equal; between lettersrightis one more. - While both pointers are inside the text and
text[left] === text[right], moveleftone step left andrightone step right. - When the loop stops, the palindrome is the text strictly between
leftandright. Its length isright - left - 1. - If that length beats
bestLength, savebestStart = left + 1and the new length. - After all centers, return
text.slice(bestStart, bestStart + bestLength).
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function longestPalindrome(text) {let bestStart = 0;let bestLength = 0;for (let center = 0; center < 2 * text.length - 1; center++) {let left = Math.floor(center / 2);let right = left + (center % 2);while (left >= 0 && right < text.length && text[left] === text[right]) {left--;right++;}const length = right - left - 1;if (length > bestLength) {bestStart = left + 1;bestLength = length;}}return text.slice(bestStart, bestStart + bestLength);}
How fast it is
| Time | O(n^2) |
|---|---|
| Extra memory | O(1) |
There are about 2n centers and each can grow up to n / 2 steps. Only a handful of numbers are stored.
- Best case
O(n)No letter repeats near itself, like
abcdef: each letter's center grows once and each gap stops at once. The loop grows 6 times for 6 letters. - Average case
O(n)Ordinary words and random text: most centers stop after one or two steps, so the run is close to linear even though the bound is
O(n^2). - Worst case
O(n^2)One letter repeated, like
aaaaaaaa: centers near the middle grow aboutn / 2steps each. For 8 letters the inner loop grows 36 times, and in general aboutn^2 / 2times.
Common mistakes
- Only trying centers on letters. Even-length palindromes like
abbahave their middle between two letters and get missed. - Reading
text[left]before checkingleft >= 0. JavaScript returnsundefinedinstead of failing, which hides the bug until twoundefinedvalues compare equal. - Computing the length as
right - left + 1. When the loop stops, both pointers are one step past the palindrome, so the length isright - left - 1. - Building a new substring at every step with
slice. It works but adds needless copying; keep numbers and slice once at the end.
When to use it
Look for these clues in a problem:
- The question mentions palindromes, mirrored text, or reading the same both ways.
- You need the longest such piece, or a count of all of them.
- The text is up to a few thousand characters, so
O(n^2)time is fine. - You want an answer with
O(1)extra memory and no table.
Check yourself
Answer each one in your head first, then open it.
How many centers does a text of 10 letters have?
19: 10 on letters and 9 on gaps, which is 2n - 1.
Why is the length right - left - 1 and not right - left + 1?
When the loop stops, left and right are both one step outside the palindrome. It runs from left + 1 to right - 1, which is right - left - 1 letters.
Why does the code return lol for lollipop, and not pop?
Both have length 3. The test is length > bestLength, so a later tie never replaces the first answer.
Why can growing stop for good at the first pair that doesn't match?
Any longer palindrome with the same middle would contain that same pair at the same mirrored positions, and those letters differ. So no longer palindrome with this middle exists.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 5 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Every palindrome has exactly one middle: its center letter if its length is odd, or the gap between its two center letters if its length is even. The loop visits all 2n - 1 middles, so the middle of every palindrome is tried exactly once.
From a middle, the while loop keeps one rule: the text strictly between left and right is a palindrome. That's true at the start, where it is empty for a gap, or is about to be a single letter. Each step adds one matching pair around it. When a pair doesn't match or a pointer leaves the text, no longer palindrome has this middle, because it would contain that same pair. So the loop stops at the longest palindrome for this middle.
The longest palindrome in the text is the longest one for its own middle, so it's measured when that middle is tried. bestLength keeps the largest length seen, and bestStart = left + 1 records where it begins, because left stops one step before it. After every middle has been tried, text.slice(bestStart, bestStart + bestLength) is the answer.
Going deeper
3 more ideas
Why every palindrome is found
Any palindrome has exactly one middle: a letter if its length is odd, or a gap between two letters if it is even. The loop tries every letter and every gap. From the right middle, growing outward checks the pairs of the palindrome one by one, so it reaches at least that palindrome before stopping.
How fast it is
There are 2n - 1 centers. Each one can grow up to about n / 2 steps, so the worst case is O(n^2), for example on aaaaaaa where every center grows far. On normal text most centers stop after one or two steps, so it runs much faster. It uses only a few number variables, so the extra memory is O(1).
Counting palindromes instead
Each successful step of the while loop finds one more palindrome around that center. To count all palindromic pieces, add 1 to a counter inside the loop instead of tracking the longest. The same 2n - 1 centers cover every piece exactly once.
Work it by hand
Every step in a table, with why
Find the longest palindrome in an 8-letter word that has two palindromes of the same best length.
text = "lollipop"
| Step | center | Start left, right | Stop left, right | length | bestStart, bestLength | What happens | Why |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 0, 0 | -1, 1 | 1 | 0, 1 | l: new best | A letter always matches itself, so every odd center grows at least once. left then falls off the start. |
| 2 | 1 | 0, 1 | 0, 1 | 0 | 0, 1 | l vs o: no growth | The gap between l and o holds no palindrome. |
| 3 | 2 | 1, 1 | -1, 3 | 3 | 0, 3 | lol: new best | It grows twice: o matches itself, then l and l match. left falls off the start. |
| 4 | 3 | 1, 2 | 1, 2 | 0 | 0, 3 | o vs l | No growth. |
| 5 | 4 | 2, 2 | 1, 3 | 1 | 0, 3 | l, then o vs l | Only the single letter. |
| 6 | 5 | 2, 3 | 1, 4 | 2 | 0, 3 | ll: too short | An even palindrome: l and l match, then o and i differ. Length 2 doesn't beat 3. |
| 7 | 6 | 3, 3 | 2, 4 | 1 | 0, 3 | l | Only the single letter. |
| 8 | 7 | 3, 4 | 3, 4 | 0 | 0, 3 | l vs i | No growth. |
| 9 | 8 | 4, 4 | 3, 5 | 1 | 0, 3 | i | Only the single letter. |
| 10 | 9 | 4, 5 | 4, 5 | 0 | 0, 3 | i vs p | No growth. |
| 11 | 10 | 5, 5 | 4, 6 | 1 | 0, 3 | p | Only the single letter. |
| 12 | 11 | 5, 6 | 5, 6 | 0 | 0, 3 | p vs o | No growth. |
| 13 | 12 | 6, 6 | 4, 8 | 3 | 0, 3 | pop: a tie, not kept | pop also has length 3, but 3 > 3 is false, so the first palindrome found keeps its place. right falls off the end. |
| 14 | 13 | 6, 7 | 6, 7 | 0 | 0, 3 | o vs p | No growth. |
| 15 | 14 | 7, 7 | 6, 8 | 1 | 0, 3 | p | The last center, 2 * 8 - 2. The loop ends. |
It returns text.slice(0, 3), which is lol. All 15 centers were tried, and the inner while loop grew 11 times in total.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Expand around the center | O(n^2) | O(1) | Texts up to a few thousand characters, and interviews: short code with no extra memory. |
| Manacher's algorithm | O(n) | O(n) | Very long texts, or when you need every center's radius for many questions. |
| Dynamic programming table | O(n^2) | O(n^2) | You need to answer "is this piece a palindrome?" for many pieces later; the table stores every answer. |
| Check every substring | O(n^3) | O(1) | Only as the slow starting point, or to test a faster version on tiny inputs. |
Extra memory: O(1): a few numbers, bestStart, bestLength, left, and right. Only the final slice makes a new string.
How the steps grow
- Expand around the center, Dynamic programming table
O(n^2) - Manacher's algorithm
O(n) - Check every substring
O(n^3)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Expand around the center | 100 | 1 million | 1 trillion |
| Manacher's algorithm | 10 | 1,000 | 1 million |
| Dynamic programming table | 100 | 1 million | 1 trillion |
| Check every substring | 1,000 | 1 billion | more than a quadrillion |
Inputs that need care
5 tricky inputs
- Empty text:
"" 2 * 0 - 1is-1, so the loop never runs, andtext.slice(0, 0)returns"".- One letter:
"q" - There is one center. It grows once, so it returns
q. - No repeated letters:
"abcd" - Every palindrome has length 1. The first one,
a, is kept, because later ties don't passlength > bestLength. - The whole text is a palindrome:
"redder" - The gap between the two
dletters grows until both pointers leave the text, atleft = -1andright = 6, so it returnsredder. - Mixed case:
"Abba" Aandaare different characters, so it returnsbb. Lowercase a copy first if case shouldn't matter, and slice the original text with the same indexes.
Variations and follow-ups
Twists interviewers add
Longest palindrome at the start
To find the fewest letters to add in front of a text to make it a palindrome, you need the longest palindrome that starts at index 0. Expanding works in O(n^2). KMP's table built on text + "#" + reversed text gives it in O(n): its last value is that palindrome's length.
A palindrome after removing one letter
To check whether deleting at most one letter makes the text a palindrome, walk two pointers in from the ends. At the first mismatch, try skipping the left letter or the right one, and check the rest normally. That is O(n).
Manacher's algorithm
It visits the same centers, but reuses what a big palindrome already says about its mirror image. Each comparison that succeeds pushes a boundary forward, so all centers together take O(n).
Longest palindromic subsequence
If the letters don't have to be next to each other, it's a different problem. Dynamic programming over left and right solves it in O(n^2) time.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "longest palindromic substring", "count palindromic substrings", or any question about pieces that read the same both ways. Pieces made of neighboring letters point to centers; letters that may be skipped point to dynamic programming.
- Name the two kinds of middles before coding: letters for odd lengths and gaps for even lengths. Forgetting the gaps is the most common bug, and naming it early shows you know.
- Use a helper called twice per index, or the
centernumbering trick; both are fine. Say the length formularight - left - 1and why: both pointers stop one step past the palindrome. - Test by hand: one letter, an even palindrome like
abba, and a text with no palindrome longer than 1. The usual follow-up is "can you do better thanO(n^2)?": Manacher's algorithm does it inO(n).
Where frontend code uses it
The same idea in real UI code
- Double-clicking a word in a text field selects it by growing outward from the click point while the next characters still belong to the word. That is the same outward growth with a different stop rule.
- Code editors that expand a selection step by step (for example to the word, then the quotes, then the brackets) also grow a
leftandrightedge outward from the cursor. - The inner two-pointer check is what you would use to validate input that must read the same both ways, such as a palindrome puzzle in a word game.