Arrays and strings: hashing, two pointers, sliding window
Before this lesson: basic Python (loops, lists, dicts, functions). Big-O notation is defined below. The Foundations track covers Python and complexity in more depth. Time: about 80 minutes, plus the notebook.
Why this matters #
The ML interview loop at a glance. Interviews for ML roles have several types of rounds. The table shows the usual round types, the roles that usually see them, and the lessons of this track that prepare each one. Companies differ, so treat it as a map, not a rule. MLE = machine learning engineer, RE = research engineer, RS = research scientist.
| Round | Usually for | What the interviewer grades | Lessons |
|---|---|---|---|
| Coding (data structures and algorithms) | MLE, RE; often RS | a correct algorithm, its cost, clean code, testing, clear talk | 01–04 |
| Practical coding (multi-part task) | MLE, RE | working code that grows over several parts, tests, design | 05 |
| ML coding | MLE, RE, RS | ML pieces from scratch in NumPy or PyTorch | 06–08 |
| ML breadth | all | short, precise answers on ML, deep learning, LLMs | 09–11 |
| Training and serving infrastructure | RE, MLE on LLM teams | memory, compute and communication estimates | 12–13 |
| ML system design | MLE (senior), RE | a clear framework, metrics, trade-offs | 14–15 |
| Research talk or paper deep dive | RS, RE | depth, judgment, communication | 16 |
| Behavioral | all | ownership, conflict, scope, reflection | 16 |
Why the coding round matters for ML people. Strong researchers often fail this round. The cause is rarely a missing algorithm. It is the routine under time pressure: a slow brute force, an off-by-one error, a wrong complexity claim. Arrays and strings are the most common inputs, and the four patterns of this lesson solve a large share of these problems. The same ideas run in ML code: hashing to remove duplicate documents from a training set, prefix sums and sliding windows over time series.
How to use this track so that you do not forget. Reading a solution feels like learning, but it fades in days. What lasts is retrieval: answering from memory, spaced over time. Each lesson asks you to predict before you read, has quizzes inside the text, and ends with recall cards. Add the cards to your review deck and answer them when they come back. Then solve the notebook exercises without looking at the lesson, and do the timed problems in section 10 aloud.
1. Predict first #
2. The picture #
The routine for every coding problem #
Interviewers grade the process, not only the final code. Follow the same six steps every time:
- Clarify. Restate the problem. Ask about edge cases: empty input, repeated values, negative numbers, the size of , what to return when there is no answer.
- Brute force. Say the simplest correct method and its cost in one sentence.
- Better plan. Say the faster idea, its time and extra space, and why it is correct (the invariant).
- Code while you talk. Use clear names.
- Test by hand. Run one normal case, then the edge cases.
- Final complexity. State time and extra space again.
The brute force is not wasted. It is your test oracle: in the notebook, every fast solution is compared with a brute force on many random inputs.
Size decides the method #
We count basic steps as a function of the input size . CPython (the standard Python) does roughly simple steps per second. For , an method needs about steps, which is about 1,000 seconds. An method needs about steps, a fraction of a second. So the constraints in the problem tell you what the interviewer expects.
Move the slider for (the input size, on a log scale). Each bar is the number of steps of one complexity class; the dashed line is about one second of Python ( steps). Bars past the line are too slow. Notice how fast crosses the line: at it is a thousand times over the budget, while is far below it.
| Largest for about 1 second of Python | Complexity | Typical method |
|---|---|---|
| about 10 | all permutations | |
| about 20–23 | all subsets (backtracking, lesson 04) | |
| about 200 | three nested loops | |
| about 3,000 | all pairs | |
| about | sorting, a heap, binary search in a loop | |
| about | one pass with a hash map, two pointers, a window |
Four ways to remove the inner loop #
The simplest case. Does [3, 1, 4, 1] contain a repeated value? The brute force compares
all pairs. With a set: see 3, see 1, see 4, then 1 is already in the set, so the
answer is yes. One pass, one lookup per element. This is the whole idea of the first
pattern. The other three patterns remove the inner loop in other ways:
- Hash map: remember what you have seen. A dict summarizes
nums[0..j-1]. One lookup replaces the loop over all earlier indicesi < j. - Prefix sums: a range sum is a difference. The sum of a subarray is the difference of two prefix sums. A question about subarrays becomes a question about pairs of numbers, and a hash map answers it.
- Two pointers: use the order. On sorted data, one comparison shows that one end cannot be part of a better answer. Drop it. Each step removes one element, so there are at most steps.
- Sliding window: reuse the last window. Add the element that enters, remove the element that leaves. Both pointers only move forward, so the total work is .
Press Next step to walk through "count the subarrays with sum ". The row holds the prefix sums. At each step the code looks up in the dict of earlier prefix counts; every earlier match is one subarray with sum , drawn as a bracket. Then switch off "start with {0: 1}" and run again: the subarrays that start at index 0 are lost.
Press Next step to run the window for "shortest subarray with sum target". The right pointer adds one element per step; while the window is valid, the code records its length and the left pointer removes one element. The counters show that each index enters once and leaves at most once. Then choose the preset with negative numbers. Before you step, predict: will the window find the true shortest subarray?
"Given a string, return the length of the longest substring that has at most two distinct characters." Which pattern fits best?
The problem asks for the longest contiguous range with a condition, and the condition is monotone: if a window has at most two distinct characters, every smaller window inside it also has. So the window can grow on the right and shrink on the left in . Sorting would destroy the substring structure, and prefix sums help with sums, not with "distinct characters".
3. Definitions and main results #
The cost model #
means: there are constants and with for all . In an interview you state the time and the extra space: the memory you use besides the input and the output.
Two words hide details, and interviewers like to ask about them:
- Amortized : one operation can be slow, but any sequence of operations costs
in total. Example:
list.appendsometimes copies the whole list into a larger buffer, but the buffer grows by a constant factor, so the copies add up to . - Average : the expected cost when the hash function spreads the keys well. A
hash table (Python's
dictandset) stores each key in a slot chosen by its hash. When many keys land in the same slots (a collision), lookups must compare with all of them. In the worst case (all keys collide) one operation costs . Also, hashing a string of length costs , so "" really means " hash computations".
| Operation (CPython) | Cost |
|---|---|
a[i], a[i] = x, a.pop(), a.append(x) |
(append: amortized) |
a.pop(0), a.insert(0, x), x in a |
|
a[i:j] (makes a copy) |
|
a.sort(), sorted(a) |
|
x in s, s.add(x), d[k], d[k] = v, del d[k] |
average, worst |
collections.deque: append, appendleft, popleft |
|
out += piece on strings, in a loop |
can be quadratic in the total length; collect a list and call "".join once |
Pattern 1: the hash map #
A hash map has three common uses:
- Complement lookup. "Is there an earlier element that pairs with
nums[j]?" Store value → index. Two Sum: for eachx, look uptarget - x. - Counting. Store value → count, with
d.get(x, 0) + 1orcollections.Counter. - Grouping by a canonical key. A canonical key is a value that is equal exactly for the members of one group. For anagrams, the sorted letters of a word, or the tuple of its 26 letter counts. Keys must be hashable: use a tuple, not a list.
def two_sum(nums, target):
seen = {} # value -> index, for nums[0..j-1] only
for j, x in enumerate(nums):
if target - x in seen: # look up first ...
return seen[target - x], j
seen[x] = j # ... then insert
return None # O(n) average time, O(n) extra space
Invariant. At the start of step , the map summarizes nums[0..j-1] and nothing else. So
every pair is examined exactly once, at step . This is why you look up first and insert
second: in the other order, element can pair with itself (with target = 6 and x = 3).
Pattern 2: prefix sums plus a hash map #
Define the prefix sums and for . Then, for ,
Claim. The number of subarrays with sum equals the number of pairs with . One pass computes it in average time and extra space.
Proof.
- Each non-empty subarray
nums[i..j-1]corresponds to exactly one pair with , and its sum is . - Walk and keep
count[v]= the number of indices with . Start withcount = {0: 1}: this entry is , the empty prefix. - At step , add
count[P_j - k]to the total. These are exactly the pairs whose subarray sums to . Then increasecount[P_j]. - Each pair is counted once, at the step of its right end . Each step does average work.
No sign condition is needed: negative numbers are fine. The same idea answers "subarray sum divisible by " (store ), "longest subarray with sum " (store the first index of each prefix value), and "equal numbers of 0s and 1s" (map 0 to , then look for sum 0).
Pattern 3: two pointers #
From both ends (sorted input). Start with i = 0 and j = n - 1. At each step, the order
shows that nums[i] or nums[j] cannot be part of any better answer, so that pointer moves.
Two Sum II on a sorted list: if nums[i] + nums[j] is too small, no partner of nums[i] inside
the range is large enough, so i += 1; if it is too large, j -= 1. 3Sum sorts the list,
fixes the first element, and runs this two-pointer search on the rest: instead of
. Skip equal neighbours to avoid repeated triplets.
Container With Most Water. Given heights , choose to maximize the area .
Claim. Start with , . Record the area, then move the pointer at the shorter side inward (either one if equal). This finds the maximum in .
Proof.
- Invariant: every pair that is better than the best area recorded so far has both ends in .
- Suppose . Every pair with has a smaller width and a height . So its area is less than , the area just recorded.
- So no pair that uses with an end inside the range can beat the record. Pairs with an end outside were excluded before (step 1). Dropping keeps the invariant. The case is symmetric.
- Each step moves one pointer inward, so there are steps.
Read/write pointers (same direction). r reads every element and w marks the end of the
part you keep. Invariant: nums[0..w-1] is the answer for nums[0..r-1]. Examples: remove all
copies of a value in place, remove repeats from a sorted list in place, move all zeros to the end.
Fast and slow pointers on a linked list. A singly linked list is a chain of nodes; each
node has a value val and a pointer next to the next node (the last one points to None). To
reach the -th node you must walk steps. Two rules prevent most bugs:
- Save
nextbefore you re-link. Reversal moves three pointers together:nxt = cur.next; cur.next = prev; prev = cur; cur = nxt. - Use a dummy head when the head may change (merge, removal): put a node
dummyin front of the list and returndummy.next. Then there is always a node before the one you change.
With a slow pointer that moves 1 node per step and a fast pointer that moves 2: when fast reaches the end, slow is at the middle. Two pointers nodes apart find the -th node from the end in one pass. And on a list that may end in a cycle, the same two pointers detect the cycle:
Set the tail length and the cycle length ( means no cycle), then press Next step. In phase 1 the tortoise moves 1 node per step and the hare moves 2. After they meet, phase 2 puts the tortoise back at the head and moves both by 1: they meet at the first node of the cycle. Check the readout: the first meeting is at the smallest multiple of that is at least , and phase 2 takes exactly steps.
Theorem (Floyd's cycle detection). Number the nodes in the order of the walk: is the
head and is the node after . If the list has a cycle, let be the index of its
first node ( if the head is on the cycle) and the number of nodes on it, so
the list has nodes. Phase 1: slow moves 1 node and fast moves 2 nodes per
step, so after steps they are at and . Phase 2: after they meet, put one pointer
back at the head and move both by 1 node per step.
Claim. Without a cycle, fast reaches None. With a cycle, phase 1 meets after steps,
where is the smallest positive multiple of with . Phase 2 meets exactly
at the first cycle node , after steps. Time , extra space .
Proof.
- For : if and only if , because after the walk repeats with period . Each node before the cycle appears only once in the walk.
- Phase 1 meets at step when . If , the node appears only at index , so there is no meeting. If , step 1 gives a meeting exactly when , that is, when divides . So the first meeting is at .
- Multiples of are apart, so : phase 1 is .
- Phase 2 starts with pointer A at and pointer B at . After steps they are at and . For : , because divides . So both are at .
- For , A is still before the cycle and B is on it, so they cannot meet earlier.
A list has a tail of nodes before the cycle, and the cycle has nodes. After how many steps of phase 1 do the tortoise and the hare first meet?
The first meeting is at the smallest positive multiple of that is at least : the multiples of 5 are 5, 10, …, and , so . At step 2 the tortoise has only just reached the cycle; the meeting needs , which on the cycle means divides . Phase 2 then takes more steps.
Pattern 4: the sliding window #
A sliding window is a contiguous range nums[left..right] that moves from left to right.
- Fixed size . After the first window, each step adds one element and removes one: per step instead of . Example: the maximum sum of consecutive values.
- Variable size. Move
rightone step at a time. For "longest": while the window is invalid, moveleft; then record. For "shortest": while the window is valid, record it and moveleft.
The variable window needs a property of the condition. Call a condition on windows monotone under shrinking if every window inside a valid window is also valid ("no repeated character" has this property). Call it monotone under growing if every window that contains a valid window is also valid ("sum target" with positive numbers has this property).
Theorem (variable window, longest version). If "valid" is monotone under shrinking, then the loop
left = 0
for right in range(n):
add(nums[right])
while not valid():
remove(nums[left]); left += 1
best = max(best, right - left + 1)
returns the length of the longest valid window. The loop body runs times, and left moves at
most times in total, so the time is when add, remove and valid are .
Proof.
- For each
right, let be the smallest such that the window is valid (the empty window is valid, so ). Because valid is monotone under shrinking, is valid exactly when . - never decreases: if is valid, then its part is valid. So .
- By induction on : when step starts,
left. Thewhileloop movesleftuntil the window is valid, so it stops exactly at . - So the recorded length at step is the longest valid window that ends at . The maximum over all is the answer.
- Cost:
leftonly increases and never passes , so the inner loop runs at most times over the whole run, although it sits inside aforloop.
The shortest version (monotone under growing)
Let "valid" be monotone under growing, and use the loop "add nums[right]; while valid: record
right - left + 1, remove nums[left], left += 1".
- A window is recorded only inside
while valid, so every recorded window is valid. leftmoves from to only right after the valid window was recorded.- Take an optimal window . If
leftpasses at some step , then by step 2 the window was recorded, and it is not longer than . - Otherwise
leftwhen step starts its inner loop. Every window with contains , so it is valid (monotone under growing). The loop movesleftup to and records . - Cost:
rightmoves times andleftat most times in total: .
The hard case: Minimum Window Substring
"Given strings s and t, return the shortest substring of s that contains every character
of t (with repeats)." Valid = "the window contains all of t", which is monotone under growing,
so the shortest version applies. The trick is to test validity in : keep need[c] = how
many more copies of c the window needs (it may go negative), and missing = the total number of
characters of t still missing.
from collections import Counter
def min_window(s, t):
need, missing = Counter(t), len(t)
left, best = 0, (0, float("inf")) # best = (start, end) of the shortest window
for right, ch in enumerate(s):
if need[ch] > 0:
missing -= 1 # ch was still needed
need[ch] -= 1
while missing == 0: # valid: record, then shrink
if right - left < best[1] - best[0]:
best = (left, right)
need[s[left]] += 1
if need[s[left]] > 0:
missing += 1 # we just removed a needed character
left += 1
return "" if best[1] == float("inf") else s[best[0]:best[1] + 1]
print(min_window("ADOBECODEBANC", "ABC")) # BANC
Time , extra space .
Answers to section 1. (1) Yes. Store a dict of prefix-sum counts, starting with {0: 1}. At
each step add count[P_j - k] to the total. This is time and space, and negative
numbers are fine. (2) No. With negative numbers a larger window can have a smaller sum, so "sum
target" is no longer monotone under growing, and step 4 of the proof fails. Example:
[1, -1, 5] with target 5. The window method returns 3 (the whole list), but [5] has length 1.
Use prefix sums instead; for "shortest subarray with sum target" with negatives, prefix sums
plus a monotonic deque solve it in (LeetCode 862; the deque appears in
lesson 02).
Heights are [1, 8, 6, 2, 5, 4, 8, 3, 7] (Container With Most Water). The first step has () and (), area . Which pointer moves, and why is that safe?
With , every pair that uses and an end inside the range has a smaller width and a height , so it cannot beat the area just recorded. So can be dropped. Moving the taller side could skip the optimum (here the best pair is with area 49).
4. Worked example #
Count the subarrays of nums = [2, 1, -1, 1, 2] with sum . The prefix sums are
. At step we need earlier prefixes equal to .
nums[j-1] |
need | count[need] |
total | count after the step |
||
|---|---|---|---|---|---|---|
| start | 0 | 0 | {0: 1} | |||
| 1 | 2 | 2 | −1 | 0 | 0 | {0: 1, 2: 1} |
| 2 | 1 | 3 | 0 | 1 | 1 | {0: 1, 2: 1, 3: 1} |
| 3 | −1 | 2 | −1 | 0 | 1 | {0: 1, 2: 2, 3: 1} |
| 4 | 1 | 3 | 0 | 1 | 2 | {0: 1, 2: 2, 3: 2} |
| 5 | 2 | 5 | 2 | 2 | 4 | {0: 1, 2: 2, 3: 2, 5: 1} |
The answer is 4:
[2, 1],[2, 1, -1, 1],[1, -1, 1, 2],[1, 2].
At step 5 one lookup adds 2, because two earlier prefixes ( and ) have the value 2. A set of prefix values, instead of counts, would find only one of them. A sliding window would fail here too, because the list has a negative number.
A second example: the longest substring without repeating characters. For s = "abcabcbb",
the condition "no repeated character" is monotone under shrinking, so the longest version
applies. Keep a set of the characters in the window.
right |
char | while loop removes |
window | length | best |
|---|---|---|---|---|---|
| 0 | a | a | 1 | 1 | |
| 1 | b | ab | 2 | 2 | |
| 2 | c | abc | 3 | 3 | |
| 3 | a | a | bca | 3 | 3 |
| 4 | b | b | cab | 3 | 3 |
| 5 | c | c | abc | 3 | 3 |
| 6 | b | a, b | cb | 2 | 3 |
| 7 | b | c, b | b | 1 | 3 |
The answer is 3. right moved 8 times and left moved 7 times: 15 moves, at most .
Run the prefix-sum count on nums = [1, -1, 1, -1] with . The correct answer is 4. What does the code return if the dict starts empty (count = {}) instead of {0: 1}?
The prefix sums are . With in the dict, the pairs of equal prefixes are for value 0 and for value 1: 4 subarrays. Without , every pair with is lost, which leaves and : 2. These lost pairs are exactly the subarrays that start at index 0.
5. In code #
Three templates, each checked against a brute force on random inputs. Copy this habit: in an interview, the brute force is your test oracle.
import random
def count_subarrays_with_sum(nums, k):
"""Number of contiguous subarrays with sum k. O(n) time, O(n) space."""
count = {0: 1} # prefix-sum value -> number of prefixes with that value; P[0] = 0
prefix = total = 0
for x in nums:
prefix += x # prefix = P[j]
total += count.get(prefix - k, 0) # earlier prefixes P[i] = P[j] - k
count[prefix] = count.get(prefix, 0) + 1
return total
def sorted_squares(nums):
"""Squares of a sorted list, in sorted order. O(n) time: two pointers from both ends."""
out = [0] * len(nums)
i, j = 0, len(nums) - 1
for w in range(len(nums) - 1, -1, -1): # fill from the largest square down
if abs(nums[i]) > abs(nums[j]): # the largest remaining square is at an end
out[w] = nums[i] * nums[i]
i += 1
else:
out[w] = nums[j] * nums[j]
j -= 1
return out
def shortest_window_at_least(nums, target):
"""All nums > 0: length of the shortest subarray with sum >= target, 0 if none."""
best = float("inf")
left = window = 0 # window = sum(nums[left:right + 1])
for right, x in enumerate(nums):
window += x # x enters
while window >= target: # valid: record it, then shrink from the left
best = min(best, right - left + 1)
window -= nums[left] # nums[left] leaves
left += 1
return 0 if best == float("inf") else best
# Brute-force references: slow but clearly correct.
def brute_count(nums, k):
return sum(sum(nums[i:j]) == k for i in range(len(nums)) for j in range(i + 1, len(nums) + 1))
def brute_shortest(nums, target):
lengths = [j - i for i in range(len(nums)) for j in range(i + 1, len(nums) + 1)
if sum(nums[i:j]) >= target]
return min(lengths, default=0)
rng = random.Random(0)
for _ in range(2000):
nums = [rng.randint(-3, 3) for _ in range(rng.randint(0, 10))]
k = rng.randint(-4, 4)
assert count_subarrays_with_sum(nums, k) == brute_count(nums, k)
assert sorted_squares(sorted(nums)) == sorted(x * x for x in nums)
pos = [rng.randint(1, 5) for _ in range(rng.randint(0, 10))]
t = rng.randint(1, 15)
assert shortest_window_at_least(pos, t) == brute_shortest(pos, t)
print(count_subarrays_with_sum([2, 1, -1, 1, 2], 3)) # 4
print(sorted_squares([-4, -1, 0, 3, 10])) # [0, 1, 9, 16, 100]
print(shortest_window_at_least([2, 3, 1, 2, 4, 3], 7)) # 2
print(shortest_window_at_least([1, -1, 5], 5), brute_shortest([1, -1, 5], 5)) # 3 1
print("2000 random tests passed")
The fourth line shows the failure with negative numbers: the window answers 3, the brute force 1.
6. Where it shows up #
- Data pipelines. Exact deduplication of a training corpus keeps a set of document hashes: one pass, average per document. Near-duplicate detection (MinHash) also hashes, then groups documents by a key. Vocabularies and the hashing trick for features are hash maps.
- Time series and science. A moving average over a signal is a fixed-size window: per
sample instead of . The classic STA/LTA earthquake trigger is the ratio of a short-term
and a long-term moving average of the signal energy: two windows. With
np.cumsum(a prefix sum), the mean of any window is one subtraction; summed-area tables (integral images) do the same for 2-D images. - Sorted streams. Matching detected events to a catalog, both sorted by time, within a tolerance, is one two-pointer pass instead of a double loop.
- Cycle detection beyond lists. Floyd's method finds cycles in any sequence with memory: in pseudo-random number generators and in Pollard's rho factoring method. The problem "find the duplicate number" (LeetCode 287) is this idea on an array.
7. Common mistakes #
- Insert before lookup in a complement search: the element pairs with itself.
- Hidden linear costs.
x in some_list,a.pop(0)or string+=inside a loop make the code quadratic. Use a set, acollections.deque, or"".join. - Forgetting (
count = {0: 1}): you miss every subarray that starts at index 0. - A sliding window on data with negative numbers for a sum condition: the monotonicity it needs is gone. Use prefix sums.
- Off by one in the window length: it is
right - left + 1. - Wrong complexity claims. A
whileinside aforis not automatically : count the total pointer moves. Dict operations are on average, not in the worst case. - Unhashable keys. A list cannot be a dict key; use a tuple (for example, of 26 counts).
- Losing the rest of a linked list by overwriting
cur.nextbefore saving it.
8. Check yourself #
ahas elements andbhas . What does[x for x in a if x in b]cost whenbis a list, and when it is a set?Answer
List: , because eachinscansb. Set: to build it plus lookups of average, so average.- Run the prefix-sum count on
nums = [1, 1, 1], . Give the total after each step.Answer
Start{0: 1}. : need , total 0. : need 0, total 1. : need 1, total 2. The answer is 2:[1, 1]twice. - Run
shortest_window_at_least([2, 3, 1, 2, 4, 3], 7)by hand. Which windows are recorded, and how many times doesleftmove?Answer
Recorded lengths: 4 ([2,3,1,2]), 4 ([3,1,2,4]), 3 ([1,2,4]), 3 ([2,4,3]), 2 ([4,3]).leftmoves 5 times, at most . The answer is 2. - Why does
sorted_squaresneed time and not ?Answer
The squares of a sorted list first decrease, then increase (a valley). The largest remaining square is always at one of the two ends, so each step places one value in its final position: steps of . - All keys of a dict have the same hash. What does building the dict cost?
Answer
Each insert compares with all keys stored so far: per insert, in total. CPython randomizes string hashes per process, so such inputs are hard to build on purpose. - "Longest subarray with sum exactly ", with negative numbers. What do you store in the dict,
and why the first index?
Answer
For each prefix value, the first index where it occurs (start with{0: 0}). At step , if was first seen at , the subarraynums[i..j-1]has sum and is the longest one that ends at , because a smaller gives a longer subarray. Never overwrite a stored index.
In Two Sum with a hash map, what do you store, and in which order do you look up and insert?
Store value → index of earlier elements. For each
x, first look uptarget - x, then insertx, so an element never pairs with itself.Write the sum of
nums[i..j-1]with prefix sums., where and .
Why does the prefix-count dict start as
{0: 1}?It stores , the empty prefix. Without it you miss every subarray that starts at index 0.
Count the subarrays with sum when numbers can be negative: which method and cost?
Prefix sums plus a dict of prefix counts: add
count[P_j - k]at each step. average time, space.When is a variable-size sliding window correct for "longest valid window"?
When validity is monotone under shrinking: every window inside a valid window is also valid.
Why is a sliding window although a
whileloop sits inside theforloop?rightmoves times andleftonly moves forward, at most times in total. Each move is .In Container With Most Water, which pointer do you move, and why?
The one at the shorter side. Every other pair with that end inside the range is narrower and not taller, so it cannot beat the current area.
Floyd's algorithm: after how many steps do slow and fast first meet?
At , the smallest positive multiple of the cycle length with (the tail length).
Floyd's algorithm: how do you find the first node of the cycle?
After the meeting, put one pointer at the head and move both by 1 node per step. They meet at the cycle start after steps.
The constraints say . Which complexity does the interviewer expect, and why?
or . is steps, about 1,000 seconds in Python.
9. Interview angle #
Recognize the pattern from the wording.
| The problem says | Try |
|---|---|
| pair with a given sum or difference; "seen before"; duplicates | hash map of value → index (or a set) |
| count or group by an equivalence (anagrams) | dict with a canonical key |
| subarray sum equals or is divisible by ; negatives allowed | prefix sums + hash map |
| sorted array; pair or triplet; "in place" | two pointers |
| linked list: middle, -th from the end, cycle | fast and slow pointers |
| longest or shortest contiguous range with a condition | sliding window (check monotonicity first) |
| fixed-size range ("every consecutive") | fixed sliding window, or prefix sums |
What to say aloud (about 30 seconds before you code): "The brute force checks all pairs, . I will keep a dict of the prefix sums I have seen, with their counts. At step , every earlier prefix equal to gives one subarray with sum . That is one pass: time and space. I start the dict with so that subarrays from index 0 count."
Typical questions and short model answers.
- "What is the complexity of a Python dict lookup?" — " on average, if the hash function spreads the keys well. In the worst case, when many keys collide, it is . Also, hashing a string costs time linear in its length. CPython randomizes string hashes, so an attacker cannot easily build colliding keys."
- "Find two numbers that sum to a target." — "Unsorted: one pass with a dict from value to index; for each number I look up the complement before I insert the number. time and space. If the input is sorted, or if I must use extra space, I sort and use two pointers from both ends: time, because the sort dominates."
- "Why is your sliding window ? There is a loop inside a loop." — "Both pointers only
move forward.
rightmoves times, andleftcan move at most times in total over the whole run. So the inner loop body runs at most times overall, not times per step." - "Does your window work with negative numbers?" — "No. With negatives, a longer window can have a smaller sum, so shrinking the window can make it valid again, and the greedy shrink can miss the answer. For sums with negatives I use prefix sums and a hash map, or a monotonic deque for the 'at least target' version."
- "Detect a cycle in a linked list with memory." — "Tortoise and hare: one pointer moves 1 node, the other 2 nodes per step. If there is no cycle, the fast one reaches the end. If there is a cycle, once both are on it the gap between them changes by one node per step, so they meet within steps. To find the cycle start, I put one pointer back at the head and move both by one; they meet at the start, because the meeting step is a multiple of the cycle length."
Typical follow-ups. " extra space?" (sort, then two pointers; say that the sort costs ). "The data is a stream?" (one pass with a dict or a window; you cannot sort). "Too big for memory?" (hash-partition into files so that equal keys land in the same file, then solve each file). "Many queries on the same array?" (precompute prefix sums once, then each range sum is ).
10. Practice #
The notebook for this lesson runs in Colab. Try each exercise from memory before you look back at the lesson. Each one is tested against a brute force on random inputs:
two_sum: complement lookup with a dict.subarray_sum_k: count subarrays with sum (negative numbers included).length_of_longest_substring: the longest window without a repeated character.cycle_start: Floyd's algorithm on a linked list built from and .three_sum(optional): all triplets with sum 0, without repeats, in .
Timed practice (25 minutes each, talking aloud; the names are the usual problem titles): Contains Duplicate, Valid Anagram, Group Anagrams, Longest Consecutive Sequence, Two Sum II, 3Sum, Container With Most Water, Longest Repeating Character Replacement, Linked List Cycle II, Minimum Window Substring (hard).
Say it aloud drill. Pick one problem from the list. Set a 2-minute timer and explain the brute force, the pattern, the invariant, and the time and space, without code. Record yourself once a week and listen for unclear steps.
11. Watch and read #
- MIT 6.006 Introduction to Algorithms, Spring 2020, course page with notes and problem sets: lecture 2 (dynamic arrays and amortized cost) and lecture 4 (hashing).
- Python documentation, Time complexity of operations on built-in types: list, dict, set and deque costs, with the notes on amortized and average cost.
- Tech Interview Handbook, cheatsheets for arrays, strings, hash tables and linked lists.
- Public problem lists: Grind 75 (choose the topics Array, String, Hash Table, Linked List) and NeetCode 150 (sections Arrays & Hashing, Two Pointers, Sliding Window).
- Wikipedia, Cycle detection: Floyd's and Brent's algorithms, with the same and notation as this lesson.
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.