The interviewer is deciding one thing: would they merge your code without checking it? Finding your own bug, out loud, answers that better than writing perfect code by luck.
Most candidates write the code, look at it, and say “I think that works”. That sentence hands the job of checking to the interviewer, and they notice. This page is a repeatable way to check code you cannot run, and a quick way to test it in C# when you can.
Four rules make this reliable rather than theatre.
A trace for a two-pointer scan looks like this. Each line records every variable that changes.
left = 0, right = 4, state = 0, best = 0.left = 0, right = 3, state = 7, best = 7. Too big, so right--.left = 1, right = 3, state = 5, best = 7. Too small, so left++.After the dry run, check these five things on purpose. They account for most interview bugs.
best seeded at 0 when it should be int.MinValue or the first element?nums[i - 1] at i = 0? In C# that throws IndexOutOfRangeException.nums[i + 1] at the final index?Do not brainstorm from scratch under pressure. Run the list for the type you were handed.
null, if the signature allows it. One element. Two elements. All the same. Already sorted. Reverse sorted. All negative. Contains zero. Contains int.MaxValue or int.MinValue.null head. One node. Two nodes. The target is the head. The target is the tail. A cycle, if cycles are possible.null root. One node. A left-only chain, which is the stack-depth case. A perfect tree. Duplicate values.k = 0. k = 1. k = n. k > n. A target larger than the total.Six questions catch nearly all of them.
for (i = 0; i < n; i++) stops before n. The range a[i..j] excludes j. Say which you meant.[left, right]? right - left + 1. The + 1 is missed constantly.< or <=? < when the two indices must differ. <= when a single element is a valid range.n or n + 1? Prefix arrays and 1-indexed nodes both need the extra slot.mid round down or up? Down, because int division truncates. So left = mid can loop forever. See Pattern 11.i versus i + 1 in backtracking, and prefix versus suffix in DP.Here is a real bug in a real solution, and how a dry run exposes it. The problem is Longest Substring Without Repeating Characters.
public static class LongestUniqueBuggy
{
/// <summary>Has a bug: left can
/// move backwards.</summary>
/// <param name="s">The text.</param>
/// <returns>A window length.</returns>
/// <example>Length("abba") returns 3,
/// but the answer is 2.</example>
public static int Length(string s)
{
var lastSeen = new Dictionary<char, int>();
int left = 0, best = 0; // empty window
for (int right = 0; right < s.Length;
right++)
{
char ch = s[right];
// BUG: no check that the old
// sighting is still in the window.
if (lastSeen.TryGetValue(ch,
out int prev))
{
left = prev + 1; // + 1: past it
}
lastSeen[ch] = right;
// + 1: [left, right] is inclusive
best = Math.Max(best,
right - left + 1);
}
return best;
}
}
public static class LongestUnique
{
/// <summary>Longest run with no repeated
/// character. O(n).</summary>
/// <param name="s">The text.</param>
/// <returns>The longest window length.</returns>
/// <example>Length("abba") returns 2.
/// </example>
public static int Length(string s)
{
var lastSeen = new Dictionary<char, int>();
int left = 0, best = 0; // empty window
for (int right = 0; right < s.Length;
right++)
{
char ch = s[right];
// Only a repeat INSIDE the window
// can push the left edge in.
if (lastSeen.TryGetValue(ch,
out int prev) && prev >= left)
{
left = prev + 1; // + 1: past it
}
lastSeen[ch] = right;
// + 1: [left, right] is inclusive
best = Math.Max(best,
right - left + 1);
}
return best;
}
}
Both pass "abcabcbb", "bbbbb" and "pwwkew", the three examples the problem gives. The bug needs a character that repeats after it has fallen out of the window. The shortest such input is "abba". Trace it:
a. Both versions keep left = 0.b. left = 0, and best becomes 2.lastSeen['b'] = 1. A real repeat, so both move left to 2.lastSeen['a'] = 0, which is behind left. The buggy version sets left = 1, moving it backwards. The fixed version keeps left = 2.At the last step the buggy version computes a width of 3 - 1 + 1 = 3 and returns 3. The correct answer is 2.
left, never moves backwards. left = Math.Max(left, prev + 1) is a defensive way to write it. Third, the shortest breaking input had four characters. Small, hostile inputs beat large random ones.Many interviews now use an editor you can run. If so, spend two minutes on a test method. It is faster than a dry run and far more convincing.
using System.Diagnostics;
public static class LongestUniqueChecks
{
/// <summary>Table-driven tests: the given examples first, then the edges.</summary>
/// <param name="solve">The solution under test.</param>
/// <returns>One message per failing case. Empty means every case passed.</returns>
/// <example>LongestUniqueChecks.Run(LongestUnique.Length) returns [].</example>
public static List<string> Run(Func<string, int> solve)
{
(string Text, int Want)[] cases =
[
("abcabcbb", 3), // from the problem statement
("bbbbb", 1), // from the problem statement
("pwwkew", 3), // from the problem statement
("", 0), // empty: no window at all
("a", 1), // single character
("abba", 2), // the repeat that already left the window
("tmmzuxt", 5), // the same trap, one step longer
];
var failures = new List<string>();
foreach (var (text, want) in cases)
{
int got = solve(text);
// Debug.Assert stops a Debug build here. Release builds remove the call.
Debug.Assert(got == want, $"\"{text}\": got {got}, want {want}");
if (got != want)
{
failures.Add($"\"{text}\": got {got}, want {want}");
}
}
return failures;
}
}
Debug.Assert vanishes in Release builds. It carries [Conditional("DEBUG")], so the compiler deletes the whole call, including its arguments. If the online editor builds in Release, your asserts silently do nothing. That is why the method above also collects failures by hand. Trace.Assert stays in every build. Throwing an exception, or printing a list of failures, works everywhere.Habits that make this worth the two minutes:
LowerBound is much easier to see in isolation.If you have time, compare against the slow version you described in phase 3 of the script. Small random inputs find bugs no hand-picked case will. Fix the seed so a failure can be repeated.
public static class CrossCheck
{
/// <summary>The obvious O(n^3) answer: try every window, test it for repeats.</summary>
/// <param name="s">The text.</param>
/// <returns>The longest window with no repeated character.</returns>
/// <example>CrossCheck.Brute("abba") returns 2.</example>
public static int Brute(string s)
{
int best = 0; // 0 = the empty window always qualifies
for (int i = 0; i < s.Length; i++) // i is the window start
{
for (int j = i; j < s.Length; j++) // j is the window end, inclusive
{
// A window is valid when its distinct count equals its length.
// + 1 because [i, j] is inclusive.
int length = j - i + 1;
if (s.Substring(i, length).Distinct().Count() == length)
{
best = Math.Max(best, length);
}
}
}
return best;
}
/// <summary>Compares a solution with Brute on random small strings.</summary>
/// <param name="solve">The solution under test.</param>
/// <param name="seed">The random seed, so a failure can be repeated.</param>
/// <param name="trials">How many random strings to try.</param>
/// <returns>The first string where they disagree, or null if none.</returns>
/// <example>CrossCheck.FirstMismatch(LongestUnique.Length, 1, 500) returns null.</example>
public static string? FirstMismatch(Func<string, int> solve, int seed, int trials)
{
var rng = new Random(seed);
for (int t = 0; t < trials; t++) // t counts the trials run so far
{
// Lengths 0..7 and a 3-letter alphabet force many repeats.
int length = rng.Next(0, 8); // 8 is exclusive, so at most 7
var chars = new char[length];
for (int i = 0; i < length; i++)
{
chars[i] = "abc"[rng.Next(3)]; // 3 = the alphabet size
}
string text = new string(chars);
if (solve(text) != Brute(text))
{
return text;
}
}
return null;
}
}
FirstMismatch finds a failing string within a few hundred trials.You will not set up a test project in an interview. But a take-home, or a question like “how would you test this in production?”, wants the real shape. In .NET that is usually xUnit. Create a project with dotnet new xunit, reference your code, and run dotnet test. The block below needs the xUnit package, so it does not build in a plain console app.
using Xunit;
public class LongestUniqueTests
{
// [Theory] runs the method once per [InlineData] row.
[Theory]
[InlineData("abcabcbb", 3)] // from the problem statement
[InlineData("", 0)] // empty
[InlineData("a", 1)] // single character
[InlineData("abba", 2)] // the repeat that left the window
public void ReturnsLongestWindow(string text, int want)
{
Assert.Equal(want, LongestUnique.Length(text));
}
// [Fact] is a single test with no parameters.
[Fact]
public void AgreesWithBruteForce()
{
Assert.Null(CrossCheck.FirstMismatch(LongestUnique.Length, seed: 1, trials: 500));
}
}
[Fact] is one test. [Theory] plus [InlineData] is the table-driven form.Assert.Equal(expected, actual) puts the expected value first. Swapping them makes failure messages read backwards.[Test] and [TestCase], or [TestMethod] and [DataRow].Last step. Stop being the author and become the reviewer. Ask these seven questions about your own code.
nums[0], Max() and First() that throw on empty input.Dictionary.Add throwing on a repeat key, and rotated binary search.int.MaxValue? Catches silent overflow. C# does not warn you.Array.Sort(nums), in-place swaps, grid marking. Say it, or copy first.List.Contains, RemoveAt(0), ranges, LINQ, string +=.Debug.Assert disappears in Release. Collect failures or throw, so your tests really run.