When is a visited-set loop check the right call over constant-space fast/slow pointers?
answer
- Both are linear in time — so what differs?
- Memory against recomputation against information
- What does the on-call engineer need to see?
- How expensive is one transition?
- Can you even write to the input?
basics
~20 sUse a visited set when the state count is small, when the step is expensive enough to want each transition computed once, or when the report must name the looping states. Use two pointers when memory is the binding constraint.
solid answer
~40 sThe set buys information and pays in memory: one pass, one evaluation of the step per state, and it hands you the first repeated state directly plus the whole path for a report — at `O(n)` space and only *expected* constant-time membership checks, which degrade under adversarial keys. Two pointers buy memory and pay in information and recomputation: `O(1)` space, roughly three step evaluations per element, and a meeting that proves only *that* a loop exists, with extra phases needed to characterise it. So the set wins when states are few, the step is costly or unrepeatable, or the answer must name the looping states; the pointers win at a billion-state space, under a per-worker memory ceiling, or on read-only input you may not annotate.
go deeper
Know the headline trade: recording visited states costs memory proportional to the states seen, while two pointers use constant memory. Both take linear time, so space is what separates them.
Explain the second axis as well — the set evaluates each transition once and keeps the path, while the pointer race recomputes and keeps nothing — and note that set membership is expected, not guaranteed, constant time.
Argue the choice from the system's actual constraints: per-process memory ceiling, cost of one transition, whether the input may be mutated, and what the diagnostic must contain. Be willing to pick the set out loud when the state space is small.
Own the maintenance side of the call. Constant-space cleverness needs a binding constraint to justify the reading cost it imposes on everyone after you, and an approximate filter's false-positive direction is a product decision, not a micro-optimisation.
## Two answers to the same question Given a deterministic walk `x, f(x), f(f(x)), ...` with no end marker, there are two standard ways to decide whether it loops. **Record what you have seen.** Keep a membership structure of visited states; before each step, check whether the current state is already in it. A hit means a loop, and the state you hit is the first one that repeats. This costs `O(n)` space in the number of distinct states reached and evaluates the step once per state. **Race two pointers.** Advance one pointer one step and another two steps per iteration until they hold the same state. This costs `O(1)` space and evaluates the step about three times per element. Both are linear in time. Choosing between them is not a complexity question at all — the asymptotics on time match — it is a question about **memory, recomputation and what you need to learn**. ## What the set gives you that pointers do not 1. **The identity of the loop.** A membership hit is the first repeated state, immediately. The pointers' meeting point is just some state inside the loop, and turning that into "here is where the tail joins" or "here are the states in the loop" requires further phases you have to write and justify. 2. **The path, for a human.** In a stuck-deployment investigation the useful artefact is "this job bounced between roll-back and re-canary eleven times". The set already holds that history; the constant-space walk has thrown it away by construction. 3. **One evaluation per transition.** If computing the next state means a remote call, a signed lookup, or a costly derivation, the set lets you pay once per state. The pointer race recomputes: about three times the step work, deliberately, because it refuses to store anything. 4. **Tolerance of a partially defined step.** Recording states as you go composes naturally with early exits, retries and terminal states; the pointer loop needs careful guards for the fast pointer at every hop. ## What pointers give you that the set does not 1. **Flat memory.** A billion reachable states, even at a handful of bytes each plus structure overhead, is many gigabytes of resident memory — for one check. Two pointers hold two states, full stop. On a fleet of workers with a fixed memory budget per process, that is the whole argument. 2. **No hashing pathology.** Membership in a hash-based set is `O(1)` *expected*, not guaranteed; keys that collide heavily — sometimes adversarially chosen, sometimes just structurally similar identifiers — degrade lookups toward linear scans of a bucket, and the check that was supposed to be cheap becomes the hot spot. The pointer race performs equality comparisons only. 3. **Read-only, unannotatable input.** A common trick for the set-free version is to mark states as visited in place. When the data is a shared, read-only snapshot — an audit manifest others are reading concurrently, a memory-mapped region, a caller's buffer you promised not to touch — marking is off the table, and the pointer race is the technique that needs neither extra memory nor mutation. 4. **Unbounded or unknown state counts.** If you cannot bound the reachable set in advance, a set-based check is a memory risk with no ceiling; you would have to cap it and give up correctness. The pointer race has no such failure mode. ## The decision, stated as a rule Ask three questions in order: | Question | If yes | If no | | --- | --- | --- | | Do I need to *name* the looping states or show the path? | Set | continue | | Is one evaluation of the step much more expensive than the equality checks? | Set (or a memoised walk) | continue | | Can the reachable state count exceed my memory budget, or is the input read-only? | Two pointers | Either; take the simpler code | That last cell is worth saying out loud in an interview: when the state space is genuinely small and bounded — a few thousand workflow stages — the set is *better engineering*. It is shorter, obviously correct on inspection, easy to extend with the diagnostic, and its memory cost is a rounding error. Reaching for the clever constant-space walk there is optimisation without a constraint, and the next person to read the code pays for it. ## A middle option worth knowing An approximate membership filter uses a small fixed amount of memory for a very large state count, at the cost of false positives. Note the direction of the error carefully: such a filter can claim a state was seen when it was not, so it can report a loop that does not exist — never miss one that does. That makes it usable as a cheap **pre-filter** that triggers an exact confirmation pass, and unusable as the final word on its own. Getting that direction backwards is a common and expensive mistake. ## The framing that impresses The strong answer does not say "two pointers, because `O(1)` space is better". It says: the two techniques cost the same asymptotic time, so the choice is memory versus recomputation versus information, and here is which of those is actually scarce in this system.
- The state space is a few thousand stages. Which do you ship, and how do you defend it?The visited set. At that scale its memory is a rounding error, it is shorter and obviously correct on inspection, and it hands the on-call engineer the actual looping states for free. The constant-space walk optimises a resource that is not scarce and costs the team reading comprehension and an extra phase to recover the same diagnostic. Cleverness needs a binding constraint to justify it.
- Can an approximate membership filter replace the exact set to save memory?Only as a pre-filter. Such a filter can report a state as seen when it was not, so it produces false loop detections while never missing a real one. That direction makes it fine for cheaply triggering an exact confirmation pass, and wrong as the final verdict — declaring a healthy job stuck is a real outage of its own. Assuming the error runs the other way is the classic mistake.
- Someone claims the set is O(1) per check so the two techniques are equivalent on time. What do you correct?Hash-based membership is `O(1)` expected and amortized, not worst-case. Heavy collisions — adversarial keys, or just structurally similar identifiers hashing poorly — push individual lookups toward scanning a bucket, and rehashing during growth adds pauses. The pointer race does equality comparisons only, with no hashing, no growth and no distribution assumption, which is a stability argument independent of the memory one.
saying these in an interview costs you the question
- Says constant space is always the better answer
- Calls hash-set membership guaranteed constant time
- Forgets the pointer race recomputes each transition about three times
- Assumes an approximate filter can miss a real repeat
- Ignores that the pointer meeting does not name the looping states