Reduce the failure
Find the smallest input that still fails. A ten-element array can hide the moment a pointer becomes invalid; a three-element array usually cannot. Remove irrelevant values while preserving the failure.
Classify the failure before changing code: wrong answer, crash, timeout, or excessive memory. Each category points toward a different layer of the solution.
Compare states, not final answers
Write the expected state after each meaningful step, then compare it with the program's state. For a sliding window, record left, right, the window contents, and the condition that makes it valid. For dynamic programming, state what each cell means before checking its value.
The first divergence matters more than the final wrong output. Later errors are often consequences of that first broken transition.
- What was true before this iteration?
- What operation changed the state?
- What must be true afterward?
- Which exact input breaks that claim?
Audit boundaries deliberately
Many algorithm bugs live at < versus <=, inclusive versus exclusive intervals, and updates performed in the wrong order. Write the meaning of each boundary next to it: right is included, hi is excluded, or slow points to the last confirmed node.
Test empty input, one element, two elements, all equal values, no valid answer, and an answer at the first or last position whenever the problem permits them.
Separate correctness from performance
A solution can be logically correct and still fail because it performs too much work. Estimate the maximum number of important operations from the constraints. Then inspect library calls inside loops: membership in a list, front deletion, string concatenation, copying subarrays, or repeated sorting.
Fix correctness with the smallest reproducible case. Fix performance with the largest plausible shape. Mixing the two investigations creates noise.
Turn every bug into a permanent test
Once fixed, keep the minimal failing input as a regression test and write one sentence describing the incorrect assumption. This turns debugging into accumulated knowledge instead of repeated pain.
A good debugging session ends with three things: corrected code, a test that would catch the bug again, and a clearer invariant than you had before.