Skip to content

Cycle Detection: Code Templates in 6 Languages

If you have not read the concept guide yet, start there for the intuition and complexity analysis. This page gives you the code you can memorize and adapt during an interview.

Main Template: Floyd’s Cycle Detection (Linked List)

The tortoise and hare algorithm is the standard for detecting cycles in linked lists. It uses

O(1)
space by keeping only two pointers, regardless of the list length.

Use this for Linked List Cycle and Linked List Cycle II .

function hasCycle(head) {
    // Null or single-node lists cannot have a cycle
    if (!head || !head.next) return false;

    let slow = head;
    let fast = head;

    // The fast pointer moves twice as fast. If a cycle exists,
    // fast will lap slow before it ever reaches null.
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;

        if (slow === fast) return true;
    }

    return false;
}

function detectCycle(head) {
    if (!head || !head.next) return null;

    let slow = head;
    let fast = head;

    // Phase 1: detect the cycle
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow === fast) break;
    }

    if (!fast || !fast.next) return null;

    // Phase 2: find where the cycle begins.
    // The distance from head to cycle entry equals the distance
    // from the meeting point to cycle entry.
    slow = head;
    while (slow !== fast) {
        slow = slow.next;
        fast = fast.next;
    }

    return slow;
}
Templates only help if you can recall them without staring at the screen. Turn these snippets into flashcards for daily mobile review.

Code Breakdown

Key Variables

  • slow: moves one step per iteration. In the detection phase it probes each node in order. In the start-finding phase it resets to head.
  • fast: moves two steps per iteration. It is the probe that either reaches the end (no cycle) or laps the slow pointer (cycle found).
  • head: the starting reference. Resetting slow to head after detection is the key to finding the cycle entry.

Visual Mechanism

    stateDiagram-v2
    [*] --> Init: slow=head, fast=head
    Init --> Probe: fast != null && fast.next != null
    Probe --> Advance: slow=slow.next, fast=fast.next.next
    Advance --> Check: slow == fast?
    Check --> Found: Yes -> Cycle detected
    Check --> Probe: No -> Continue
    Probe --> NoCycle: fast or fast.next == null
    Found --> FindStart: slow=head
    FindStart --> Locate: slow != fast
    Locate --> AdvanceBoth: slow=slow.next, fast=fast.next
    AdvanceBoth --> Locate: Still not equal
    Locate --> Entry: slow == fast -> Cycle start
    NoCycle --> [*]
    Entry --> [*]
  

Critical Sections

The null check at the start handles empty and single-node lists. Without it, accessing head.next on a null reference throws an error.

The while loop condition fast && fast.next ensures the fast pointer never accesses null. If fast can make two moves, the list is long enough to check one more step.

Phase 2 relies on a mathematical property: the distance from the head to the cycle start equals the distance from the meeting point to the cycle start. Resetting one pointer to head and moving both one step at a time guarantees they converge at the entry node.

2. DFS Cycle Detection (Directed and Undirected)

For graph cycle detection, DFS with state tracking is the standard approach.

  • Directed graphs: track each node as unvisited, visiting (in the current recursion stack), or visited (fully processed). A back edge to a visiting node means a cycle.
  • Undirected graphs: track visited nodes and the parent of each node. A back edge to a visited node that is not the parent means a cycle.

Use this for Course Schedule and general graph cycle detection.

// Directed Graph
function hasCycleDirected(graph) {
    const visiting = new Set();
    const visited = new Set();

    function dfs(node) {
        if (visiting.has(node)) return true;  // back edge to active stack
        if (visited.has(node)) return false;  // already fully processed

        visiting.add(node);
        for (const neighbor of graph[node]) {
            if (dfs(neighbor)) return true;
        }
        visiting.delete(node);
        visited.add(node);
        return false;
    }

    for (let i = 0; i < graph.length; i++) {
        if (dfs(i)) return true;
    }
    return false;
}

// Undirected Graph
function hasCycleUndirected(graph) {
    const visited = new Set();

    function dfs(node, parent) {
        visited.add(node);
        for (const neighbor of graph[node]) {
            if (neighbor === parent) continue;  // skip the edge we came from
            if (visited.has(neighbor)) return true;  // back edge = cycle
            if (dfs(neighbor, node)) return true;
        }
        return false;
    }

    for (let i = 0; i < graph.length; i++) {
        if (!visited.has(i) && dfs(i, -1)) return true;
    }
    return false;
}

3. Union-Find Cycle Detection (Undirected Graphs)

For undirected graphs, Union-Find detects cycles without a DFS traversal. If two nodes of an edge already belong to the same set, adding that edge creates a cycle.

Use this for Graph Valid Tree .

function hasCycleUnionFind(edges, n) {
    const parent = Array.from({length: n}, (_, i) => i);

    function find(x) {
        // Path compression flattens the tree so future
        // lookups are nearly constant time
        if (parent[x] !== x) parent[x] = find(parent[x]);
        return parent[x];
    }

    for (const [u, v] of edges) {
        const rootU = find(u);
        const rootV = find(v);
        if (rootU === rootV) return true; // already connected = cycle
        parent[rootU] = rootV;
    }
    return false;
}

Variations

1. Topological Sort with Kahn’s Algorithm (Directed Graph)

Instead of DFS, Kahn’s algorithm uses indegrees to detect cycles. Nodes with indegree zero have no dependencies and can be processed first. If the graph has a cycle, some nodes will never reach indegree zero.

from collections import deque

def has_cycle_kahn(num_nodes, edges):
    graph = [[] for _ in range(num_nodes)]
    indegree = [0] * num_nodes

    for u, v in edges:
        graph[u].append(v)
        indegree[v] += 1

    queue = deque([i for i in range(num_nodes) if indegree[i] == 0])
    processed = 0

    while queue:
        node = queue.popleft()
        processed += 1
        for neighbor in graph[node]:
            indegree[neighbor] -= 1
            if indegree[neighbor] == 0:
                queue.append(neighbor)

    # If we could not process all nodes, a cycle exists
    return processed != num_nodes

2. Collecting Cycle Nodes

If you need the actual nodes in the cycle (not just a boolean), collect them after detection.

// After slow === fast in Floyd's algorithm
if (slow === fast) {
    const cycle = [];
    let temp = slow;
    do {
        cycle.push(temp.val);
        temp = temp.next;
    } while (temp !== slow);
    return cycle;
}

3. Happy Number (Floyd’s on Digits)

Floyd’s algorithm works on sequences where each element is derived from the previous one by a deterministic function, even without explicit linked-list nodes.

def is_happy(n: int) -> bool:
    def next_num(x):
        return sum(int(d) ** 2 for d in str(x))

    slow = n
    fast = next_num(n)

    while fast != 1 and slow != fast:
        slow = next_num(slow)
        fast = next_num(next_num(fast))

    return fast == 1
Each variation above is a separate review card in the app. Drill them individually so you do not mix up the conditions under pressure.

Now head to the practice problems to apply these templates to real interview questions.

Now that you have the templates, the next step is application. Head to the practice problems or start reviewing these templates with spaced repetition .