You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering Leet Code Algorithms

Foundation: Arrays & Strings

Master fundamental data structures and basic manipulation techniques essential for solving entry-level problems

Last generated

Why Arrays and Strings Are the Starting Point

Pull up almost any list of practice coding problems and scan the first page. Reverse this. Find the longest substring. Merge these two collections. Remove duplicates. Rotate this by k positions. Long before you encounter a graph, a tree, or a dynamic programming table, you are handed a sequence of values sitting one after another — an array — or a sequence of characters — a string — and asked to do something with it. Why do these two structures show up so relentlessly at the entry point of algorithmic practice? Arrays and strings are the simplest possible containers for ordered data, which makes them the cheapest way to pose a problem that still requires real reasoning about position, order, and repetition. That simplicity is exactly why they're worth taking seriously instead of rushing past.

The Building Blocks Behind Every Problem

An array is nothing more than an ordered collection of values you can access by position. A string is, in almost every practical sense, an array of characters with a few language-specific quirks layered on top. Because both structures reduce a problem down to "a sequence of things in a fixed order," they let problem designers isolate a single skill — noticing a pattern in position or value — without requiring you to first understand pointers, node structures, or recursive traversal.

This isn't just a pedagogical convenience — it mirrors how these structures behave in real programs. A row of pixels in an image, a line of text in a file, a list of sensor readings, a sequence of stock prices: nearly any ordered data your program touches gets represented, at some level, as an array or a string. Mastering how to scan them, compare adjacent elements, and track running state isn't a warm-up exercise you'll discard later; it's the operational vocabulary you'll keep using once problems get harder.

🎯 Key Principle: If you can't confidently reason about a single sequence of elements, every more advanced structure — trees, graphs, heaps — will be harder to learn, because each is frequently implemented using arrays underneath, or requires the same instinct for tracking position and boundaries that arrays and strings demand first.

From Primitives to Patterns

Almost every "advanced" technique you'll meet later in this roadmap is really just a disciplined way of moving through an array or string. Two pointers is two index variables walking through the same array with a rule about when each one moves. Sliding window is a pair of pointers that expand and contract to track a contiguous range. Hashing, applied to arrays and strings, is a way of remembering what you've already seen so you don't have to re-scan the sequence. None of these are new data structures — they are strategies for reading and updating the array or string primitive more cleverly than a plain nested loop would.

            ┌─────────────────────────────┐
            │   Hashing / Hash Maps       │
            ├─────────────────────────────┤
            │  Sliding Window             │
            ├─────────────────────────────┤
            │  Two Pointers               │
            ├─────────────────────────────┤
            │  Traversal · In-place edits │
            │  Prefix sums · Sorting      │
            ├─────────────────────────────┤
            │   Arrays & Strings          │
            │ (contiguous, indexable data)│
            └─────────────────────────────┘

This stack is a simplified picture of how the techniques relate, not a strict dependency chain — some sliding window problems use hashing internally, and some two-pointer problems don't need sorting at all. Treat it as a map of where these ideas typically sit, not a rigid order to learn them in.

A small illustration: checking whether two arrays contain the same elements, ignoring order.

def same_elements(a: list[int], b: list[int]) -> bool:
    # Sorting turns "same elements, any order" into
    # "same elements, same position" — a much easier check.
    if len(a) != len(b):
        return False
    return sorted(a) == sorted(b)

print(same_elements([3, 1, 2], [1, 2, 3]))  # True
print(same_elements([1, 2, 2], [1, 1, 2]))  # False

Sorting is one of the basic manipulations this lesson covers — a preprocessing step, not a standalone "pattern" like two pointers or sliding window. Once the arrays are sorted, comparing them becomes a simple element-by-element scan, exactly the kind of linear traversal you'll practice below.

A second example makes the same point from the string side:

def is_palindrome(s: str) -> bool:
    # Basic indexing from both ends inward — the same
    # left/right scanning idea used on arrays.
    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True

print(is_palindrome("racecar"))  # True
print(is_palindrome("hello"))    # False

This already looks like a two-pointer solution, and it's a natural precursor to that pattern — but the goal here isn't to teach the technique itself (that's covered later); it's to show that the technique is basic index manipulation applied with a bit more intention. Once indexing from both ends and checking a loop condition feels automatic, the named pattern will feel like a label for something you already know how to do.

💡 Mental Model: Think of arrays and strings as the alphabet of algorithmic problem-solving, and patterns like two pointers, sliding window, and hashing as the grammar built from that alphabet. You can't compose fluent sentences — efficient solutions — without first knowing the letters cold.

What This Lesson Covers — and What It Doesn't

This lesson is deliberately narrow: it stays at the level of structure, complexity, and basic manipulation, and does not attempt to teach the named patterns that sit on top of that foundation.

ScopeTopicCovered where
🔧 In scopeHow arrays/strings are represented in memory and what that implies for speedCovered in Arrays and Strings Under the Hood
🔧 In scopeTraversal, in-place edits, prefix sums, sorting as a preprocessing stepCovered in Foundational Manipulation Patterns
🔧 In scopeBeginner mistakes like off-by-one errors and mutating while iteratingCovered in Common Pitfalls When Working with Arrays and Strings
🚫 Out of scope (for now)Two pointers, sliding window, and hash-map-based techniques in depthIntroduced only as forward references here; taught fully in later lessons
🚫 Out of scope (for now)Non-linear structures such as trees, graphs, or heapsOutside this lesson entirely

⚠️ Common Mistake: It's tempting to skip straight to the named patterns because they show up more in interview prep guides and feel more "advanced." The cost shows up later — a two-pointer solution with a quiet off-by-one bug, or a sliding-window implementation that mishandles an empty input, usually traces back to a shaky grip on the array or string mechanics underneath, not a misunderstanding of the pattern itself.

Arrays and Strings Under the Hood

Every algorithm's time and space complexity ultimately traces back to how its underlying data is laid out in memory. Before reasoning about why one solution runs in O(n) and another in O(n²), you need a clear picture of what an array or string actually looks like once it leaves your source code and lands in RAM.

Arrays as Contiguous Memory Blocks

An array is, at the hardware level, a single unbroken block of memory where every element sits the same fixed number of bytes apart. Because the elements are contiguous and uniformly sized, the computer doesn't need to search for the i-th element — it calculates exactly where it lives:

address(i) = base_address + (i * element_size)
Array of 4-byte integers, base_address = 1000

Index:      0      1      2      3      4
Address: [1000] [1004] [1008] [1012] [1016]
Value:   [ 42 ] [ 17 ] [ 99 ] [  3 ] [ 65 ]

To fetch arr[3], the runtime computes 1000 + 3*4 = 1012 and reads directly from that address — no traversal required. This is why array access is O(1): the cost is a single arithmetic operation and a memory fetch, regardless of array size or index.

🎯 Key Principle: O(1) access isn't a language feature or clever optimization — it's a direct consequence of contiguous, fixed-size storage. Any structure that gives up contiguity (like a linked list) gives up constant-time indexing along with it.

Strings: Character Sequences With Different Mutability Rules

A string is conceptually just an array of characters, and in many languages it's implemented that way. Where languages diverge is in whether that underlying character array can be modified after creation.

In C, a string is literally a mutable char array:

char greeting[] = "Hello";
greeting[0] = 'J';       // in-place mutation
printf("%s\n", greeting); // prints "Jello"

In Python and Java, strings are immutable — the character sequence backing a string object can never change. Any operation that looks like modification actually builds a brand-new string:

greeting = "Hello"
try:
    greeting[0] = "J"   # raises TypeError
except TypeError as e:
    print(e)             # 'str' object does not support item assignment

new_greeting = "J" + greeting[1:]  # creates a new string object

⚠️ Common Mistake: Assuming string immutability is just a syntax restriction rather than a memory-level fact. Operations that feel like small edits — replacing a character, appending in a loop — actually allocate new memory each time. The concrete cost of this pattern is covered in "Common Pitfalls When Working with Arrays and Strings."

The Complexity Cheat Sheet: Access, Search, Insert, Delete

Once you internalize contiguous storage, the complexity of every core operation follows logically. Access by index is O(1) — direct address calculation. Search for a value requires checking elements one at a time, giving O(n) worst case — no shortcut without additional structure like sorting or hashing. Insertion and deletion are where contiguity has a cost: inserting in the middle requires shifting every later element one slot over to keep the array unbroken; deleting works the same way in reverse.

Insert 'X' at index 2:

Before: [ A ][ B ][ C ][ D ][ E ]
After shifting C,D,E right by one:
        [ A ][ B ][   ][ C ][ D ][ E ]
Insert X:
        [ A ][ B ][ X ][ C ][ D ][ E ]

In the worst case (inserting near the front), this touches nearly every remaining element, so insertion and deletion are O(n). Inserting or deleting at the very end, with spare capacity, is the cheap case — O(1) — which matters for the dynamic array discussion below.

OperationTime ComplexityWhy
🔓 Access by indexO(1)Direct address calculation
🔍 Search by valueO(n)Must scan until match found
➕ Insert (middle)O(n)Elements must shift to stay contiguous
➕ Insert (end, capacity available)O(1)No shifting needed
➖ Delete (middle)O(n)Elements must shift to close the gap

Static vs. Dynamic Arrays: The Amortized Resizing Trick

A static array has a fixed size decided at creation time. Most languages you'll actually use for interview problems — Python's list, Java's ArrayList — give you a dynamic array instead: a structure that appears to grow freely, backed internally by a static array that gets swapped out for a bigger one when it fills up.

class DynamicArray:
    def __init__(self):
        self.capacity = 4
        self.size = 0
        self.data = [None] * self.capacity

    def append(self, value):
        if self.size == self.capacity:
            self._resize(self.capacity * 2)  # double the backing array
        self.data[self.size] = value
        self.size += 1

    def _resize(self, new_capacity):
        new_data = [None] * new_capacity
        for i in range(self.size):
            new_data[i] = self.data[i]   # copy every existing element
        self.data = new_data
        self.capacity = new_capacity

Most calls to append just drop the value into the next open slot — O(1). But occasionally, when size == capacity, the array must allocate a new block (typically double the size) and copy every existing element over, costing O(n) for that one call. Because doubling means resizes happen exponentially less often as the array grows, the expensive resizes are rare enough that the average cost per append, spread across many operations, works out to O(1). This is amortized O(1) — not because every individual append is cheap, but because the total cost of n appends divided by n stays constant.

💡 Mental Model: Think of amortized cost like budgeting for a new roof. Most months there's no roof expense, but setting aside a fixed amount every month means the expensive replacement is already "paid for" when it happens. Dynamic arrays do the same thing — cheap operations subsidize the occasional expensive one. (Different languages tune the growth factor differently, but the amortized-O(1) result holds as long as growth is geometric rather than fixed-increment.)

Multi-Dimensional Arrays: Mapping Rows and Columns to Linear Memory

Memory itself is one-dimensional, so a two-dimensional array has to be flattened into that line somehow. Most languages use row-major order: every element of row 0 is stored first, followed by row 1, and so on.

2D array (3 rows x 3 cols):
[ 1 2 3 ]
[ 4 5 6 ]
[ 7 8 9 ]

Row-major layout in memory:
[1][2][3][4][5][6][7][8][9]
address(row, col) = base + (row * num_cols + col) * element_size

For [1][2] (value 6) in the 3x3 grid: (1*3+2)=5, the 6th slot (0-indexed) in the flat layout — which matches. This is why iterating row-by-row is typically faster than column-by-column in languages with true row-major contiguous storage (C arrays, NumPy arrays): scanning a row reads consecutive addresses, while scanning a column jumps num_cols addresses at a time.

⚠️ Common Mistake: Assuming a Python list of lists behaves like a true row-major contiguous 2D array. It doesn't — each inner list is a separate object allocated independently, and the outer list just stores references to them. The row-major mental model still applies to indexing, but pure Python lacks the contiguity guarantee that specialized array libraries provide.

grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(grid[1][2])  # 6 — logically row-major indexing,
                    # but grid[0] and grid[1] are separate list objects

🤔 Did you know? Row-major isn't universal — Fortran and (historically) MATLAB default to column-major order, where columns are stored contiguously instead of rows. Working across language boundaries with shared numerical data, this distinction can silently flip which loop order is memory-efficient.

The complexity numbers established here — O(1) access, O(n) search/insert/delete, amortized O(1) append — are the baseline every later technique tries to beat or work around. Traversal, prefix sums, and sorting as preprocessing all exist because raw O(n) search and O(n) insertion are often too slow — and two pointers, sliding window, and hash maps, introduced briefly ahead and expanded later in the roadmap, are strategies for avoiding those costs.

Foundational Manipulation Patterns

Once you know how an array or string is laid out in memory, the next question is: what do you actually do with it? Almost every entry-level array or string problem decomposes into a small number of repeatable moves — scan the data once, rewrite it without extra storage, precompute partial answers, or reorder it to expose structure. These moves are the vocabulary that more advanced techniques like two pointers and sliding windows are built from.

Linear Traversal: The Baseline Operation

Linear traversal means visiting each element or character exactly once, in order, typically with a single loop and an index or iterator variable. It's the simplest pattern here, but the one every other pattern extends — prefix sums, sorting comparisons, and two-pointer scans are all variations on "walk through the data and accumulate something as you go."

def max_and_sum(nums):
    """Single pass to compute both the running sum and the max value."""
    total = 0
    current_max = nums[0]
    for num in nums:
        total += num
        if num > current_max:
            current_max = num
    return total, current_max

## Example: max_and_sum([3, -1, 7, 2]) -> (11, 7)

Folding multiple computations into one traversal is a small optimization now, but it becomes essential once problems require tracking several running values simultaneously — as with sliding window techniques later in the roadmap.

In-Place Modification: Swapping and Overwriting

In-place modification rewrites the existing array or string buffer instead of allocating a new one, keeping extra space at O(1) beyond the input itself. Two operations dominate: swapping two elements to reorder them, and overwriting a position with a new value to compress or filter the data.

def reverse_in_place(nums):
    left, right = 0, len(nums) - 1
    while left < right:
        nums[left], nums[right] = nums[right], nums[left]
        left += 1
        right -= 1
    return nums

## Example: reverse_in_place([1, 2, 3, 4, 5]) -> [5, 4, 3, 2, 1]

Overwriting is the tool of choice when you need to remove or compress elements rather than reorder them. A common version deduplicates a sorted array without a second array, using a "write pointer" that only advances when a genuinely new value is found:

def remove_duplicates_sorted(nums):
    if not nums:
        return 0
    write = 1  # next position to overwrite
    for read in range(1, len(nums)):
        if nums[read] != nums[write - 1]:
            nums[write] = nums[read]
            write += 1
    return write  # number of unique elements now stored in nums[:write]

## Example: remove_duplicates_sorted([1, 1, 2, 2, 3]) -> 3, nums becomes [1, 2, 3, 2, 3]

The returned length tells you how much of the array is meaningful; the leftover trailing values are simply ignored by convention. This read/write pointer idea — two indices moving through the same buffer at different rates — is the direct ancestor of the two-pointer technique covered later in this roadmap.

⚠️ Common Mistake: Overwriting values while a loop still references old indices can silently corrupt data if the read and write pointers get out of sync. Always confirm write never overtakes read in a way that reads an already-overwritten value.

Prefix Sums: Precomputing for Range Queries

Linear traversal answers "what is true across the whole array?" but many problems ask a narrower question repeatedly: "what is the sum of elements between index i and j?" Recomputing that sum from scratch for every query costs O(n) per query. The prefix sum pattern trades a small amount of upfront work and memory for much faster queries afterward.

nums:    [ 4,  2,  6,  1,  3]
prefix:  [ 4,  6, 12, 13, 16]
           ^   ^   ^   ^   ^
         prefix[i] = sum(nums[0..i])

Once this array exists, the sum of any range [i, j] is a single subtraction. Building the prefix array costs one O(n) traversal, and every query afterward costs O(1):

def build_prefix_sums(nums):
    prefix = [0] * (len(nums) + 1)  # prefix[0] = 0 as a base case
    for i, num in enumerate(nums):
        prefix[i + 1] = prefix[i] + num
    return prefix

def range_sum(prefix, i, j):
    """Inclusive sum of nums[i..j] using a prefix array of length len(nums)+1."""
    return prefix[j + 1] - prefix[i]

## nums = [4, 2, 6, 1, 3]
## prefix = build_prefix_sums(nums) -> [0, 4, 6, 12, 13, 16]
## range_sum(prefix, 1, 3) -> 7  (2 + 6 + 1)

The leading zero simplifies the subtraction so you don't need a special case for queries starting at index 0. This O(n) space, O(1)-per-query trade-off is the same underlying idea behind more specialized structures like Fenwick trees and segment trees, which extend prefix sums to handle updates efficiently — beyond this foundational lesson, but worth knowing exists.

Sorting as a Preprocessing Step

Sometimes the fastest path to a solution is reorganizing the data first so a simpler traversal becomes possible. Sorting as preprocessing means applying a sort (typically O(n log n)) before the main logic, turning an otherwise awkward search or comparison problem into a linear one.

Consider finding whether two numbers in an array sum to a target value. Scanning every pair directly costs O(n²). But if the array is sorted first, one pointer at the start and one at the end can move toward each other based on whether the current sum is too small or too large — sorted order guarantees this narrowing process never misses the answer. That pointer-narrowing technique gets its own dedicated treatment later in the roadmap; the enabling move — sorting first — belongs here.

def has_pair_with_sum(nums, target):
    nums_sorted = sorted(nums)  # O(n log n) preprocessing step
    left, right = 0, len(nums_sorted) - 1
    while left < right:
        current = nums_sorted[left] + nums_sorted[right]
        if current == target:
            return True
        elif current < target:
            left += 1
        else:
            right -= 1
    return False

## Example: has_pair_with_sum([2, 7, 11, 15], 9) -> True

Sorting also simplifies problems involving duplicates or grouping, since related values end up adjacent — but it costs O(n log n) time (and possibly O(n) extra space), so it's worth it only when it genuinely simplifies the remaining logic.

⚠️ Common Mistake: Sorting a copy versus sorting in place changes both memory use and whether the original index order survives — many problems (like "return the indices of the two numbers") need the original positions, which sorting destroys unless you track them separately, e.g. by sorting (value, original_index) pairs.

📋 Quick Reference Card: Foundational Patterns

🔧 Pattern🎯 Best For⏱️ Time🔒 Extra Space
🧠 Linear traversalAccumulating a single-pass answer (sum, max, count)O(n)O(1)
🔧 In-place swap/overwriteReordering or compressing without a new bufferO(n)O(1)
📚 Prefix sumsMany range queries on the same dataO(n) build, O(1) per queryO(n)
🎯 Sorting as preprocessingSimplifying comparisons or enabling pointer techniquesO(n log n)O(n) or O(1)*

*Space for sorting depends on the language and algorithm — some in-place sorts use O(1) extra space, others allocate O(n).

🎯 Key Principle: This set of four patterns is a strong starting toolkit for entry-level array and string problems, not an exhaustive list — the base layer you reach for first, with more specialized techniques layered on top as problems demand.

These four moves rarely appear alone in interview-level problems — they combine into the specialized patterns previewed earlier (two pointers, sliding window, hash maps). But before reaching for those, it's worth pausing on the mistakes that trip up almost everyone while still working at this foundational level.

Common Pitfalls When Working with Arrays and Strings

Most wrong answers on array and string problems don't come from a flawed algorithm — they come from small mechanical slips in how the array or string is indexed, copied, or iterated. These five patterns account for a disproportionate share of "almost correct" submissions.

Off-by-one errors. Loop bounds are the single most common source of bugs in this domain: using <= instead of < against a length, forgetting that len(nums) - 1 is the last valid index, or miscounting how many elements a slice actually includes. There's no shortcut around this except deliberate habit — before writing a loop, state out loud what the first and last iteration should touch, then check the code matches.

Mutating a collection while iterating over it. Removing or inserting elements from a list while a for loop is walking through it shifts every later index, causing elements to be skipped or visited twice:

nums = [1, 2, 2, 3]
for n in nums:
    if n == 2:
        nums.remove(n)  # mutates the list mid-iteration
print(nums)  # [1, 2, 3] — one "2" survives unexpectedly

The safe alternatives are building a new list via a comprehension, iterating over a copy (for n in nums[:]), or using the read/write pointer style from in-place modification above, which is explicitly designed to avoid this trap.

Treating immutable strings as mutable. In Java and Python, every += on a string inside a loop allocates a brand-new string, since the previous one can never be edited in place:

result = ""
for ch in "hello world":
    result += ch.upper()   # allocates a new string each iteration

For n characters, this costs O(n) per concatenation across O(n) iterations — O(n²) total — compared to O(n) for building a list of characters and joining once ("".join(...)). This is a direct consequence of the mutability rules covered earlier in "Arrays and Strings Under the Hood."

Confusing shallow and deep copies. Copying a list of lists with list(original) or slicing (original[:]) only copies the outer container — the inner lists are still shared references:

original = [[1, 2], [3, 4]]
shallow = original[:]
shallow[0][0] = 99
print(original[0][0])  # 99 — the inner list was never actually copied

Avoiding this requires an explicit deep copy (copy.deepcopy) when nested mutable structures must be fully independent — a distinction that matters constantly once problems involve grids or grouped data.

Skipping edge cases. Empty inputs, single-element inputs, and inputs full of duplicate values break assumptions that hold for the "normal" case — a two-pointer loop that assumes at least two elements, or a prefix-sum lookup that assumes index 0 is always safe to subtract from. Testing these boundary cases explicitly, before trusting a solution on the general case, catches a large share of otherwise silent failures.

⚠️ Common Mistake: These pitfalls are individually small, but they compound — an off-by-one error combined with an unguarded empty-input case is one of the most frequent sources of "almost correct" submissions in practice interviews and online judges.

Summary and What's Next

You started this lesson treating arrays and strings as familiar, almost boring building blocks. By now they should look different: memory layouts with specific performance guarantees, and the substrate on which nearly every advanced technique in algorithm interviews is built.

Memory and complexity. An array's performance profile is a direct consequence of its physical layout — contiguous memory gives O(1) indexed access, while search and mid-array insertion/deletion remain O(n). Strings either inherit that same contiguous, mutable layout (C++) or are treated as immutable value types (Java, Python), which changes the cost model for edits even though the underlying data is still stored contiguously. Dynamic arrays (Python's list, Java's ArrayList) hide a resizing operation behind a simple append — occasionally expensive, but amortized O(1) on average. Multi-dimensional arrays aren't a new structure; they flatten into the same one-dimensional memory using row-major order.

Manipulation patterns. Traversal is the O(n) baseline every other approach is measured against. In-place modification (swapping, overwriting) avoids the O(n) space cost of a new copy. Prefix sums precompute cumulative totals so range queries collapse from O(n) to O(1) after an O(n) setup. Sorting as preprocessing accepts an O(n log n) upfront cost to unlock simpler O(n) logic afterward. Note that remove_duplicates_sorted from earlier is already a two-pointer pattern in disguise — read and write are two pointers moving through the same array at different rates, which is exactly what the two-pointer technique formalizes.

Pitfalls. Off-by-one loop bounds, mutating mid-iteration, treating immutable strings as mutable, confusing shallow and deep copies, and skipping edge cases — none of these are exotic, and they're worth re-checking every time you write a loop or a copy statement, not just once during study.

How These Fundamentals Extend

Everything in the upcoming sub-topics is a specialization of what you just reviewed, not a departure from it.

Two pointer techniques run two indices through the array simultaneously — often one from each end, or one lagging behind the other — to solve problems like pair-sum search or in-place partitioning in a single pass instead of nested loops.

Sliding window patterns extend traversal and prefix-sum thinking to contiguous subarrays or substrings, computing a running property incrementally as the window moves rather than recomputing it from scratch — the same amortization principle that makes dynamic array resizing efficient shows up again here in a different form.

Hash maps and hash sets address a gap plain arrays can't fill alone: fast, average-case O(1) lookup by value rather than by index. Where prefix sums answer "what is the sum over this range," a hash map answers "have I seen this value before, and where" — the query many array and string problems ultimately reduce to.

💡 Mental Model: This lesson is the vocabulary; the next three sub-topics are the grammar built from it — two pointers and sliding windows are structured ways of traversing, and hash maps are a structured way of remembering what you've traversed. None of them replace the fundamentals: a sliding window is still a traversal, a two-pointer solution is still bounded by the same O(n) array-access guarantees, and a hash map lookup is still, underneath, an array indexed by a computed hash.

Practical Next Steps

  1. Re-implement the traversal, in-place, prefix-sum, and sorting examples from memory, then modify remove_duplicates_sorted to work on an unsorted array using a hash set — a natural bridge into the hashing sub-topic.
  2. Practice identifying which pattern a problem statement hints at — "contiguous subarray" often signals sliding window, while "pair that sums to a target" often signals two pointers or a hash map, depending on whether the input is sorted.
  3. Keep the pitfalls list nearby during practice, especially the immutable-string and shallow-copy issues — they resurface constantly in the more advanced patterns ahead, where variables and windows get copied and re-copied frequently.