Module 5 · Strings

String APIs, Scan Costs & Idioms

Concept~5 min

Split / process / join

The workhorse pipeline for token-level transformations:

s = "  the   sky is  blue  "
words = s.split()             # no-arg split: splits on runs of whitespace,
                              # discards empties -> ["the","sky","is","blue"]
result = " ".join(words[::-1])

Note the asymmetry — Python's no-arg split() treats any run of spaces as one separator and drops the empty strings that leading/trailing spaces would otherwise leave behind; JS's split() doesn't do either automatically, so it needs /\s+/ plus a filter for the leftover empty tokens. Both pipelines are O(n) time and O(n) space: strings are immutable, so rearranging pieces of one means building a new string (or a mutable char-array copy) that scales with the input — there's no way to beat that O(n) space floor without dropping to a mutable array first.

Scanning costs you should price correctly

  • sub in s / s.includes(sub) — substring search is O(n·m) in the worst case (naive; libraries do better on average, but never assume O(1)). Call it once per iteration of an n-length loop and the total becomes O(n²·m) — the "price the body honestly" rule, and strings are where it bites hardest.

    Trace the naive search for "ab" in "aabab" to see where the cost comes from — at each starting position, compare character by character until a mismatch or a full match:

    Start iComparisonsResult
    0text[0]='a' vs 'a' → match; text[1]='a' vs 'b' → mismatch (2 compares)fail, advance
    1text[1]='a' vs 'a' → match; text[2]='b' vs 'b' → match (2 compares)match found

    Four character comparisons before the naive scan finds the match — and in the worst case (no match anywhere, or many near-misses) that count grows to roughly n·m, one full pattern-length comparison attempted at every starting position.

  • s.startswith(p) — O(|p|): it only compares up to the prefix's length and stops at the first mismatch. Cheap; the Longest Common Prefix problem leans on it.

  • s.find(c) / indexOf — O(n): a full scan, no shortcut. Inside a loop, the same multiplication applies — quadratic alarm.

Palindrome and prefix idioms

Two micro-patterns that recur enough to preload. Palindrome check is converging pointers with reads instead of swaps: compare s[left] and s[right], close the gap until they meet. O(n) time, O(1) space — no char-array conversion needed, since immutability only blocks writes and a palindrome check never writes anything. Common prefix scan walks index i forward while every candidate string still agrees at position i, stopping at the first mismatch or the shortest string's end — whatever matched so far is the answer. Both appear as problems in this module; if you can derive them from their invariants without peeking, the Arrays module did its job.

0
1
2
3
4
l
e
v
e
l
1def is_palindrome(s: str) -> bool:
2 cleaned = [c.lower() for c in s if c.isalnum()]
3 left, right = 0, len(cleaned) - 1
4 while left < right:
5 if cleaned[left] != cleaned[right]:
6 return False
7 left += 1
8 right -= 1
9 return True

step 1 / 4left = 0, right = 4. Compare inward until they meet.

Check yourself

2 questions

01

A loop over n words calls text.includes(word) on each (text has length n). Total cost?

02

Why does a palindrome check need no char-array conversion while string reversal does?