Part 4 of 30 in Data Structures & Algorithms: The Complete Course
Strings & String Algorithms: Immutability, Building, Searching, and Classic Patterns
How strings work under the hood, why immutability makes naive concatenation O(n²), the string builder fix, pattern searching from brute force to KMP intuition, and classic problems: reversal, anagrams, palindromes.
In plain words — the granite engraving analogy
Imagine text engraved in granite. You can read any letter instantly (O(1) by position), but you cannot add a letter to the middle — you'd have to chisel out the whole inscription and re-engrave it from scratch. That's an immutable string. Every time you 'change' one in code, the language carves a brand new piece of granite. Now imagine a helper with a scratch pad: they accumulate your edits in pencil, and only engrave one final version. That's a string builder, and it's the key to avoiding O(n²) performance.
Strings as character arrays
A string is, structurally, just an array of characters — so everything from the last lesson applies: O(1) access by index, O(n) search, cache-friendly scans. But strings add two twists that earn them their own lesson: immutability, which changes the cost model in ways that bite real programs, and a family of classic algorithms (searching, matching, transforming text) that show up constantly in both interviews and production code.
Immutability and the hidden O(n²)
In Java, Python, JavaScript, C#, Kotlin and most modern languages, a string cannot be modified after creation. 'Changing' a character or appending a suffix actually allocates a new string and copies the old contents into it. Immutability buys real benefits — strings can be shared safely across threads, used as hash-table keys, and interned — but it means every concatenation is an O(n) copy of the combined length.
The classic trap follows immediately: building a string by appending in a loop. Appending one character to a string of length k copies k + 1 characters; doing that n times copies 1 + 2 + … + n ≈ n²/2 characters. A loop that 'just adds a character each time' is quietly O(n²). For n = 100,000 that's five billion character copies to build one modest string.
IMMUTABLE CONCATENATION — why it's O(n²)
Iteration 1: result = "" + "a" → allocates "a" (1 copy)
Iteration 2: result = "a" + "b" → allocates "ab" (2 copies)
Iteration 3: result = "ab" + "c" → allocates "abc" (3 copies)
…
Iteration n: result = (n-1 chars) + "z" → allocates n-char string (n copies)
Total copies: 1 + 2 + 3 + … + n = n(n+1)/2 ≈ n²/2 → O(n²)
STRING BUILDER — why it's O(n)
Buffer is a dynamic array of characters (amortized O(1) append):
append("a") → buffer: [a]
append("b") → buffer: [a,b]
append("c") → buffer: [a,b,c]
…
append("z") → buffer: [a,b,c,…,z]
join() → one final copy of n chars → O(n)
Total: n × O(1) amortized + O(n) join = O(n) ✓The fix: build, then join
Accumulate pieces in a growable buffer — a string builder, or a dynamic array of parts joined once at the end. The buffer is a dynamic array of characters with amortized O(1) append, so the whole build is O(n). Same output, linear time. This single idiom fixes one of the most common performance bugs in application code.
Core operation costs
| Operation | Complexity | Why |
|---|---|---|
| Access character by index | O(1)* | Array underneath (*see the Unicode caveat below) |
| Length | O(1) | Stored, not counted, in mainstream languages |
| Compare two strings | O(min(n, m)) | Character-by-character until first difference or end |
| Concatenate two strings | O(n + m) | Allocate + copy both into a new string |
| Build by repeated concatenation in loop | O(n²) total | Each append re-copies the entire prefix |
| Build via StringBuilder / join | O(n) total | Amortized O(1) appends, one final copy |
| Substring / slice | O(k) | Copies k characters in most modern runtimes |
| Naive substring search | O(n·m) | Try every alignment (n = text length, m = pattern length) |
| KMP substring search | O(n + m) | Never re-examines already-matched text characters |
| Sort the string (as chars) | O(n log n) | Standard comparison sort on character array |
| Anagram check via frequency count | O(n) | One pass each string, O(1) fixed-alphabet array |
The Unicode caveat
'Index i gives character i' is only strictly true for fixed-width encodings. Text using variable-width encodings (UTF-8, or UTF-16 with surrogate pairs — emoji, many scripts) breaks the byte-index = character-index assumption, and 'reverse the string' by swapping code units can corrupt such text. For algorithm practice, ASCII assumptions are fine; in production text handling, they are bugs waiting to happen.
Substring search: from brute force to KMP
The defining string problem: find pattern P (length m) inside text T (length n). Brute force tries every starting position: align P at T[0], compare character by character; on mismatch, slide one right and start over. Worst case O(n·m) — pathological on repetitive inputs like searching 'aaab' in 'aaaaaaaaaa…'. In practice it's often fine, because on random text mismatches happen fast.
Brute-force substring search:
function search(T, P): # T = text, P = pattern
n ← length(T), m ← length(P)
for s from 0 to n - m: # try each starting position
match ← true
for i from 0 to m - 1:
if T[s + i] ≠ P[i]:
match ← false
break
if match: return s # found at index s
return -1 # not found
Time: O(n × m) worst case (e.g. T = "aaa…ab", P = "aab")
Space: O(1)- The waste brute force repeats: after matching 'aaab' against 'aaaa…' and failing on the 'b', brute force slides one step and re-compares the same 'aaa' prefix it already verified.
- KMP's insight: preprocess the pattern into a failure table. For each prefix of P, record the length of the longest proper prefix that is also a suffix. On a mismatch at P[i], shift the pattern by what the table dictates — the text pointer never moves backward.
- Result: every text character is examined O(1) times → O(n + m) guaranteed, with O(m) extra space for the failure table.
- Rabin-Karp variant: instead of character comparison, compute a rolling hash of the current text window. Compare hash values in O(1), recompute incrementally as the window slides. O(n + m) expected, O(nm) worst (hash collisions), and naturally extends to multi-pattern search.
KMP failure table example — pattern "ABABC"
Index: 0 1 2 3 4
Pattern: A B A B C
Fail: 0 0 1 2 0
Reading: fail[i] = length of longest proper prefix of P[0..i]
that is also a suffix of P[0..i]
"AB" → prefix "A" is also suffix? No → fail[1] = 0
"ABA" → "A" is both prefix and suffix → fail[2] = 1
"ABAB" → "AB" is both prefix and suffix → fail[3] = 2
"ABABC"→ no match → fail[4] = 0
On mismatch at pattern index 3 (expecting 'B', got 'X'):
skip to fail[2] = 1 (pattern index 1), text pointer stays put ✓Classic patterns, worked
Pattern 1: Reverse a string — the two-pointer template
Convert to a mutable character array, then set left = 0 and right = n − 1; swap the characters and move both pointers inward until they cross. O(n) time, O(n) space for the char array (O(1) extra beyond the array itself).
function reverseString(s):
chars ← toCharArray(s) # O(n) copy from immutable string
left ← 0
right ← length(chars) - 1
while left < right:
swap(chars[left], chars[right])
left ← left + 1
right ← right - 1
return join(chars) # O(n) to build result string
Trace on "hello": h e l l o
↑ ↑ swap h↔o → o e l l h
↑ ↑ swap e↔l → o l l e h
↑ left==right, stop
Result: "olleh" ✓Pattern 2: Anagram check — the frequency counting template
Are two strings rearrangements of each other? Sorting both and comparing works in O(n log n). The better pattern: count characters. Walk the first string incrementing a count per character, walk the second decrementing; if any count goes negative (or the lengths differ), they're not anagrams. O(n) time, O(1) extra space for a fixed-size alphabet array.
function isAnagram(s, t):
if length(s) ≠ length(t): return false
count ← array of 26 zeros # for 'a'..'z'
for c in s: count[c - 'a'] += 1 # increment
for c in t: count[c - 'a'] -= 1 # decrement
for each freq in count:
if freq ≠ 0: return false
return true
Example: s = "listen", t = "silent"
After s-pass: l=1, i=1, s=1, t=1, e=1, n=1
After t-pass: all back to 0 → anagram ✓
Example: s = "rat", t = "car"
After s-pass: r=1, a=1, t=1
After t-pass: r=0, a=0, t=1, c=-1 → not anagram ✓Pattern 3: Palindrome check — two pointers with filtering
Compare characters from both ends moving inward; any mismatch means not a palindrome. Real variants add filtering (skip non-alphanumerics, normalize case) — do that with pointer advancement, not by building a cleaned copy, to keep O(1) extra space.
function isPalindrome(s):
left ← 0
right ← length(s) - 1
while left < right:
while left < right and not isAlphanumeric(s[left]):
left ← left + 1 # skip non-alphanum
while left < right and not isAlphanumeric(s[right]):
right ← right - 1
if toLower(s[left]) ≠ toLower(s[right]):
return false
left ← left + 1
right ← right - 1
return true
Example: "A man, a plan, a canal: Panama"
Pointers skip commas and spaces, compare a↔a, m↔m, … ✓
O(n) time, O(1) extra spacePattern 4: Longest palindromic substring — expand from center
Expand outward from each of the 2n − 1 possible centers (each character, and each gap between characters, so even-length palindromes aren't missed), tracking the widest expansion. O(n²) time, O(1) space. The idea of 'expand from each candidate center' recurs in interval and sliding-window problems.
Common pitfalls
- Concatenating in a loop. The O(n²) trap from above. Reach for a builder/join the moment string-building enters a loop.
- Forgetting slices copy. Taking a substring inside a loop can turn an O(n) algorithm into O(n²). Track index ranges instead of materializing substrings.
- Comparing strings with identity instead of equality. In several languages, == on strings may compare references, not contents (or vice versa). Know your language's rule cold.
- Assuming one byte = one character. Unicode breaks indexing, length, and reversal assumptions the instant text leaves ASCII.
- Ignoring the alphabet-size constant. 'O(1) space because the count array is size 26' is only valid when the problem really is lowercase English. For general Unicode, use a hash map and say O(k).
- Off-by-one in sliding window string problems. The window length formula is right - left + 1 (inclusive both ends). Getting this wrong by one position corrupts the entire search.
Practice problems
- First non-repeating character. Given a string, find the index of the first character that appears exactly once. Solve it in O(n) time with O(1) space (hint: two passes — count frequencies, then find the first with count 1).
- Group anagrams. Given a list of words, group all anagrams together. Solve in O(n × k log k) time where k is the average word length (sort each word as a key). Then think about whether you can do O(n × k) using frequency tuples as keys.
- Minimum window substring. Given strings S and T, find the shortest substring of S that contains all characters of T. The sliding window approach runs in O(n + m). This is a hard problem worth tracing carefully on paper before coding.
Key takeaways
- A string is a character array with immutability layered on top in most languages — reads are cheap, 'writes' allocate copies.
- Repeated concatenation is O(n²); buffer-then-join is O(n). This is the most practically valuable fact in the lesson.
- Substring search runs from O(n·m) brute force to O(n + m) KMP; the shared idea is preprocessing the pattern so mismatches skip instead of restart.
- Two templates — two pointers and frequency counting — solve most interview string problems: reversal, palindromes, anagrams, uniqueness.
- Unicode invalidates naive index/length/reverse assumptions; know when your 'string' is really bytes.
Next, the course leaves contiguous memory behind: linked lists, where elements live anywhere and pointers hold the structure together. Practice string-algorithm questions offline in the **AI Learning app**.
A private, offline app to learn Artificial Intelligence, Machine Learning & Data Science at your own level: ~10,000 Q&A, illustrated guides, cheat sheets, quizzes and mock tests.
Sources
- Cormen, Leiserson, Rivest & Stein — Introduction to Algorithms, 4th ed. (MIT Press, 2022), Ch. 32: String Matching
- Sedgewick & Wayne — Algorithms, 4th ed. (Addison-Wesley, 2011), Ch. 5: Strings
- Knuth, Morris & Pratt — Fast Pattern Matching in Strings (SIAM Journal on Computing, 1977)
Frequently asked questions
More in Learn AI & Data Science
- Confusion Matrix Explained: TP, FP, FN, TN — and the Metrics They BuildEvery classification metric you've heard of — accuracy, precision, recall, F1 — is built from the same four numbers. Here's how to read them.
- Precision vs Recall: What They Mean and When to Optimize WhichTwo metrics, two different kinds of failure. The right one to optimize depends on which mistake costs you more.
- Cross-Validation Explained: How K-Fold Works and Why It Beats a Single SplitOne random test split can flatter or sabotage a model by pure luck. K-fold cross-validation replaces that lottery with an honest average.