Unit 1 · Algorithms · Lesson 22 of 24
Correctness, invariants, edge cases and testing
Builds on
Step 1: Learn the ideas
- Specifications: inputs, outputs and awkward cases
- Loop invariants: initialization, maintenance, termination
- Induction for recursive code
- Termination measures
- Boundary and degenerate cases
- Input-space partitioning
- Hand simulation with a variable trace
- Brute-force cross-checks on random inputs
- Property-based testing and shrinking
- Hypothesis-driven debugging
Why it matters
A solution with an untested boundary bug can leave a weaker impression than a slightly slower one that the candidate tested and justified. Interviewers look for whether you verify your own work: do you know why the code is right, can you find its edge cases before they do, and can you debug calmly when a test fails? The same habits make production code trustworthy: invariants become assertions, edge-case lists become test suites, and brute-force references become oracles for randomized and property-based tests.
Core ideas
Specification first. Before coding, write down the input domain and what the output must satisfy, including awkward cases: empty input, duplicates, negative numbers, ties, no valid answer. Many bugs are really disagreements about the specification.
Loop invariants. An invariant is a statement that is true before every iteration. Three facts make it useful: it holds initially (initialization), one iteration preserves it (maintenance), and when the loop stops it implies the result (termination). For lower-bound binary search on a half-open range [lo, hi): every index below lo holds a value smaller than the target, and every index at or above hi holds a value at least the target. It holds for lo = 0, hi = n; each branch preserves it; when lo == hi, the answer is lo. That sentence settles the usual questions (lo < hi or lo <= hi, hi = mid or mid - 1) without guessing. Also name a termination measure, something that strictly decreases, such as hi - lo.
Induction for recursion. Assume the recursive calls are correct on smaller inputs and show the current call combines them correctly; separately check the base cases and that every call makes progress toward one. It is the same argument as an invariant, organized differently.
Boundary cases by partition. Split the input space into classes, test one value per class plus its boundaries, and choose cases that exercise every branch. Arrays: sizes 0, 1 and 2, all equal, sorted, reverse sorted. Numbers: zero, negatives, and the extremes of the type. Graphs: disconnected, self-loop, single vertex. Strings: empty, one character, all identical.
Hand simulation. Trace a small input with a table of variables per iteration, reading the code you actually wrote rather than the code you meant. Pick an input that finishes in a minute but is not trivial, such as five elements with a duplicate.
Brute-force cross-check. Write the obviously correct slow version (try every pair, subset or split), generate many small random inputs and compare. When they disagree, shrink the input by removing elements while the failure persists; small counterexamples explain bugs quickly.
import random
def cross_check(fast, slow, trials=2000):
for _ in range(trials):
xs = [random.randint(-3, 3) for _ in range(random.randint(0, 7))]
if fast(list(xs)) != slow(list(xs)):
return xs # a failing input; shrink it next
return None
Property-based thinking. Check properties that must hold for every input rather than only specific outputs: a sort's output is ordered and is a permutation of its input; an encode-decode round trip returns the original; a reported shortest path is no longer than any path you construct. Libraries such as Hypothesis generate inputs and shrink failures automatically.
Debugging. Reproduce the failure with the smallest input, form a hypothesis about which invariant breaks, and check exactly that point with an assertion or a print. Random edits waste time and often add a second bug.
Common misconceptions
- "Passing the given examples means it is correct." Examples rarely cover empty inputs, duplicates or extremes; derive your own cases from the input classes.
- "Invariants are only for formal proofs." One sentence about what your variables mean is what lets you choose loop bounds and conditions without trial and error.
- "Random testing proves correctness." It finds bugs but cannot show their absence; pair it with an argument.
- "Debugging means adding prints everywhere." Minimize the input first, then check one hypothesis at a time.
- "Edge cases belong at the end of the interview." Asking about them up front shapes the design and avoids rewrites.
How to approach it
In an interview, clarify edge cases while restating the problem, state the invariant as you write each loop, and after coding trace one normal case and one boundary case aloud before saying you are done. If something fails, say what you suspect and check that. In practice, keep a log of every bug with its class (off-by-one, wrong initialization, missed empty case, overflow) and review it weekly so that recurring mistakes become visible and can be targeted. Self-testing is expected at every level; mid and senior candidates are also expected to propose a test plan and reason about failure modes beyond the happy path.
Suggested order
- 1Read MIT 6.006 Lecture 1 (Introduction) and Recitation 1 for how the course argues correctness by induction; restate the birthday-matching proof in your own words.
- 2Read MIT 6.102 Reading 2 (Testing), especially partitioning the input space and including boundaries, then Reading 13 ((Avoiding) Debugging) on reproducing bugs and the scientific method.
- 3Write lower-bound binary search with its half-open invariant as a comment, and test it on every partition class: empty, one element, all equal, and targets below, inside and above the range.
- 4Solve Binary Search and Find First and Last Position of Element in Sorted Array, writing the invariant before each loop.
- 5Solve Merge Intervals and Insert Interval, then cross-check both against brute-force versions on random small inputs.
- 6Solve Sort Colors (state the three-region invariant), First Missing Positive and Reverse Nodes in k-Group, keeping a bug log with the cause of every failed attempt.
- 7Optionally rewrite one cross-check as a property-based test with Hypothesis.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
MIT 6.006 Introduction to Algorithms (Spring 2020)Start here
MIT OpenCourseWareSection: Lecture 1: Introduction
What it means for an algorithm to be correct and why correctness proofs over inputs of arbitrary size rely on induction; Recitation 1 repeats the argument with more detail.
Open(opens in a new tab)Link responded, 5 Oct 2026MIT 6.102 Software Construction (readings)Start here
MIT EECSSection: Reading 2: Testing
Choosing test cases by partitioning the input space, including boundaries, and documenting a testing strategy; Reading 13 covers systematic debugging.
Open(opens in a new tab)Link responded, 5 Oct 2026Microsoft hiring tips: technical interviewing
Microsoft CareersOfficial guidance noting that interviewers are likely to expect you to test your own solution and point out corner cases before saying you are done.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
These problems are chosen for their boundaries, not their algorithms. For each one, write the invariant as a comment before the loop, list at least five test inputs from different classes before running anything, hand-trace one of them, and then run a brute-force cross-check. Record every bug with its category.
- Binary SearchLeetCode(opens in a new tab)Not attemptedLeetCode
- Find First and Last Position of Element in Sorted ArrayLeetCode(opens in a new tab)Not attemptedLeetCode
- Merge IntervalsLeetCode(opens in a new tab)Not attemptedLeetCode
- Insert IntervalLeetCode(opens in a new tab)Not attemptedLeetCode
- Sort ColorsLeetCode(opens in a new tab)Not attemptedLeetCode
- First Missing PositiveLeetCode(opens in a new tab)Not attemptedLeetCode
- Reverse Nodes in k-GroupLeetCode(opens in a new tab)Not attemptedLeetCode
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1What three facts must you show for a loop invariant to justify a loop?
- Question 2What does lo mean at every iteration of your binary search?
- Question 3Which input classes would you test for a function over an array of integers?
- Question 4How do you shrink a failing random test to a minimal counterexample?
- Question 5Which properties of a sorting function can you test without knowing the expected output?
- Question 6Why do you need both a correctness argument and tests?
You’re done when you can
- State the invariant of a loop you wrote, with initialization, maintenance and termination, in under two minutes.
- List at least six meaningful edge cases for an unseen problem within three minutes of reading it.
- Build a brute-force cross-check for one practice problem and use it to find or rule out a bug.
- Keep a bug log for two weeks of practice and name your two most frequent bug categories.
Finish the lesson
Unlocks next: Greedy algorithms and exchange arguments, Explaining solutions aloud


