DSA · Strings
The same four patterns, over and over
Stage 0 taught you string syntax. This module is about recognition: palindromes converge two pointers inward, anagrams count characters, the longest palindromic substring grows outward from a centre, and atoi is one careful left-to-right scan. You will trace Top spot! by hand, then step both pointer walks yourself.
Walk the pointers through Top spot! →01 The idea
Recognise the shape, then write the loop
String questions in placement rounds look endless. Palindrome, anagram, compression, atoi, longest common prefix, longest palindromic substring — a new one every round. They are not endless. Almost all of them are one of four shapes, and the shape is decided by a single question: how does the index move?
Two indices starting at the ends and walking towards each other handles palindromes and in-place reversal. One index plus a small table of counts handles anagrams and character frequency. A centre that grows outward in both directions handles the longest palindromic substring. And one left-to-right scan carrying a little state — a sign, a running number, a run length — handles atoi and run-length compression. Naming the shape is most of the work. The loop that follows is four to seven lines.
02 Worked example
Is Top spot! a palindrome?
Nine characters — Top spot! — with one capital, one space, and one exclamation mark. A valid-palindrome check ignores every character that is not a letter or a digit, and compares letters without caring about case. left starts at index 0 and right at index 8. Every row below is one pass of the loop, and a pass does exactly one thing: skip a character, or compare a pair. The space is drawn as · so you can see it.
Two things did the real work. The pointers move inward, and a pointer is allowed to move on its own when the character under it does not count. That second rule is what handles punctuation and whitespace without a second pass and without a cleaned copy. Hold on to the direction — inward — because the console's second mode reverses it: the longest palindromic substring starts in the middle and grows outward.
03 The code
Two patterns, four versions
First the palindrome, in the version most people write and the version interviewers want. Both are O(n) time, so the difference is memory — and, on a string that fails early, how much of it you read at all. Then the anagram check, where the difference is time and the honest answer is not the one-liner.
Naive · clean, copy, reverse
clean = ''for ch in s: if isalnum(ch): clean += lower(ch)return clean == reverse(clean) # O(n) extra memory
Two pointers · nothing copied
l = 0; r = len(s) − 1while l < r: if not isalnum(s[l]): l += 1; continue if not isalnum(s[r]): r −= 1; continue if lower(s[l]) != lower(s[r]): return false l += 1; r −= 1return true
Why two pointers when both are O(n) time? The naive version allocates a cleaned string and then a reversed one — up to 2n characters of new memory for a question that needs none. The two-pointer walk holds two integers, whatever the length of the input. "Can you do it in O(1) extra space?" is the standard follow-up, and this is the version that already answers it.
Why continue after moving one pointer? A skip is not a comparison. After l += 1 the loop condition has to be re-tested, because the new character might also be punctuation and because l might have reached r. On Top spot! that is exactly what happens at l = 3: the space pushes l to 4, l < r becomes false, and the loop ends without a fourth comparison. Drop the continue and you compare the space against a letter.
Why while l < r and not l <= r? When both pointers land on the same index there is nothing to compare, because a character always equals itself. On an odd-length palindrome that middle character is free. Using <= is not wrong in output, only wasteful — but it hides the fact that you know why the pointers may safely stop.
Now the second pattern, on characters from the same string. spot and stop are anagrams: same characters, different order. There are two standard answers, and the complexity gap between them is real.
By sorting · O(n log n)
if len(a) != len(b): return falsesa = sorted(a) # 'spot' -> 'opst'sb = sorted(b) # 'stop' -> 'opst'return sa == sb
By counting · O(n)
if len(a) != len(b): return falsecount = {} # char -> count; a missing key reads as 0for ch in a: count[ch] += 1for ch in b: count[ch] −= 1 if count[ch] < 0: return falsereturn true
Is counting actually faster, honestly? Yes, but say where it matters. Sorting is O(n log n) time and allocates two sorted copies; counting is O(n) time and O(k) space, where k is the alphabet size. On spot versus stop the difference is unmeasurable. On strings of 105 characters, or when you have to bucket 104 words, the log n factor is precisely what the question is testing.
Why the length check on line 1? Without it, counting att against at builds t: 2 and then decrements only one t. No count ever goes negative, so you return true for two strings that are not anagrams. The one-line check removes the entire class of bug, and it also lets the counting version exit early — with equal lengths, "no count went negative" is enough to prove every count landed back on zero.
Why one table and a decrement, rather than two tables compared? Same complexity, half the memory, and it can bail out on the first character that goes negative instead of finishing both strings. Comparing two hash maps is also easy to get wrong when one of them holds a key the other has never seen.
05 Cheat sheet
The family, and what each one costs
| Problem | Pattern | Cost | The trap |
|---|---|---|---|
| Valid palindrome | Two pointers converging | O(n) / O(1) | Comparing after the pointers meet, and treating a skip as a comparison. |
| Anagram check, counting | One frequency table | O(n) / O(k) | No length check first. Assuming 26 lowercase letters when the input can be anything. |
| Anagram check, sorting | Sort both, compare | O(n log n) / O(n) | Slower than counting, and it allocates two sorted copies — but the sorted string is the key you need to group anagrams. |
| Longest palindromic substring | Expand around 2n − 1 centres | O(n²) / O(1) | Trying only odd centres, so abba comes back as a single character. |
| String to integer (atoi) | One scan carrying state | O(n) / O(1) | Checking overflow after the multiply. Not stopping at the first non-digit. |
| Longest common prefix | Vertical scan, column by column | O(total chars) | Empty array, empty string, and running past the end of the shortest string. |
| Run-length compression | One pass with a run counter | O(n) | Never flushing the final run. The output can be longer than the input. |
06 Where & why
Which pattern, and when not to
None of these four patterns is clever. Their value is that none of them needs a copy of the input — three of the four run in O(n) time, and the expansion is the one that does not — on questions most people first solve by building extra strings. And an interviewer can tell in about ten seconds which one you reached for.
Reach for a converging two-pointer when…
Use something else when…
| Situation | Better choice | Why |
|---|---|---|
| Many words must be grouped, not compared two at a time | Sorted string as a hash-map key | Checking every pair is O(n²) anagram calls. Bucketing each word under its sorted form is one pass — this is the one job where sorting beats counting, because a sorted string is hashable as it stands. |
| You need the longest palindromic substring in linear time | Manacher's algorithm | Expand-around-centre is O(n²): each of the 2n − 1 centres can grow about n/2 times, so a thousand identical characters cost about half a million comparisons. Manacher reuses the radii it has already computed and runs in O(n). |
| You are searching for one string inside another | KMP or Rabin–Karp | Converging pointers say nothing about substring search. KMP's prefix function gives O(n + m) instead of the O(n·m) of restarting the match at every index. |
| Two strings differ by insertions, not just order | Edit-distance DP | A frequency table carries no position information, so it cannot tell you how many edits apart two strings are — only whether they use the same characters. |
07 Interview questions
What interviewers actually ask
Eleven questions in the order an interview escalates — the palindrome walk first, then the anagram trade-off, then centre expansion, then the parsing edge cases that decide the round.
How do you check whether a string is a palindrome, ignoring case and punctuation?
Why two pointers instead of cleaning the string and comparing it with its reverse?
In that loop, why while l < r and not while l <= r?
Two ways to check anagrams — sorting or counting. Which do you pick?
Why does the counting version need a length check before it starts?
Find the longest palindromic substring. What is your approach?
Why 2n − 1 centres and not n?
Walk me through the edge cases of atoi.
How do you find the longest common prefix of an array of strings?
Compress a string with run-length encoding. What is the trap?
Which of these would you actually use in production?
08 Practice problems
Six problems, four patterns
Every one is solvable with what is above: converge, count, expand, scan. Name the pattern before you write the loop.