cswork / libraryOpen practice workspace

Articles

How to Dry Run Code in a Coding Interview: Traces, Invariants, and Boundary Cases

Learn to dry run coding interview solutions with a trace table, loop invariant, adversarial boundary cases, and a clear account of what remains unverified.

“I dry-ran it and it looked fine” is not a convincing coding-interview answer. A dry run is useful only when it exposes the state that determines correctness and when the chosen input could reveal the bug you suspect. Walking through one friendly example while silently skipping loop updates is closer to storytelling than testing.

This guide gives you an original sliding-window exercise, a complete trace, the invariant behind the trace, and a set of boundary cases that change the algorithm. It also marks what a dry run cannot establish. The exercise is illustrative; it is not a report that any employer asks this exact problem. For related practice, Test Severity Filtering with Contract-Distinguishing Cases shows the value of selecting inputs that separate interpretations.

What should a useful dry run contain?

Write down the function contract first. For this exercise, given a list of nonnegative integers and a positive target, return the length of the shortest contiguous subarray whose sum is at least the target; return zero if none exists. “Nonnegative” and “positive” are not decorative. They justify the sliding-window rule used below. If either condition is absent, ask before committing to the algorithm.

Then choose a trace table with only state that can affect the answer: right, incoming value, left, window sum, candidate length, and best length. Add a short note for each shrink. Do not copy the entire array into every row. The purpose is to make each transition auditable.

Finally, state an invariant that connects the code to the problem. For a window [left, right], total must equal the sum of exactly those elements. After the inner loop finishes, total < target. Whenever total >= target, the current window is a valid candidate, so record its length before removing the leftmost element. The nonnegative-input promise means extending the window never decreases its sum and shrinking never increases it.

Those statements give you something falsifiable. If your table says left = 3, right = 4, and total = 6 for values [2, 4], it is consistent. If it says total = 8, you have lost an update. If a valid window is shortened without recording a candidate, the answer can be wrong even when the final sum looks sensible.

Work the trace, not just the sample output

Start with nums = [2, 1, 3, 2, 4] and target = 6. The correct answer is two, from the final [2, 4]. Here is the intended code:

def shortest_window(nums, target):
    if target <= 0:
        raise ValueError("target must be positive")
    if any(value < 0 for value in nums):
        raise ValueError("values must be nonnegative")

    left = 0
    total = 0
    best = len(nums) + 1
    for right, value in enumerate(nums):
        total += value
        while total >= target:
            best = min(best, right - left + 1)
            total -= nums[left]
            left += 1
    return 0 if best == len(nums) + 1 else best

Trace every inner-loop pass, not only every outer-loop iteration. That is where the shortest window can appear:

Eventrightleft before actiontotal before actionCandidatebest after action
Add 2002—6 (sentinel)
Add 1103—6
Add 3; shrink20633
Add 2; shrink31633
Add 4; first shrink42933
Same right; second shrink43622

After each shrink, subtract nums[left] and advance left. The table deliberately records totals before subtraction. For example, at right = 4, the first valid window is [3, 2, 4], sum nine, length three. Remove three and the remaining [2, 4] still sums to six. The second while pass records length two. Remove two and the sum becomes four, so the loop stops.

That final second pass is the trap. If you use if total >= target rather than while, the code may record only length three. A casual narration—“the window is now valid, so move left”—can miss the need to test it again without moving right.

Derive the invariant in plain language

The core equality is total == sum(nums[left:right + 1]) whenever the window is nonempty. At initialization, both sides are zero for an empty window. Adding nums[right] maintains equality when the right boundary moves. Subtracting nums[left] before incrementing left maintains equality when the left boundary moves. This is a loop invariant: a property maintained by each transition, not merely an observation about the final output.

The second useful property is: when the inner loop finishes for a fixed right, the remaining window sum is below target. With nonnegative values, any earlier start would have made a window at least as large, and those valid starts were considered during shrinking. You can therefore move to the next right without losing a shorter valid window ending at the current position.

Do not overstate the proof. The table proves nothing for all inputs by itself; the invariant and monotonicity argument explain why the algorithm generalizes. Conversely, a proof sketch without tracing a tricky sample may hide an off-by-one update. Use both. In an interview, say what each variable means, show how every branch preserves that meaning, and then demonstrate the difficult transition once.

The time complexity is O(n) after validation: right increases at most n times and left increases at most n times. The nested while does not imply O(n²) because the total number of left-boundary moves across all outer iterations is bounded by n. The validation scan is another O(n) pass, not a change in asymptotic complexity. Extra state is O(1), excluding the input.

Choose boundary cases that can change the answer

Do not say “I would test edge cases” and stop. Name an input, expected output, and the rule it challenges:

Input and targetExpectedRule challenged
[], 60Empty input and sentinel return
[6], 61Equality at the threshold
[5], 60No qualifying window
[0, 0, 6], 61Repeated shrinking past zeros
[2, 1, 3, 2, 4], 62More than one shrink at the same right
[1, -1, 6], 6Reject under this contractNegative value breaks the chosen guarantee

The zero case matters. When [0, 0, 6] reaches the six, the inner loop should shrink through both zeros and eventually record the one-element [6]. A code path that advances left only once produces a longer answer. The no-solution case matters because best starts as a sentinel, not as a valid length.

Negative numbers need special treatment. This particular implementation rejects them. If the interviewer explicitly permits negatives, the monotonic-window reasoning fails: removing a negative can increase the sum, and extending by a negative can decrease it. Do not quietly use the same proof. Ask whether a different algorithm, such as a prefix-sum/deque approach for the generalized problem, is expected. The exact alternative depends on the task's constraints; naming one is not a license to skip its own correctness argument.

Also ask what target zero means if the prompt allows it. Mathematically, an empty subarray might qualify under one convention, while an interview task may require a nonempty subarray. This exercise avoids that ambiguity by requiring target > 0 and raising for other values. State the choice instead of letting a loop accidentally define the API.

Diagnose a deliberately broken variant

Now replace the while with if and dry-run the same input. At right = 4, you see [3, 2, 4], record length three, remove three, and exit the if. The remaining [2, 4] is still valid, but the program advances to the next outer iteration, which never arrives. It returns three instead of two.

This is a strong interview explanation because it gives a minimal counterexample to the code's mistaken rule: “one shrink is enough per new element.” You can even reduce the input to [1, 1, 6] with target six. At the final element, several valid windows exist and the shortest is [6]. If the code only shrinks once, it returns two or three depending on its update order. The exact trace tells you which.

When a dry run disagrees with an expected result, avoid randomly editing conditions. Reconstruct the contract, find the first row at which the invariant or a required candidate is lost, and change the operation at that point. Then rerun the smallest failing case and one ordinary case. A patch that fixes one output while breaking empty input is not a verified repair.

Know what mental execution cannot verify

A dry run can expose an arithmetic mistake, an incorrect branch, an off-by-one boundary, or an invalid algorithmic assumption. It cannot prove that the actual source code has no syntax error, that a language library behaves as remembered, that integer arithmetic will not overflow under the platform's type limits, or that performance is acceptable at the largest input. It also cannot replace automated tests over a wider set of inputs.

When execution is allowed, translate the table into assertions and run them. Python's unittest reference provides assertion patterns, but a simple local harness works too. The expected values should come from the problem contract and independent reasoning. Do not use the function under test to generate its own expected values.

When execution is unavailable, report the narrower conclusion: “This trace covers one ordinary and two adversarial cases; the invariant argues for all nonnegative inputs; runtime behavior remains unverified.” That is honest and still strong. Princeton's coding interview preparation guide is general preparation guidance; it does not make a hand trace a substitute for a program run.

A concise interview script

You can say: “I am going to track left, right, and total in a table because the bug risk is in repeated shrinking. My invariant is that total equals the current window sum. On [2,1,3,2,4] at target six, the last element forces two shrink steps and reveals a length-two answer. I will also check empty, equality, zeros, and no-solution cases. This approach assumes nonnegative values; if negatives are allowed, I need another algorithm.”

That is not a rehearsed monologue. It is a compact method for making your reasoning inspectable. The interviewer can challenge any assumption and you can adjust without pretending your first path was universally valid.

Practice questions that reward explicit traces

Use the exact contract on each page rather than importing this sliding-window solution unchanged.

QuestionTrace focus
Test Severity Filtering with Contract-Distinguishing CasesInputs that distinguish two interpretations
Validate Unit-Test Coverage and Identify Missing ScenariosMissing branches and boundary behavior
Debug a Test-Driven C++ ProjectFirst failing state versus final symptom
Debug the Last-Element Random Allocation CaseIndex and last-element transitions
Design unit tests for grid navigationBoundary and adversarial paths

Sources and further reading