DSA sheet · Strings · Two Pointers pattern

Longest Palindromic Substring

In this video the teacher takes a string and finds its longest piece that reads the same from both ends. She first builds the obvious brute force (make every substring, check each one with two pointers), works out why it is about n³, and then flips the palindrome check around: instead of starting at the two ends and shrinking inwards, she starts in the middle and expands outwards. That one change removes a whole loop and gives O(n²). She also explains why two pointers is the right pattern here (and not sliding window), how to handle odd and even palindromes, and how to get the substring back when you only have one index and a length. The same trick is reused in the very next problem (Palindromic Substrings), so it's worth learning well.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Words used on this page

index01234
sbabad"aba" (1..3) is a substring and a palindrome
sbabad"bbd" is not a substring (letters skipped)

What "two pointers" means

The slow way to look at pairs of positions is two nested loops: try every (i, j). That's about n·n/2 pairs, so O(n²). Two pointers is the idea of keeping just two indexes and moving them by a rule, so that at each step you can safely throw away some pairs without checking them. Usually this works because the array is sorted, or has some shape, that tells you which pointer to move. The teacher's point in this batch: sorting is not the real requirement. What you really need is to know which pointer to move, and when.

The family of patterns in this playlist, in one line each:

shapehow the pointers moveexample
Opposite endsleft starts at 0, right at n−1, they walk towards each other. On a sorted array, if the pair is too small you move left up; if it's too big you move right down. The other choice could only make it worse, so no answer is skipped.pair sum, Container With Most Water, Valid Palindrome
Same directiona slow "write" pointer and a fast "read" pointer both go left to right. Fast looks at every item, slow marks where the next kept item goes.Move Zeroes
Three pointerslow / mid / high split the array into the 0s, 1s, unknown part and 2s.Sort Colors
Fix one + two pointersa loop fixes one element, and opposite-end pointers search the rest. Skip equal neighbours to avoid duplicate answers.3Sum
left-max / right-maxopposite ends, but each side also remembers the tallest bar it has seen, and the side with the smaller max moves.Trapping Rain Water
Both ends of a stringcompare s[left] with s[right], then move both inwards.Valid Palindrome (and Part A here)
Expand around a centerboth pointers start in the middle and move outwards while the characters match.Part B of this page, Palindromic Substrings

This page needs the last two rows. Here they are properly.

Checking a palindrome: reverse copy vs two pointers

The teacher recalls the two ways she taught earlier for checking whether something like "bab" is a palindrome:

  1. Reverse copy: make a second string that is the first one reversed, then compare them letter by letter. It works, but it needs extra space to hold the reversed copy.
  2. Two pointers (better): reading the same from both sides means the 1st letter equals the last, the 2nd equals the 2nd-last, and so on. So put left = 0, right = n − 1. If s[left] != s[right], it's not a palindrome, stop right away. If they match, move both inwards (left += 1, right -= 1) and compare the next pair. No extra space.
check 1banabb = b ✓ → both move in
L   R
check 2banaba = a ✓ → both move in
 L R 
stopbanabL = R, a single letter always matches itself → palindrome

How many comparisons? The pointers meet in the middle, so a string of length n needs only about n/2 comparisons. Length 3 → 1 comparison. Length 5 → 2 comparisons (the middle letter is never compared). The loop runs while left < right: when they point to the same index there's nothing to compare, and once they cross, every pair is done.

Doubt: why is two pointers the right pattern for strings here? Why not sliding window, prefix sum or counting?
→ The teacher's answer: prefix sums and counting-style tricks are for number problems (sums, frequencies of values). On strings we mostly use two pointers or sliding window. A sliding window is about everything between L and R, the whole block. A palindrome check only compares the two characters at the ends (first with last, then second with second-last). Nothing in between matters for that comparison. "Compare just these two positions, then move" is exactly two pointers. And n/2 comparisons is the least you can do to check a palindrome, so this check can't be made faster.

Expand around a center (the idea used in Part B)

Every palindrome is symmetric around its middle. So instead of starting at the two ends and moving in, you can start at the middle and move out: if the letters just left and just right of the current palindrome are equal, the bigger piece is also a palindrome. Keep going until they differ or you fall off the string.

But where is "the middle"? It depends on the length:

Why are there 2n − 1 centers? A string of n letters has n letters and n − 1 gaps between neighbouring letters. Each palindrome has exactly one center, and it's either a letter (odd) or a gap (even). So the possible centers are n + (n − 1) = 2n − 1. For "babad" (n = 5) that's 5 letter-centers + 4 gap-centers = 9:

  letters:   b     a     b     a     d
  index:     0     1     2     3     4
  centers:   o  e  o  e  o  e  o  e  o
             ↑  ↑
   o = odd center (a letter, L = R = i)
   e = even center (the gap between i and i+1, L = i, R = i+1)

   5 odd + 4 even = 9 = 2·5 − 1 centers

In code we loop i from 0 to n−1 and try both kinds at every i. At the last i, the "even" call uses i+1 = n, which is outside the string, so it stops at once and finds nothing. That's why we make 2n calls but only 2n − 1 real centers.


Part A · Brute force: every substring + a two-pointer check

LeetCode 5

1The question in simple words

You get a string s. Return the longest substring of s that is a palindrome. You return the substring itself, not its length.

The teacher lists everything for s = "babad", length by length:

lengthsubstringspalindromes
1b, a, b, a, dall of them (a single letter always reads the same both ways)
2ba, ab, ba, adnone
3bab, aba, badbab, aba
4baba, abadnone
5babadno

The longest is length 3, and there are two of them: "bab" and "aba". The question doesn't say which one to pick, so either is accepted. Returning whichever you find first is fine.

Doubt: what if two answers have the same length?
→ Here, return any of them. But the teacher warns that some versions of this question ask for the lexicographically smallest one (dictionary order). Then you'd have to return "aba", because "aba" comes before "bab" in the dictionary, and you'd need to compare the strings on ties. Read the question to see if it asks for this.

Example 2: s = "cbbd" → answer "bb" (an even-length palindrome, which will matter in Part B).

2What the constraints tell us

3Intuition: try every substring, keep the longest palindrome

The simplest plan has two jobs:

  1. Make every substring (two loops: a start i and an end j).
  2. Check each one with the two-pointer palindrome check from Part 0. If it's a palindrome and longer than the best one found so far, remember it.

Making every substring with i and j

This is the same way we listed all subarrays in the arrays topic. Fix the start i, then let the end j walk from i to the end of the string:

start ij goes i … 4substrings made
00, 1, 2, 3, 4b, ba, bab, baba, babad
11, 2, 3, 4a, ab, aba, abad
22, 3, 4b, ba, bad
33, 4a, ad
44d
Doubt: why does j start at i and not at 0?
→ We want the substrings that start at i. The shortest of them is the single letter s[i], so the end begins at i too. For i = 2 we want "b", "ba", "bad", nothing to the left of index 2. If j started at 0, we'd get "ends before the start", which isn't a substring at all.

4Building the logic from the example

Don't build the substring for the check, pass i and j

The check function doesn't need a new string. Give it the whole s plus the two ends i and j, and it uses them directly as left and right. It only compares letters between those two positions.

Is it the longest so far? Use the length j − i + 1

Being a palindrome isn't enough. We want the longest. So we keep the best answer in res (it starts as the empty string, length 0), and when we find a palindrome from i to j, we compare its length with len(res).

The length of the piece from i to j, with both ends included, is j − i + 1. For i = 0, j = 2 ("bab") that's 2 − 0 + 1 = 3.

Cutting out the answer: why j + 1?

Slicing (Java's substring(i, j), Python's s[i:j]) includes the start and excludes the end. We want index j included, so we cut s[i:j + 1]. For i = 0, j = 0 that's s[0:1] = "b", exactly one letter, which is what we want.

Watching it on "babad"

5Approach steps

  1. res = "".
  2. For every start i from 0 to n−1, and every end j from i to n−1:
  3. Check whether s[i..j] is a palindrome with two pointers moving inwards (left = i, right = j).
  4. If it is, and j − i + 1 > len(res), set res = s[i:j+1].
  5. After both loops, return res.

6Code (Python)

Brute force · O(n³)
class Solution:
    def longestPalindrome(self, s):
        res = ""
        n = len(s)
        for i in range(n):                    # i = start of the substring
            for j in range(i, n):             # j = end, begins at i
                if self.isPalindrome(s, i, j):
                    if j - i + 1 > len(res):  # longer than the best so far?
                        res = s[i:j + 1]      # j + 1 because the end is excluded
        return res

    def isPalindrome(self, s, left, right):
        while left < right:                   # stop when they meet or cross
            if s[left] != s[right]:
                return False                  # one mismatch is enough
            left += 1                         # shrink inwards
            right -= 1
        return True

7Code line by line

linewhat it means
res = ""Best palindrome so far. Empty means length 0, so the first palindrome found always beats it.
for i in range(n):Pick where the substring starts.
for j in range(i, n):Pick where it ends. Starting at i gives the one-letter substring first.
if self.isPalindrome(s, i, j):Check only the part from i to j. We pass the indexes, so no new string is made.
if j - i + 1 > len(res):Length of s[i..j] (both ends included). Strictly greater, so on a tie the earlier one stays.
res = s[i:j + 1]Cut out the answer. The slice stops before its end index, so add 1 to include j.
while left < right:Compare pairs until the pointers meet (a single middle letter needs no check) or cross.
if s[left] != s[right]: return FalseA mismatched pair means it can't read the same both ways. Stop at once.
left += 1 right -= 1The pair matched, so move to the next pair inwards.
return TrueEvery pair matched, so it's a palindrome.

8Dry run (hand table)

s = "babad". "pairs compared" lists the (left, right) comparisons that isPalindrome makes.

ijs[i..j]pairs compared (left, right)palindrome?length vs len(res)res after
00bnone (left = right)yes1 > 0 → updateb
01ba(0,1) b≠ano–b
02bab(0,2) b=b, then left = rightyes3 > 1 → updatebab
03baba(0,3) b≠ano–bab
04babad(0,4) b≠dno–bab
11anoneyes1 > 3? nobab
12ab(1,2) a≠bno–bab
13aba(1,3) a=ayes3 > 3? no (tie)bab
14abad(1,4) a≠dno–bab
22bnoneyesnobab
23ba(2,3) b≠ano–bab
24bad(2,4) b≠dno–bab
33anoneyesnobab
34ad(3,4) a≠dno–bab
44dnoneyesnobab

The key moment, i = 0, j = 2 (the check works inwards from both ends):

comparebabadb = b ✓, move in → L = R = 1 → stop → palindrome, length 3
L R  

Final answer: "bab" ✓ (15 substrings checked, which is n(n+1)/2 for n = 5).

9Complexity & remember

Remember the brute force for i, for j from i → if isPalindrome(s, i, j) and j − i + 1 > len(res) → res = s[i:j+1].
The check is two pointers moving inwards while left < right. Total ≈ n · n · n/2.

Part B · Optimal: expand around every center

1The question again, with the new goal

Same input and output: return the longest palindromic substring of s. The goal now is to stop generating substrings with two loops and get from about n³ to about n².

2What the constraints tell us

3Intuition: check the palindrome from the inside out

In Part A, to check "bab" we start at the two ends and shrink. The teacher's trick is to do the opposite: start at the middle and expand.

The win: for each center, one expansion finds the longest palindrome around it, and along the way it has passed through every smaller one around it too. So instead of handing the function a substring (two indexes from two loops), we hand it only the index i and tell it to grow by itself. One loop over the centers replaces the two loops over (i, j).

Doubt: why is it safe to stop expanding at the first mismatch? Could a bigger palindrome around the same center exist?
→ No. Any bigger piece around the same center has that mismatched pair inside it, at mirror positions. A palindrome needs every mirror pair equal, so one broken pair rules out all bigger pieces around this center. Stopping skips nothing.

4Building the logic from examples

Odd and even centers

The teacher points out a catch. For an odd palindrome like "bab", left and right start on the same letter. But for an even palindrome like "aa" or "abba", there is no middle letter, so they must start on two neighbouring letters: left = i, right = i + 1. Then expand the same way. Using "aaaa" as the example: put L at index 1 and R at index 2, they match; step out to 0 and 3, they match, so "aaaa" is a palindrome.

odd startbabL = R = i, the letter is the center
 LR 
even startaaaaL = i, R = i + 1, the gap is the center
 LR 

We don't know in advance whether index i is the middle of an odd palindrome, an even one, or both. So for every i we try both: expand(s, i, i) and expand(s, i, i + 1), and keep the longer result. Over all i this covers all 2n − 1 centers from Part 0.

The while condition: three checks, and their order

While building it, the teacher first carried over the shrinking loop's left < right condition, then realised expanding needs something different. When we expand, left goes down and right goes up, so they can run off the ends: left can become −1 and right can become n. So the loop needs:

  1. left >= 0: left is still inside the string.
  2. right < len(s): right is still inside the string.
  3. s[left] == s[right]: the two letters match.

The two boundary checks must come first. and stops at the first False, so we only read s[left] and s[right] when both are valid indexes.

Doubt: what about left <= right from the shrinking version? Do we need it?
→ No. When expanding, left starts at or before right and only moves away from it, so left <= right is always true. It's harmless, but it checks nothing. The real stopping rules are the two boundaries and the mismatch.
Doubt (Python only): what goes wrong if I forget left >= 0?
→ In Java you'd get an index error. In Python it's worse: s[-1] is allowed and means the last character. The loop would quietly compare against the other end of the string and could report a palindrome that doesn't exist. Always keep the boundary checks.

Return a length instead of True

Part A's check returned True or False, and we had i and j to work out the length. Now the function only got one index, so True alone wouldn't tell us how long the palindrome is. So the teacher changes the return value: return the length of the palindrome it grew.

When the loop stops, left and right have gone one step too far. They point at the first bad pair (or outside the string). The palindrome is the part strictly between them. Her example: s = "babad", odd center at i = 0:

Why minus 1? The palindrome runs from left + 1 to right − 1. Its length is (right − 1) − (left + 1) + 1 = right − left − 1. Her advice: don't memorise formulas. Work them out from where your code leaves the pointers. Here they finish on invalid positions, so the "+1" formula doesn't apply.

Check it on an even center that finds nothing: i = 0, L = 0, R = 1, b ≠ a, so the loop never runs → 1 − 0 − 1 = 0. Correct: no even palindrome there.

Getting the substring back from i and a length

In Part A we had both ends (i and j), so we could cut s[i:j+1]. Now we only have the center i and the length. The palindrome reaches about half its length to the left of i and half to the right. Her example: "bab" in "babad", center i = 1, length 3. Going 1 step left gives index 0, going 1 step right gives index 2, so "bab" is s[0..2].

The exact formulas, which handle odd and even:

From center + length to the two ends start = i - (length - 1) // 2
end = i + length // 2
then the answer is s[start:end + 1]
Doubt: while explaining, she says "i minus length/2", but the code uses (length − 1) // 2. Why the −1?
→ For odd lengths both give the same answer: length 3 → 3//2 = 1 and (3−1)//2 = 1. For even lengths they differ, and only the −1 version is right. An even palindrome's center is the gap after i, so i itself is the left one of the middle two letters. It has one letter fewer on its left than on its right. Example: "cbbd", even center at i = 1, length 2. The answer "bb" is indexes 1..2.
• i − length//2 = 1 − 1 = 0 → s[0..2] = "cbb" wrong
• i − (length−1)//2 = 1 − 0 = 1, end = i + length//2 = 1 + 1 = 2 → s[1..2] = "bb" right
So the left reach is (length−1)//2 and the right reach is length//2. The test file checks "cbbd" for exactly this reason.

Only update when it's longer

We can't overwrite start/end at every i, or a short palindrome found later would replace a longer one found earlier. So first compare. end − start is the "j − i" of the best piece so far, and we update when length > end − start.

Doubt: end − start is one less than the best length. Is length > end − start correct?
→ It never loses the longest one, so the answer is always correct. But it lets an equal length replace the old one, since length 3 > 2 is true when the best is also length 3. That's why this code returns "aba" for "babad" while Part A returned "bab". Both are accepted. If you want the first one found (or need strict control over ties), compare with length > end - start + 1, which is the real best length.

Why save indexes and not the string?

We could keep a res string and cut a new copy every time we find a better palindrome. The teacher instead keeps only the two numbers start and end, and cuts the substring once, at the very end. That avoids making copies inside the loop. start = end = 0 at the beginning means "the first letter", which is always a valid palindrome of length 1.

5Approach steps

  1. start = end = 0.
  2. For every index i:
  3. len1 = expand(s, i, i) (odd, the center is the letter i).
  4. len2 = expand(s, i, i + 1) (even, the center is the gap after i).
  5. length = max(len1, len2).
  6. If length > end − start: start = i − (length − 1)//2, end = i + length//2.
  7. expand: while inside the string and s[left] == s[right], move left down and right up. Return right − left − 1.
  8. Return s[start:end + 1].

6Code (Python)

Expand around center · O(n²) time, O(1) space
class Solution:
    def longestPalindrome(self, s):
        start, end = 0, 0                     # best palindrome = s[start..end]
        for i in range(len(s)):
            len1 = self.expand(s, i, i)       # odd: center is the letter i
            len2 = self.expand(s, i, i + 1)   # even: center is the gap after i
            length = max(len1, len2)
            if length > end - start:
                start = i - (length - 1) // 2
                end = i + length // 2
        return s[start:end + 1]               # cut the string only once

    def expand(self, s, left, right):
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1                         # grow outwards
            right += 1
        return right - left - 1               # both pointers went one step too far

7Code line by line

linewhat it means
start, end = 0, 0The best palindrome is stored as two indexes. At first it's s[0..0], one letter, which is always a palindrome.
for i in range(len(s)):One loop. Each i is tried as an odd center and as the left side of an even center.
len1 = self.expand(s, i, i)Grow from the single letter i. Gives the longest odd palindrome centred at i (at least 1).
len2 = self.expand(s, i, i + 1)Grow from the pair (i, i+1). Gives the longest even palindrome centred there (0 if s[i] ≠ s[i+1] or i+1 is outside).
length = max(len1, len2)We don't know which kind i belongs to, so keep the longer.
if length > end - start:Only replace the stored answer if this one is long enough (see the tie doubt above).
start = i - (length - 1) // 2 end = i + length // 2Turn "center i + length" into the two ends. The −1 on the left handles even lengths.
return s[start:end + 1]Cut the answer once. +1 because the slice end is excluded.
while left >= 0 and right < len(s) and s[left] == s[right]:Boundaries first, then compare. Keep growing while the new outer pair matches.
left -= 1 right += 1Expanding: the opposite direction of Part A's check.
return right - left - 1The palindrome is s[left+1 .. right−1], so its length is right − left − 1.

8Dry run

s = "babad". Each row is one call of expand. "grows" lists the pairs it compared.

stepicenterstart L, Rpairs compared (L,R) → resultstops at L, Rlengthdecisionbest after (start..end)
10odd "b"0, 0(0,0) b=b ✓ → (−1,1) outside−1, 11max = 1 > 0 − 0 → update: start 0, end 00..0 "b"
20even b|a0, 1(0,1) b≠a ✗0, 10
31odd "a"1, 1(1,1) ✓ → (0,2) b=b ✓ → (−1,3) outside−1, 333 > 0 → start = 1 − 1 = 0, end = 1 + 1 = 20..2 "bab"
41even a|b1, 2(1,2) a≠b ✗1, 20
52odd "b"2, 2(2,2) ✓ → (1,3) a=a ✓ → (0,4) b≠d ✗0, 433 > 2 − 0 = 2 → tie replaces: start 1, end 31..3 "aba"
62even b|a2, 3(2,3) b≠a ✗2, 30
73odd "a"3, 3(3,3) ✓ → (2,4) b≠d ✗2, 411 > 2? no1..3 "aba"
83even a|d3, 4(3,4) a≠d ✗3, 40
94odd "d"4, 4(4,4) ✓ → (3,5) outside3, 511 > 2? no1..3 "aba"
104even d|?4, 5R = 5 is outside at once (no 10th center)4, 50

The string at the key moments (yellow = the pair being compared, grey = already inside the palindrome):

step 3ababadcenter i = 1, L = R = 1 → "a" ✓
 LR   
step 3bbabadexpand: b = b ✓ → "bab", next L = −1 is outside → length 3
L R  
step 5babadcenter i = 2 grew to "aba", then b ≠ d ✗ → stop at L = 0, R = 4 → 4 − 0 − 1 = 3
L   R

How far each of the 9 centers grows (the picture to keep in your head):

  index:      0   1   2   3   4
  s:          b   a   b   a   d

  odd  @0     b                          → 1
  even @0|1   ✗                          → 0
  odd  @1    [b   a   b]                 → 3   "bab"
  even @1|2       ✗                      → 0
  odd  @2        [a   b   a]             → 3   "aba"
  even @2|3           ✗                  → 0
  odd  @3                 a              → 1   (b ≠ d stops it)
  even @3|4               ✗              → 0
  odd  @4                     d          → 1

Final answer: "aba" ✓, a valid longest palindrome (length 3). As explained in the tie doubt, the brute force gave "bab". Both are accepted.

Second check, s = "cbbd": at i = 1 the even center (1, 2) b = b ✓ grows to (0, 3) c ≠ d ✗ → length 3 − 0 − 1 = 2 → start = 1 − 0 = 1, end = 1 + 1 = 2 → "bb" ✓.

9Complexity & remember

Remember expand around center For every i: expand(i, i) and expand(i, i+1) → 2n − 1 centers.
expand: while left ≥ 0 and right < n and s[left] == s[right]: left −= 1, right += 1 → return right − left − 1.
ends: start = i − (len−1)//2, end = i + len//2, answer s[start:end+1].

Part C · Revision page

Brute force (Part A)Expand around center (Part B)
how substrings are foundtwo loops: every (i, j)one loop: every center, grown outwards
pointer directionends → middle (shrink)middle → ends (expand)
check function returnsTrue / Falsethe length found
loop conditionleft < rightleft ≥ 0 and right < n and s[left] == s[right]
length formulaj − i + 1 (both ends valid)right − left − 1 (both ends one step too far)
stored answerthe string restwo indexes, cut once at the end
"babad" gives"bab" (first longest)"aba" (a tie replaces)
time / spaceO(n³) ≈ 5·10⁸ for n = 1000 / O(1)O(n²) / O(1)
odd centereven center
wherea letterthe gap between two letters
startleft = right = ileft = i, right = i + 1
how manynn − 1
smallest result1 (the letter itself)0 (if the two letters differ)
example"bab", "racecar""bb", "abba"
If you remember only 5 lines 1. The palindrome check can't get cheaper than n/2, so cut the substring generation instead.
2. Grow from the middle: try each i as an odd center (i, i) and an even center (i, i+1). That's 2n − 1 centers.
3. Expand while inside the string and the letters match. Check the boundaries first.
4. Return right − left − 1, because the pointers stop one step outside the palindrome.
5. start = i − (len−1)//2, end = i + len//2, return s[start:end+1].
Mistakes to avoid ✗ only trying odd centers (misses "bb" in "cbbd")
✗ comparing letters before the boundary checks (and in Python, s[-1] silently reads the last letter)
✗ returning right − left + 1 from expand
✗ start = i − len//2 (wrong for even lengths)
✗ updating start/end without comparing lengths first
✗ forgetting + 1 in s[start:end + 1]
✗ expecting one exact string when there's a tie (any longest one is accepted)
test it yourself (paste under either Solution above)
s = Solution()
print(s.longestPalindrome("babad"))              # "bab" or "aba"
print(s.longestPalindrome("cbbd"))               # "bb"   (even center)
print(s.longestPalindrome("a"))                  # "a"
print(s.longestPalindrome("ac"))                 # "a"    (any single letter)
print(s.longestPalindrome("aaaa"))               # "aaaa" (all the same letter)
print(s.longestPalindrome("forgeeksskeegfor"))   # "geeksskeeg"

Based on this video: Longest Palindromic Substring | Two Pointers