Type at least 2 letters. Enter opens the first result, Esc closes.

    ML Interview Prep · Lesson 1 · Coding rounds

    Arrays and strings: hashing, two pointers, sliding window

    Beginner 80 min 4 interactive figures 5 videos Colab notebook

    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:

    1. Clarify. Restate the problem. Ask about edge cases: empty input, repeated values, negative numbers, the size of nn, what to return when there is no answer.
    2. Brute force. Say the simplest correct method and its cost in one sentence.
    3. Better plan. Say the faster idea, its time and extra space, and why it is correct (the invariant).
    4. Code while you talk. Use clear names.
    5. Test by hand. Run one normal case, then the edge cases.
    6. 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 nn. CPython (the standard Python) does roughly 10710^7 simple steps per second. For n=105n = 10^5, an O(n2)O(n^2) method needs about 101010^{10} steps, which is about 1,000 seconds. An O(nlog⁡n)O(n \log n) method needs about nlog⁡2n≈1.7⋅106n \log_2 n \approx 1.7 \cdot 10^6 steps, a fraction of a second. So the constraints in the problem tell you what the interviewer expects.

    Interactive figure: loading…

    Move the slider for nn (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 (10710^7 steps). Bars past the line are too slow. Notice how fast n2n^2 crosses the line: at n=105n = 10^5 it is a thousand times over the budget, while nlog⁡nn \log n is far below it.

    Largest nn for about 1 second of Python Complexity Typical method
    about 10 O(n!)O(n!) all permutations
    about 20–23 O(2n)O(2^n) all subsets (backtracking, lesson 04)
    about 200 O(n3)O(n^3) three nested loops
    about 3,000 O(n2)O(n^2) all pairs
    about 5⋅1055 \cdot 10^5 O(nlog⁡n)O(n \log n) sorting, a heap, binary search in a loop
    about 10710^7 O(n)O(n) 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 n(n−1)/2=6n(n-1)/2 = 6 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 O(1)O(1) 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 indices i < 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 nn 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 O(n)O(n).
    Interactive figure: loading…

    Press Next step to walk through "count the subarrays with sum kk". The row PP holds the prefix sums. At each step the code looks up Pj−kP_j - k in the dict of earlier prefix counts; every earlier match is one subarray with sum kk, drawn as a bracket. Then switch off "start with {0: 1}" and run again: the subarrays that start at index 0 are lost.

    Interactive figure: loading…

    Press Next step to run the window for "shortest subarray with sum ≥\ge 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?

    Quick check

    "Given a string, return the length of the longest substring that has at most two distinct characters." Which pattern fits best?

    3. Definitions and main results #

    The cost model #

    f(n)=O(g(n))f(n) = O(g(n)) means: there are constants c>0c > 0 and n0n_0 with f(n)≤c g(n)f(n) \le c\,g(n) for all n≥n0n \ge n_0. 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 O(1)O(1): one operation can be slow, but any sequence of mm operations costs O(m)O(m) in total. Example: list.append sometimes copies the whole list into a larger buffer, but the buffer grows by a constant factor, so the copies add up to O(m)O(m).
    • Average O(1)O(1): the expected cost when the hash function spreads the keys well. A hash table (Python's dict and set) 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 O(n)O(n). Also, hashing a string of length LL costs O(L)O(L), so "O(1)O(1)" really means "O(1)O(1) hash computations".
    Operation (CPython) Cost
    a[i], a[i] = x, a.pop(), a.append(x) O(1)O(1) (append: amortized)
    a.pop(0), a.insert(0, x), x in a O(n)O(n)
    a[i:j] (makes a copy) O(j−i)O(j - i)
    a.sort(), sorted(a) O(nlog⁡n)O(n \log n)
    x in s, s.add(x), d[k], d[k] = v, del d[k] O(1)O(1) average, O(n)O(n) worst
    collections.deque: append, appendleft, popleft O(1)O(1)
    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 each x, look up target - x.
    • Counting. Store value → count, with d.get(x, 0) + 1 or collections.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 jj, the map summarizes nums[0..j-1] and nothing else. So every pair i<ji < j is examined exactly once, at step jj. This is why you look up first and insert second: in the other order, element jj can pair with itself (with target = 6 and x = 3).

    Pattern 2: prefix sums plus a hash map #

    Define the prefix sums P0=0P_0 = 0 and Pj=nums[0]+⋯+nums[j−1]P_j = \text{nums}[0] + \dots + \text{nums}[j-1] for j=1,…,nj = 1, \dots, n. Then, for 0≤i<j≤n0 \le i < j \le n,

    nums[i]+nums[i+1]+⋯+nums[j−1]=Pj−Pi.\text{nums}[i] + \text{nums}[i+1] + \dots + \text{nums}[j-1] = P_j - P_i .

    Claim. The number of subarrays with sum kk equals the number of pairs i<ji < j with Pi=Pj−kP_i = P_j - k. One pass computes it in O(n)O(n) average time and O(n)O(n) extra space.

    Proof.

    1. Each non-empty subarray nums[i..j-1] corresponds to exactly one pair (i,j)(i, j) with i<ji < j, and its sum is Pj−PiP_j - P_i.
    2. Walk j=1,…,nj = 1, \dots, n and keep count[v] = the number of indices i<ji < j with Pi=vP_i = v. Start with count = {0: 1}: this entry is P0P_0, the empty prefix.
    3. At step jj, add count[P_j - k] to the total. These are exactly the pairs (i,j)(i, j) whose subarray sums to kk. Then increase count[P_j].
    4. Each pair is counted once, at the step of its right end jj. Each step does O(1)O(1) average work. ■\blacksquare

    No sign condition is needed: negative numbers are fine. The same idea answers "subarray sum divisible by kk" (store Pj mod kP_j \bmod k), "longest subarray with sum kk" (store the first index of each prefix value), and "equal numbers of 0s and 1s" (map 0 to −1-1, 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: O(n2)O(n^2) instead of O(n3)O(n^3). Skip equal neighbours to avoid repeated triplets.

    Container With Most Water. Given heights h0,…,hn−1h_0, \dots, h_{n-1}, choose i<ji < j to maximize the area (j−i)⋅min⁡(hi,hj)(j - i) \cdot \min(h_i, h_j).

    Claim. Start with i=0i = 0, j=n−1j = n - 1. Record the area, then move the pointer at the shorter side inward (either one if equal). This finds the maximum in O(n)O(n).

    Proof.

    1. Invariant: every pair that is better than the best area recorded so far has both ends in [i,j][i, j].
    2. Suppose hi≤hjh_i \le h_j. Every pair (i,k)(i, k) with i<k<ji < k < j has a smaller width k−i<j−ik - i < j - i and a height min⁡(hi,hk)≤hi\min(h_i, h_k) \le h_i. So its area is less than (j−i) hi(j - i)\,h_i, the area just recorded.
    3. So no pair that uses ii with an end inside the range can beat the record. Pairs with an end outside [i,j][i, j] were excluded before (step 1). Dropping ii keeps the invariant. The case hj<hih_j < h_i is symmetric.
    4. Each step moves one pointer inward, so there are n−1n - 1 steps. ■\blacksquare

    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 kk-th node you must walk kk steps. Two rules prevent most bugs:

    • Save next before 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 dummy in front of the list and return dummy.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 kk nodes apart find the kk-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:

    Interactive figure: loading…

    Set the tail length μ\mu and the cycle length λ\lambda (λ=0\lambda = 0 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 λ\lambda that is at least μ\mu, and phase 2 takes exactly μ\mu steps.

    Theorem (Floyd's cycle detection). Number the nodes in the order of the walk: x0x_0 is the head and xt+1x_{t+1} is the node after xtx_t. If the list has a cycle, let μ\mu be the index of its first node (μ=0\mu = 0 if the head is on the cycle) and λ≥1\lambda \ge 1 the number of nodes on it, so the list has n=μ+λn = \mu + \lambda nodes. Phase 1: slow moves 1 node and fast moves 2 nodes per step, so after tt steps they are at xtx_t and x2tx_{2t}. 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 t∗t^* steps, where t∗t^* is the smallest positive multiple of λ\lambda with t∗≥μt^* \ge \mu. Phase 2 meets exactly at the first cycle node xμx_\mu, after μ\mu steps. Time O(n)O(n), extra space O(1)O(1).

    Proof.

    1. For s,t≥μs, t \ge \mu: xs=xtx_s = x_t if and only if s≡t(modλ)s \equiv t \pmod{\lambda}, because after μ\mu the walk repeats with period λ\lambda. Each node x0,…,xμ−1x_0, \dots, x_{\mu-1} before the cycle appears only once in the walk.
    2. Phase 1 meets at step t≥1t \ge 1 when xt=x2tx_t = x_{2t}. If t<μt < \mu, the node xtx_t appears only at index t≠2tt \ne 2t, so there is no meeting. If t≥μt \ge \mu, step 1 gives a meeting exactly when 2t≡t(modλ)2t \equiv t \pmod{\lambda}, that is, when λ\lambda divides tt. So the first meeting is at t∗t^*.
    3. Multiples of λ\lambda are λ\lambda apart, so t∗≤μ+λ=nt^* \le \mu + \lambda = n: phase 1 is O(n)O(n).
    4. Phase 2 starts with pointer A at x0x_0 and pointer B at xt∗x_{t^*}. After kk steps they are at xkx_k and xt∗+kx_{t^* + k}. For k=μk = \mu: t∗+μ≡μ(modλ)t^* + \mu \equiv \mu \pmod{\lambda}, because λ\lambda divides t∗t^*. So both are at xμx_\mu.
    5. For k<μk < \mu, A is still before the cycle and B is on it, so they cannot meet earlier. ■\blacksquare
    Quick check

    A list has a tail of μ=2\mu = 2 nodes before the cycle, and the cycle has λ=5\lambda = 5 nodes. After how many steps of phase 1 do the tortoise and the hare first meet?

    Pattern 4: the sliding window #

    A sliding window is a contiguous range nums[left..right] that moves from left to right.

    • Fixed size kk. After the first window, each step adds one element and removes one: O(1)O(1) per step instead of O(k)O(k). Example: the maximum sum of kk consecutive values.
    • Variable size. Move right one step at a time. For "longest": while the window is invalid, move left; then record. For "shortest": while the window is valid, record it and move left.

    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 ≥\ge 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 nn times, and left moves at most nn times in total, so the time is O(n)O(n) when add, remove and valid are O(1)O(1).

    Proof.

    1. For each right =r= r, let ℓ(r)\ell(r) be the smallest ll such that the window [l,r][l, r] is valid (the empty window is valid, so ℓ(r)≤r+1\ell(r) \le r + 1). Because valid is monotone under shrinking, [l,r][l, r] is valid exactly when l≥ℓ(r)l \ge \ell(r).
    2. ℓ\ell never decreases: if [l,r+1][l, r+1] is valid, then its part [l,r][l, r] is valid. So ℓ(r)≤ℓ(r+1)\ell(r) \le \ell(r+1).
    3. By induction on rr: when step rr starts, left =ℓ(r−1)≤ℓ(r)= \ell(r-1) \le \ell(r). The while loop moves left until the window is valid, so it stops exactly at ℓ(r)\ell(r).
    4. So the recorded length at step rr is the longest valid window that ends at rr. The maximum over all rr is the answer.
    5. Cost: left only increases and never passes nn, so the inner loop runs at most nn times over the whole run, although it sits inside a for loop. ■\blacksquare
    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".

    1. A window is recorded only inside while valid, so every recorded window is valid.
    2. left moves from ll to l+1l + 1 only right after the valid window [l,right][l, \text{right}] was recorded.
    3. Take an optimal window [l∗,r∗][l^*, r^*]. If left passes l∗l^* at some step r≤r∗r \le r^*, then by step 2 the window [l∗,r][l^*, r] was recorded, and it is not longer than [l∗,r∗][l^*, r^*].
    4. Otherwise left ≤l∗\le l^* when step r∗r^* starts its inner loop. Every window [l,r∗][l, r^*] with l≤l∗l \le l^* contains [l∗,r∗][l^*, r^*], so it is valid (monotone under growing). The loop moves left up to l∗l^* and records [l∗,r∗][l^*, r^*].
    5. Cost: right moves nn times and left at most nn times in total: O(n)O(n). ■\blacksquare
    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 O(1)O(1): 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 O(∣s∣+∣t∣)O(|s| + |t|), extra space O(alphabet)O(\text{alphabet}).

    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 O(n)O(n) time and O(n)O(n) space, and negative numbers are fine. (2) No. With negative numbers a larger window can have a smaller sum, so "sum ≥\ge 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 ≥\ge target" with negatives, prefix sums plus a monotonic deque solve it in O(n)O(n) (LeetCode 862; the deque appears in lesson 02).

    Quick check

    Heights are [1, 8, 6, 2, 5, 4, 8, 3, 7] (Container With Most Water). The first step has i=0i = 0 (h=1h = 1) and j=8j = 8 (h=7h = 7), area 8⋅1=88 \cdot 1 = 8. Which pointer moves, and why is that safe?

    4. Worked example #

    Count the subarrays of nums = [2, 1, -1, 1, 2] with sum k=3k = 3. The prefix sums are P=(0,2,3,2,3,5)P = (0, 2, 3, 2, 3, 5). At step jj we need earlier prefixes equal to Pj−3P_j - 3.

    jj nums[j-1] PjP_j need Pj−3P_j - 3 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] =P2−P0=3−0= P_2 - P_0 = 3 - 0,
    • [2, 1, -1, 1] =P4−P0=3−0= P_4 - P_0 = 3 - 0,
    • [1, -1, 1, 2] =P5−P1=5−2= P_5 - P_1 = 5 - 2,
    • [1, 2] =P5−P3=5−2= P_5 - P_3 = 5 - 2.

    At step 5 one lookup adds 2, because two earlier prefixes (P1P_1 and P3P_3) 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 2n=162n = 16.

    Quick check

    Run the prefix-sum count on nums = [1, -1, 1, -1] with k=0k = 0. The correct answer is 4. What does the code return if the dict starts empty (count = {}) instead of {0: 1}?

    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, O(1)O(1) 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: O(1)O(1) per sample instead of O(k)O(k). 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 xt+1=f(xt)x_{t+1} = f(x_t) with O(1)O(1) 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, a collections.deque, or "".join.
    • Forgetting P0=0P_0 = 0 (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 while inside a for is not automatically O(n2)O(n^2): count the total pointer moves. Dict operations are O(1)O(1) 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.next before saving it.

    8. Check yourself #

    1. a has nn elements and b has mm. What does [x for x in a if x in b] cost when b is a list, and when it is a set?
      AnswerList: O(nm)O(nm), because each in scans b. Set: O(m)O(m) to build it plus nn lookups of O(1)O(1) average, so O(n+m)O(n + m) average.
    2. Run the prefix-sum count on nums = [1, 1, 1], k=2k = 2. Give the total after each step.
      AnswerStart {0: 1}. P=1P = 1: need −1-1, total 0. P=2P = 2: need 0, total 1. P=3P = 3: need 1, total 2. The answer is 2: [1, 1] twice.
    3. Run shortest_window_at_least([2, 3, 1, 2, 4, 3], 7) by hand. Which windows are recorded, and how many times does left move?
      AnswerRecorded lengths: 4 ([2,3,1,2]), 4 ([3,1,2,4]), 3 ([1,2,4]), 3 ([2,4,3]), 2 ([4,3]). left moves 5 times, at most n=6n = 6. The answer is 2.
    4. Why does sorted_squares need O(n)O(n) time and not O(nlog⁡n)O(n \log n)?
      AnswerThe 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: nn steps of O(1)O(1).
    5. All nn keys of a dict have the same hash. What does building the dict cost?
      AnswerEach insert compares with all keys stored so far: O(n)O(n) per insert, O(n2)O(n^2) in total. CPython randomizes string hashes per process, so such inputs are hard to build on purpose.
    6. "Longest subarray with sum exactly kk", with negative numbers. What do you store in the dict, and why the first index?
      AnswerFor each prefix value, the first index where it occurs (start with {0: 0}). At step jj, if Pj−kP_j - k was first seen at ii, the subarray nums[i..j-1] has sum kk and is the longest one that ends at jj, because a smaller ii gives a longer subarray. Never overwrite a stored index.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. In Two Sum with a hash map, what do you store, and in which order do you look up and insert?

    2. Write the sum of nums[i..j-1] with prefix sums.

    3. Why does the prefix-count dict start as {0: 1}?

    4. Count the subarrays with sum kk when numbers can be negative: which method and cost?

    5. When is a variable-size sliding window correct for "longest valid window"?

    6. Why is a sliding window O(n)O(n) although a while loop sits inside the for loop?

    7. In Container With Most Water, which pointer do you move, and why?

    8. Floyd's algorithm: after how many steps do slow and fast first meet?

    9. Floyd's algorithm: how do you find the first node of the cycle?

    10. The constraints say n=105n = 10^5. Which complexity does the interviewer expect, and why?

    Open the review →

    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 kk or is divisible by kk; negatives allowed prefix sums + hash map
    sorted array; pair or triplet; "in place" two pointers
    linked list: middle, kk-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 kk consecutive") fixed sliding window, or prefix sums

    What to say aloud (about 30 seconds before you code): "The brute force checks all pairs, O(n2)O(n^2). I will keep a dict of the prefix sums I have seen, with their counts. At step jj, every earlier prefix equal to Pj−kP_j - k gives one subarray with sum kk. That is one pass: O(n)O(n) time and O(n)O(n) space. I start the dict with P0=0P_0 = 0 so that subarrays from index 0 count."

    Typical questions and short model answers.

    1. "What is the complexity of a Python dict lookup?" — "O(1)O(1) on average, if the hash function spreads the keys well. In the worst case, when many keys collide, it is O(n)O(n). Also, hashing a string costs time linear in its length. CPython randomizes string hashes, so an attacker cannot easily build colliding keys."
    2. "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. O(n)O(n) time and space. If the input is sorted, or if I must use O(1)O(1) extra space, I sort and use two pointers from both ends: O(nlog⁡n)O(n \log n) time, because the sort dominates."
    3. "Why is your sliding window O(n)O(n)? There is a loop inside a loop." — "Both pointers only move forward. right moves nn times, and left can move at most nn times in total over the whole run. So the inner loop body runs at most nn times overall, not nn times per step."
    4. "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."
    5. "Detect a cycle in a linked list with O(1)O(1) 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 O(n)O(n) 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. "O(1)O(1) extra space?" (sort, then two pointers; say that the sort costs O(nlog⁡n)O(n \log n)). "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 O(1)O(1)).

    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:

    1. two_sum: complement lookup with a dict.
    2. subarray_sum_k: count subarrays with sum kk (negative numbers included).
    3. length_of_longest_substring: the longest window without a repeated character.
    4. cycle_start: Floyd's algorithm on a linked list built from μ\mu and λ\lambda.
    5. three_sum (optional): all triplets with sum 0, without repeats, in O(n2)O(n^2).

    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 #

    How a hash table works inside, and why its cost is "expected" O(1), not worst case.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    The prefix-sum and hash map idea of section 3, drawn step by step.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    A variable sliding window with a set; compare it with the table in section 4.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Why the two pointers must meet; then try the cycle start (phase 2) yourself.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    The "move the shorter side" rule; compare it with the proof in section 3.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play