DataData/02 · String
CHAPTER 02 · String

The String

An array of characters that cannot be edited. Reading any position costs O(1), but changing one character means copying the whole text. Underneath, a character is a number, and half the traps in this chapter come from the tables that turn numbers into characters.

§01

Intuition: a line carved in stone

First see how close it is to an array, then accept the one rule that makes it different.

The array in the previous chapter is a row of whiteboard cells: to change one, you erase it and write again. A string is a row of characters carved into stone. Each character still has its own slot, numbered from 0, exactly like an array. But once carved, the slot cannot be rewritten. To turn "rain tomorrow" into "sun tomorrow" you carve a new stone and copy over the parts that did not change as well.

This rule is called immutability. It holds for Java String, Python str, and JavaScript strings alike. It is a deliberate design, not a defect, and the story card below explains why. What matters first is the consequence: every operation that appears to modify a string, whether concatenation, replacement, or a case change, actually copies the whole text. That single fact decides half the complexity results in this chapter.

One more thing to settle up front: the slots do not really hold characters. A computer only stores numbers. A character is what you see after a number is looked up in a character encoding: 65 shows as 'A', and 23383 shows as 字. Three rules follow.

RULE 01
Immutable

The contents are fixed at creation. replace, toUpperCase, and concatenation all return a new string and leave the original untouched. The real cost of an "edit" is one full copy, O(n).

RULE 02
A character is a number

Every character is a number in an encoding table: 'A' = 65, 'a' = 97, 字 = 23383. Comparing, sorting, and changing case are all integer operations.

RULE 03
A read-only character array

A string behaves like a character array you may read but not write: O(1) by index, plus iteration and slicing. Careful: one index is not always one character (see §02). To edit in place, convert to a real, mutable character array first.

Why did all three languages choose immutability?

Safety. Strings are the most common hash key (chapter 06). If the contents could change after a key was stored in a hash table, the entry could never be found again. Immutability also lets the hash code be computed once and cached. Sharing. Because the contents cannot change, several variables can safely point at the same copy, which is how the Java string pool and Python string interning save memory. No locking between threads. A value that never changes can be read by any number of threads at once. There is one cost: code that changes a string often becomes expensive, so all three languages offer a mutable stand-in, whether StringBuilder, a Python list, or a JavaScript array. In §04 you build one.

§02

Memory and encoding: from ASCII to UTF-8

What a character really is in memory, told as the history of one table.

In 1963 ASCII gave the numbers 0 to 127 to the English letters, the digits, the punctuation, and a set of control codes. One character, one byte, and everything worked, as long as you only wrote English. Chinese has tens of thousands of characters and does not fit in 128 numbers, so each region built its own table (GB2312, Big5, Shift-JIS, and others). The same bytes opened in another country produced unreadable text. That is where the old problem of garbled characters comes from.

Unicode solves it in one move: put every character in the world into one list and give each a unique number, called a code point, written U+XXXX. 'A' is U+0041, 字 is U+5B57, and 🙂 is U+1F642. More than 150,000 code points have been assigned. But a code point is only a number. How that number is stored as bytes is a separate question, and that is the job of UTF-8, UTF-16, and UTF-32. Here is the single character 字 at every level.

Level字 at this levelWhat it means
What you seeThe shape drawn on screen (the glyph)
Unicode code pointU+5B57 (23383)A globally unique number. Still only a number; nothing about storage yet.
UTF-8 (3 bytes)E5 AD 97Variable length, 1 to 4 bytes. Most Chinese characters take 3. The standard for files and the network.
UTF-16 (2 bytes)5B 572 bytes for code points up to U+FFFF; above that, two units (a surrogate pair). Java and JavaScript index strings this way.
UTF-32 (4 bytes)00 00 5B 57Fixed 4 bytes. Indexing is simplest, space use is worst, so it is rarely written to disk.

UTF-8 became the standard on the web because it is variable length: common characters get short encodings, rare ones get long encodings, and plain English text stored as UTF-8 is byte for byte identical to ASCII from 1963. The length is chosen from the code point in four bands.

Code point rangeUTF-8 bytesTypical characters
U+0000 – U+007F1English letters, digits, common punctuation (that is, ASCII)
U+0080 – U+07FF2Latin extensions, Greek, Cyrillic, Arabic
U+0800 – U+FFFF3Most Chinese characters, Japanese kana, Korean
U+10000 – U+10FFFF4Emoji and rare historic scripts; these need a surrogate pair in UTF-16

The three languages index strings differently, and that decides what "length" means. Java and JavaScript index by UTF-16 code unit, so length counts 16-bit units. Python 3 str is a sequence of code points, so len counts code points. Note that a code point is still not always one visible character: an accented letter written as a base letter plus a combining mark is two code points, and a family emoji is several. The difference is invisible in plain English text and shows up the moment an emoji appears. Check it yourself below.

Encoding lab: type a character and see the numbers behind it
AU+0041
decimal 65
41UTF-8: 1 byte
UTF-16: 1 unit
U+5B57
decimal 23383
E5AD97UTF-8: 3 bytes
UTF-16: 1 unit
🙂U+1F642
decimal 128578
F09F9982UTF-8: 4 bytes
UTF-16: 2 units (surrogate pair)
3 code pointsJavaScript .length = 4 (UTF-16 code units)8 bytes stored as UTF-8
Look at the row marked as a surrogate pair. Its code point is above U+FFFF, so UTF-16 needs two code units for it. That is why JavaScript counts it as 2 in .length, and why charAt returns only half of it. Java behaves the same way. This is the trap in §05.

“Length” has three meanings, and all three have caused real bugs

Take the single emoji "👍". You see one character. In Java and JavaScript length is 2, because UTF-16 needs a surrogate pair. As UTF-8 it is 4 bytes. In Python len is 1, counting code points. Truncating a string by length, sizing a database column in bytes, and walking emoji text by index have each broken production systems. The habit to build: before you use a length, ask which unit it counts.

In practice: why files and networks are almost all UTF-8

HTML, JSON, HTTP, Git, and Linux filenames: the large majority of text on the internet travels as UTF-8. Three reasons. It is byte-compatible with ASCII, so old systems keep working. Text that is mostly English takes about half the space of UTF-16. And the byte stream is self-synchronizing: from any position you can find the next character boundary, because the leading byte and the continuation bytes have different bit patterns. Java and JavaScript chose UTF-16 in the 1990s, when two bytes per character looked like enough. Emoji broke that assumption, and surrogate pairs are the patch. Java 9 and later store many strings as Latin-1 bytes internally, but the API still counts UTF-16 code units, so the behavior you see does not change.

§03

Core operations: every cost comes from copying

Reading is cheap and every edit is expensive. The key case: why += inside a loop is O(n²).

OperationComplexityWhy
Read one position s[i] / charAt(i)O(1)Same address formula as an array. Java and JavaScript return one UTF-16 code unit; Python returns one code point. In a language that stores strings as UTF-8 bytes (Go, Rust), reaching the i-th character means walking the bytes, which is O(n).
LengthO(1)The length is stored in a field when the string is created, so it is just a read.
Concatenate s + tO(n+m)Immutability means nothing can be appended to s, so a new string of length n+m is allocated and both sides are copied into it.
Slice / substring s[a..b]O(k)k characters are copied into a new string. Java before version 7 shared the original array and was O(1), but a small substring could keep a huge string alive; since Java 7 it copies.
Compare contents equals / == / ===O(n)Character by character, up to the last one in the worst case. Different lengths are rejected in O(1). Use equals in Java, == in Python, === in JavaScript.
Find a substring (naive)O(n·m)n starting positions, each comparing up to m characters. KMP in §04 brings it down to O(n + m).

The row worth studying is concatenation. A single s + t is O(n+m), which sounds acceptable. The real problem is += inside a loop. Suppose you build a string of length n one character at a time.

The first += copies 1 character. The second copies 2, the old one plus the new one. The third copies 3. At step i the old string already has i−1 characters and all of them are copied again before the new one is added. The total is 1 + 2 + 3 + … + n = n(n+1)/2 ≈ n²/2. At n = 100,000 that is about 5 billion character copies. Each step looks harmless; the sum is not. This is the arithmetic-series trap: += on an immutable string inside a loop is O(n²).

The fix is a mutable stand-in: append into a mutable container, then build the string once at the end. The total returns to O(n). Watch the two strategies race.

Concatenation race: append 60 characters, who copies less?
s += c (rebuild each time)
0 chars copied
Append to a mutable buffer
0 chars copied
Both runners do the same job: append 60 characters to a string. The number of appends is identical. What differs is how many characters get copied in total.
Step 0 / 60

The same trap in all three languages

By the rules of the language, the loop is O(n²) in Java, Python, and JavaScript alike, because a string cannot be extended in place. Java: the compiler turns s += x into a new StringBuilder plus toString() on every iteration, so the total stays O(n²). You have to lift the builder out of the loop yourself. Python: CPython has an optimization that extends the buffer in place when the string has exactly one reference, but the language does not guarantee it and it stops applying as soon as a second reference exists. The idiom is to collect the parts in a list and call "".join() once. JavaScript: V8 represents a + b as a small node that points at both halves and only flattens it when the value is read, so a loop of += is often faster than n² in practice, but the flattening and the peak memory are still paid, and no engine promises this. The conclusion is the same everywhere: use a mutable container explicitly rather than relying on an engine optimization.

§04

Write it yourself: a builder, indexOf, and KMP

Two small implementations and one idea. KMP takes half of this section.

First: a string builder. It works exactly like the dynamic array from the previous chapter. Inside is a mutable character array. append writes into a free slot, which is amortized O(1), and when the array is full the capacity doubles and the contents move once. Only at the end does build copy everything into an immutable string. Reading this makes it clear why collecting first and building once is O(n).

my_string_builder.py
1# The Python idiom: use a list as the mutable buffer, join at the end
2class MyStringBuilder:
3 def __init__(self):
4 self.buf = [] # a list is a dynamic array; append is amortized O(1)
5
6 def append(self, s):
7 self.buf.append(s) # only stores a reference, copies no characters, O(1)
8
9 def build(self):
10 # join computes the total length, allocates once, then copies each part in: O(n)
11 return "".join(self.buf)
12
13# Usage
14sb = MyStringBuilder()
15for ch in "hello":
16 sb.append(ch)
17print(sb.build()) # "hello"
Python has no StringBuilder class because "".join(list) already is one: collect the parts, then build the string in a single step.

Second: naive substring search (indexOf). Find a pattern of length m inside a text of length n. Try each starting position, and on a mismatch move the start one step right and compare from the beginning again. The idea is direct, and the worst case is O(n·m).

naive_index_of.py
1def index_of(text: str, pattern: str) -> int:
2 """Index of the first occurrence of pattern in text, or -1 if absent"""
3 n, m = len(text), len(pattern)
4 if m == 0:
5 return 0
6 # The last useful start is n-m: any later and fewer than m characters remain
7 for i in range(n - m + 1):
8 j = 0
9 # Compare character by character, starting at i
10 while j < m and text[i + j] == pattern[j]:
11 j += 1
12 if j == m: # every position of pattern matched
13 return i
14 return -1 # every start failed

The idea: KMP, where a failure is information. Where does the naive method lose time? Look at one typical failure. Search for ABABC inside the text ABABABC. The first four characters ABAB match and the fifth does not. The naive reaction is to move the start one step right and forget everything, comparing from the beginning again. But we already know that those four characters of the text are ABAB. That knowledge is thrown away.

text      A B A B A B C
pattern   A B A B C
          ✓ ✓ ✓ ✓ ✗       mismatch at j=4. Matched so far = "ABAB"

Naive:    move the start 1 to the right, reset j to 0, compare from scratch
          (the 4 characters just compared are compared all over again)

KMP:      inside "ABAB", the longest overlap between its start and its end
          is "AB", of length 2
          → slide the pattern until that overlap lines up, continue at j = 2,
            and never move the pointer into the text backwards

text      A B A B A B C
pattern       A B A B C
              ✓ ✓ ✓ ✓ ✓   match

The whole secret of KMP (Knuth–Morris–Pratt) is that one slide. When the comparison fails, the end of the part that already matched, ABAB, and the start of the pattern share a longest overlap, AB. Slide the pattern until that overlap lines up. The overlapping characters need no recheck, because they were just compared and are known to be equal, so the comparison resumes at the overlap length. The pointer into the text therefore never moves backwards, and the whole search is O(n + m).

"How long is the overlap between the start and the end of each prefix" can be computed in advance from the pattern alone and stored in an array. That array is the prefix function, written next or lps. Its definition is exact: next[i] is the length of the longest proper prefix of pattern[0..i] that is also a suffix of pattern[0..i]. "Proper" means it cannot be the whole of pattern[0..i], otherwise the answer would always be i+1. Here it is for ABABC.

0Anext=0
1Bnext=0
2Anext=1
3Bnext=2
4Cnext=0
PrefixProper prefixesProper suffixesLongest matchnext
A(none)(none)0
ABAB0
ABAA, ABA, BAA1
ABABA, AB, ABAB, AB, BABAB2
ABABCA, AB, ABA, ABABC, BC, ABC, BABC0

Matching then follows one rule: when the comparison fails at j, set j to next[j−1] and try again; only when j is already 0 does the text pointer move forward. Why does this never skip a valid match? Because next gives the longest overlap, and every shorter alignment is reached by applying the rule again. If the first jump still fails, jump again, following the chain down to 0. The full implementation is below. The code that builds next looks almost the same as the code that matches, because building next is the pattern matching against itself.

kmp.py
1class Solution:
2 def strStr(self, text: str, pattern: str) -> int:
3 n, m = len(text), len(pattern)
4 if m == 0:
5 return 0
6
7 # ---- 1) Build nxt: the pattern matched against itself, O(m) ----
8 nxt = [0] * m # nxt[0] is always 0
9 k = 0 # k = length of the current overlap
10 for i in range(1, m):
11 # If it cannot be extended, follow the nxt chain down
12 while k > 0 and pattern[i] != pattern[k]:
13 k = nxt[k - 1]
14 if pattern[i] == pattern[k]:
15 k += 1
16 nxt[i] = k
17
18 # ---- 2) Match: i never goes back; on a mismatch j jumps, O(n) ----
19 j = 0 # j = how much of the pattern matched
20 for i in range(n):
21 while j > 0 and text[i] != pattern[j]:
22 j = nxt[j - 1] # slide the pattern, skip the known overlap
23 if text[i] == pattern[j]:
24 j += 1
25 if j == m: # the whole pattern matched
26 return i - m + 1
27 return -1

Why KMP is O(n + m), even though the loops look nested

Building next is O(m), matching is O(n), so the total is O(n + m) with O(m) extra space. The inner while makes the matching loop look like O(n·m), but count the changes to j instead of the loops. Each turn of the outer loop increases j by at most 1, so over the whole run j increases at most n times. Every turn of the inner loop sets j to next[j−1], which is strictly smaller than j, so it decreases j by at least 1, and j never goes below 0. A value that rises at most n times in total and never falls below 0 can be lowered at most n times in total. So all the inner loops together run at most n times, and matching is O(n). The same argument with k and m covers the preprocessing. Interviews rarely ask you to write KMP from memory, but they do ask three questions: what does the naive method waste (the contents of the part that already matched), what is the next array (for each prefix, the length of its longest proper prefix that is also a suffix), and why does the text pointer never move back (after the slide the overlapping characters are known to be equal).

§05

Three languages: one rule, three habits

The Java string pool, the Python join idiom, and the JavaScript emoji trap. The toolbar switches the code language for the whole site.

In all three languages strings are immutable, indexable, iterable, and rebuilt on every concatenation, so at the level of ideas they agree. What differs is the habits. Java has a string pool and the == trap. Python turns "collect, then join" into muscle memory. JavaScript has the most convenient template literals and the deepest UTF-16 emoji trap.

string_basics.py
1# Python: str is immutable, and a "character" is just a str of length 1
2s = "data"
3s[0] # 'd': O(1), and it is a str; there is no char type
4s[-1] # 'a': a negative index counts from the end
5s[1:3] # 'at': a slice copies, O(k)
6s.upper() # 'DATA': returns a new string; s is unchanged
7ord('d'), chr(100) # 100, 'd': converting between character and code point
8
9# f-string: the modern way to build a formatted string (3.6+)
10name, n = "world", 42
11msg = f"hello {name}, n={n}"
12
13# Idiom: in a loop, collect and join; never +=
14parts = []
15for i in range(3):
16 parts.append(str(i))
17s = "".join(parts) # "012": O(n) in total
Common mistake: s[0] returns a str, not a number; use ord() for the code point. And str is a sequence of code points, so len("👍") = 1, the only one of the three languages that counts it as one.
OperationJava (String)Python (str)JavaScript (string)Complexity
Lengths.length()len(s)s.lengthO(1)
Read one positions.charAt(i)s[i]s[i] / charAt(i)O(1)
Substring / slices.substring(a, b)s[a:b]s.slice(a, b)O(k)
Find a substrings.indexOf(t)s.find(t)s.indexOf(t)O(n·m)†
Splits.split(",")s.split(",")s.split(",")O(n)
Join many piecesString.join / StringBuilder"".join(parts)parts.join("")O(n)
Change cases.toLowerCase()s.lower()s.toLowerCase()O(n)
Trim both endss.strip() / s.trim()s.strip()s.trim()O(n)
Replace every matchs.replace(a, b)s.replace(a, b)s.replaceAll(a, b)*O(n)
To a character arrays.toCharArray()list(s)[...s] / s.split("")O(n)

* In JavaScript, s.replace("a", "b") with a string argument replaces only the first match. Use replaceAll, or a regular expression with the g flag. This is the easiest mistake to make when coming from another language. † O(n·m) is the worst case of the naive scan, which is what Java indexOf does. CPython str.find uses a smarter algorithm with an O(n + m) worst case, and JavaScript engines vary. Also, every operation that returns a new string uses O(n) space.

In practice: the string pool, interning, and intern()

Letting equal strings share one copy in memory has a name: string interning. Java puts compile-time literals in the pool, and at run time you can ask for it with s.intern(). Python automatically interns short strings that look like identifiers, which is why "abc" is "abc" is sometimes True. That is an implementation detail, not a promise: use == to test equality and keep is for "the same object". JavaScript engines do the same sharing internally, but it is never visible, because === on strings compares the value. So the three languages differ exactly here: Java needs equals, Python needs == rather than is, and JavaScript has no equivalent trap. All of this rests on immutability from §01: nobody would dare share an object whose contents can change.

§06

Three techniques: two pointers, sliding window, counting

★ Interview core

Where most LeetCode string problems live. Three representative problems, taken apart step by step.

String techniques overlap heavily with array techniques, which is no surprise given how close the two structures are, but three of them belong here. Read the problem and look for the signal. Palindrome or reversal points to two pointers moving toward each other. The best contiguous substring points to a sliding window. Anagrams or character counts point to a counting array. These three cover most of the problem set in this chapter.

TECHNIQUE 01
Two pointers, closing in

Start at both ends and compare one pair per step. The invariant: everything outside the range has already been checked. Standard for palindrome checks and in-place reversal. Expanding from the center is the same idea run backwards. See LC 125, 344, 5.

TECHNIQUE 02
Sliding window

Use it when the answer is a contiguous substring and the condition inside the window can be updated step by step. Keep a Set or a counter inside the window, take in on the right and give up on the left. See LC 3, 438, 76.

TECHNIQUE 03
Counting array

When the alphabet is small and known (26 lowercase letters, or 128 ASCII codes), a fixed-size int array replaces a hash table: faster, smaller, shorter to write. See LC 242, 438, 383. For arbitrary Unicode input, go back to a hash map.

Walkthrough A

LC 125 · Valid Palindrome

EASY

The problem: considering only letters and digits and ignoring case, decide whether the string reads the same forwards and backwards. Brute force: build a filtered lowercase string, then compare it with its reverse. That is correct but builds two new strings of size O(n). Better: two pointers decide it in place, with no new string at all.

LC 125 · two pointers, step by step
L
R
A0
?1
b2
B3
,4
a5
The rule: look only at letters and digits, and ignore case. L and R start at the two ends and move toward each other.
1 / 6
lc125_valid_palindrome.py
1class Solution:
2 def isPalindrome(self, s: str) -> bool:
3 l, r = 0, len(s) - 1
4 while l < r:
5 # Each pointer skips over characters that are not letters or digits
6 while l < r and not s[l].isalnum():
7 l += 1
8 while l < r and not s[r].isalnum():
9 r -= 1
10 # Compare in lower case: the check is case-insensitive
11 if s[l].lower() != s[r].lower():
12 return False
13 l += 1
14 r -= 1 # both step inward
15 return True # the pointers met; every pair matched

Complexity and follow-up questions

Time O(n), because each character is examined by l or r at most once, and extra space O(1). Compare that with the filter-and-reverse solution, which needs O(n) extra space. That difference is the point of doing it in place. Follow-up one: "what if you may delete at most one character?" (LC 680: on a mismatch, try the two branches l+1 and r−1 and continue each). Follow-up two: "why does the inner while also test l < r?" (a string made entirely of punctuation would run a pointer past the end; watching the boundary is the basic skill of two-pointer code).

Walkthrough B

LC 3 · Longest Substring Without Repeating Characters

MEDIUM

The problem: find the length of the longest contiguous substring with no repeated character. Brute force: enumerate every start and end and check each substring for a repeat, O(n³), or O(n²) with a Set. Better: the two sliding-window signals are both present, contiguous and incrementally maintainable. The key insight: while the window [l..r] has no repeat, a new duplicate only requires dropping characters from the left, and l never has to move back, because any earlier start still contains the duplicate that was just removed.

LC 3 · sliding window with a Set (s = "abcabcbb")
a0
b1
c2
a3
b4
c5
b6
b7
The window [l..r] holds a stretch with no repeated character. A Set travels with it and records which characters are inside.
1 / 9
lc3_longest_substring.py
1class Solution:
2 def lengthOfLongestSubstring(self, s: str) -> int:
3 window = set() # characters inside the window
4 l = best = 0
5 for r, c in enumerate(s):
6 # Does the new character repeat one inside? Drop from the left until it is gone
7 while c in window:
8 window.remove(s[l])
9 l += 1
10 window.add(c) # the new character enters
11 best = max(best, r - l + 1) # the window is valid at every step
12 return best

Complexity and follow-up questions

The loop invariant: at the end of every step, s[l..r] contains no repeated character. Both l and r move at most n steps forward, and each character enters and leaves the Set once, so the time is O(n) and the space is O(min(n, size of the alphabet)). Follow-up one: "can it be faster?" Store the last position of each character in a hash map and jump l straight past it, which removes the inner loop. Follow-up two: "what if the input contains emoji?" §02 answers it: in JavaScript, split with Array.from(s) first, otherwise a surrogate pair is cut into two halves that are not characters.

Walkthrough C

LC 5 · Longest Palindromic Substring

MEDIUM

The problem: find the longest contiguous substring that is a palindrome. Note that you have to find it, not just check one. Brute force: enumerate all O(n²) substrings and spend O(n) checking each, giving O(n³). Better: change what you enumerate. Instead of substrings, enumerate the centers. A palindrome has a useful property: remove one character from each end and it is still a palindrome. Read backwards, that says you can grow outward from a center until the two sides disagree, and that is the longest palindrome at this center. There are only 2n−1 centers, n characters plus n−1 gaps.

LC 5 · expand around center (s = "babad")
b0
a1
b2
a3
d4
A palindrome of odd length has a character at its center; one of even length has a gap at its center. For n characters there are n characters plus n−1 gaps, so 2n−1 centers in total. Try each center and expand outward from it.
1 / 7
lc5_longest_palindrome.py
1class Solution:
2 def longestPalindrome(self, s: str) -> str:
3 # Grow outward from the center (l, r); return the final boundaries
4 def expand(l: int, r: int) -> tuple[int, int]:
5 while l >= 0 and r < len(s) and s[l] == s[r]:
6 l -= 1
7 r += 1 # the two ends match, so grow one more layer
8 return l + 1, r - 1 # the loop overshot by one; pull back
9
10 best = (0, 0)
11 for i in range(len(s)):
12 # Two centers per position: odd length (i,i) and even length (i,i+1)
13 for l, r in (expand(i, i), expand(i, i + 1)):
14 if r - l > best[1] - best[0]:
15 best = (l, r)
16 return s[best[0] : best[1] + 1]

Complexity and follow-up questions

The invariant inside expand: before each step, s[l+1..r−1] is already a palindrome, so s[l..r] is a palindrome exactly when s[l] equals s[r]. 2n−1 centers, each growing at most O(n) layers, gives time O(n²) and extra space O(1). Follow-up one: "is anything faster?" Yes, Manacher's algorithm is O(n). It inserts separators between characters so odd and even cases become one, and reuses the symmetry of palindromes already found. It is hard to write, so naming it and describing the idea is usually enough. Follow-up two: "what about dynamic programming?" It works: dp[i][j] says whether s[i..j] is a palindrome. Also O(n²) time, but O(n²) space, so expanding around the center wins on memory.

§07

Problem set: 10 string problems

Hot 100 selection

Two pointers, then counting, then sliding window, then KMP, from easy to hard. Your progress is stored locally. Think for 30 seconds before opening the hint.

§08

Chapter quiz

✎ Chapter quiz

Get all 7 right to turn on the green dot for this chapter.

QUESTION 01 / 7

What does it mean exactly to say that a string is immutable?

QUESTION 02 / 7

You run s += c n times in a loop, appending one character each time. What is the total time complexity?

QUESTION 03 / 7

In Java, what is the correct way to compare the contents of two strings?

QUESTION 04 / 7

Which of these statements about UTF-8 are correct? (Select all that apply.)

QUESTION 05 / 7

Why does Python have no separate char type?

QUESTION 06 / 7

Which signal should make you think of a sliding window first?

QUESTION 07 / 7

In JavaScript, what is the value of "👍".length?

What to take away from this chapter
  • A string is an immutable character array: reading is O(1), and every "edit" is a full copy. Concatenation is O(n+m), and += inside a loop is O(n²) in Java, Python, and JavaScript alike. Collect into a StringBuilder, a list, or an array, and build the string once at the end.
  • A character is a number. Unicode assigns the number (the code point) and UTF-8, UTF-16, and UTF-32 decide how it is stored. Java and JavaScript index by UTF-16 code unit, so an emoji counts as 2, while Python 3 indexes by code point. Before you use a length, ask which unit it counts.
  • Substring search: naive is O(n·m). KMP uses the prefix function, where next[i] is the length of the longest proper prefix of pattern[0..i] that is also a suffix, so the pointer into the text never moves back: O(n + m). Remember the three questions: what does brute force waste, what is next, and why is no backtracking needed.
  • Three techniques: two pointers closing in (palindromes and reversal), sliding window (a contiguous substring whose condition updates incrementally), and a counting array (a small, known alphabet, instead of a hash table).
  • Language differences worth memorizing: Java == compares references, equals compares contents; Python has no char type (a str of length 1) and concatenates with join, and is is not ==; JavaScript === compares string values, but charAt splits a surrogate pair, so handle emoji with for...of and codePointAt.