Chapter 6
15 min read
Section 33 of 261

Circular Linked Lists

Linked Lists

Definition and Invariant

A circular linked list is a linked list in which the last node's next field, instead of holding the null sentinel, points back to a designated start node. The chain becomes a ring.

Formally, for a circular list of size nn with nodes v0,v1,,vn1v_0, v_1, \dots, v_{n-1}:

vi.next=v(i+1)modn,0i<n.v_i.\text{next} = v_{(i+1) \bmod n}, \quad 0 \le i < n.

That single change of the modular index is the entire definition. The structural invariant is that starting from any node and following nextn\text{next}^n times returns you to the same node. There is no terminator—no None, no NULL—and asking “is this the last node?” only makes sense relative to a chosen start.

  • Singly-circular: one next pointer per node, ring oriented in one direction.
  • Doubly-circular: every node holds prev and next, with tail.next=head\text{tail.next} = \text{head} and head.prev=tail\text{head.prev} = \text{tail}. The Linux kernel's struct list_head is the canonical example.

Why a Cycle on Purpose?

A circular list is what you reach for when the problem is itself cyclic: there is no “first” or “last,” only a current cursor that keeps walking around forever. The pattern shows up in:

DomainUseWhy circular
Operating systemsRound-robin process schedulerEach process gets a quantum, then control passes to next; no end-of-list
NetworkingToken Ring, fair-queue schedulersToken passes around forever; no node is privileged
MultimediaAudio / video ring buffers (JACK, ALSA)Producer and consumer cursors lap each other endlessly
Games / UITurn order, image carousels, repeat-all playlistsLast player loops back to first; UX is naturally circular
CryptographyEnigma rotors (literally circular wheels of substitutions)Each keypress advances the rotor by one position, mod 26
ConcurrencyLock-free MPMC queues built on circular buffersIndex arithmetic mod N; producer/consumer never collide
CachingDNS / CDN caches with TTL-driven rotationEviction sweep walks the ring; new entries replace the oldest

In every case the key affordance is “given any node, advance once and you have the next thing to do,” with no special case at the boundary. The cycle removes a special case rather than adding one.


The Tail-Pointer-as-Handle Idiom

In a non-circular list you usually keep a head pointer and, if you care about O(1)O(1) append, also a tail pointer. In a circular list, holding only the tail is strictly better:

  1. Head is one hop away. By the cyclic invariant, tail.next is the head. So O(1)O(1) head access is free.
  2. Append is O(1). The tail is in your hand; just splice a new node between tail and tail.next, then move tail.
  3. Prepend is O(1). Same splice, but don't move the tail—the new node simply becomes the new head.
  4. Delete head is O(1). Rewrite tail.next to skip the old head.
  5. One pointer of state, two endpoints accessible. Half the bookkeeping, no consistency bugs between separate head/tail pointers.
Whenever you see a textbook implementation of a circular list expose a head pointer, treat it as a smell. The library implementations that survive (kernel list_head, ring buffers in real-time audio, fair queue schedulers) all pivot on a single tail-or-cursor pointer.

Traversal Without a None Terminator

The first time you write a loop over a circular list, your reflex—while p is not None—is now a bug. There is no None; the loop runs forever. The canonical idiom is to remember the start node and break when you wrap:

🐍python
1# WRONG: infinite loop in a circular list.
2p = head
3while p is not None:        # condition never becomes True
4    visit(p.value)
5    p = p.next
6
7# RIGHT: stop when we come full circle.
8start = head
9p = start
10while True:
11    visit(p.value)
12    p = p.next
13    if p is start:
14        break               # we've visited every node exactly once

Two subtleties:

  • Use identity (p is start), not equality (p == start). Two different nodes can carry the same value; you want the literal same object you started from.
  • The do-while shape (visit then check) is correct precisely because visiting first guarantees the start node is processed exactly once. Reversing the order skips the start.

Bounded variant when the size is known:

🐍python
1p = head
2for _ in range(n):           # exactly n hops, no identity check needed
3    visit(p.value)
4    p = p.next

This shape is preferable inside hot loops where the identity check would prevent loop-unrolling, and is the standard idiom inside ring-buffer code where nn is a power of two and the compiler can replace the increment with a bitwise & (mask).


Core Operations and Their Cost

OperationSingly-circular (tail handle)Doubly-circularNotes
head() / tail()O(1)O(1)tail.next is head; tail is tail
prepend(v)O(1)O(1)splice between tail and head
append(v)O(1)O(1)prepend, then move tail
delete headO(1)O(1)rewrite tail.next
delete tailO(n)O(1)singly: must walk to find predecessor
delete given nodeO(1) if you have prevO(1)doubly: cur.prev and cur.next are at hand
traverseO(n)O(n)stop when p is start, OR loop n times
rotate by kO(k)O(k mod n)tail = tail.next, k times
split in halfO(n)O(n)fast/slow pointers; sever and re-tie
concat L1 ++ L2O(1)O(1)rewire two pointers; both rings become one

Concatenation deserves a moment because it's the place circular lists shine over arrays and over non-circular linked lists alike:

🐍python
1# Splice list B onto the end of list A — O(1), no walking required.
2def splice(A, B):
3    if A.tail is None: return B
4    if B.tail is None: return A
5    A_head = A.tail.next
6    B_head = B.tail.next
7    A.tail.next = B_head        # A's tail now leads into B's head
8    B.tail.next = A_head        # B's tail now closes the merged ring
9    A.tail = B.tail             # the tail of the merged ring is B's tail
10    A.size += B.size
11    return A

Two pointer rewrites, no copying, no walking either ring. With arrays this would be O(A+B)O(|A| + |B|). With non-circular linked lists you'd also need to walk to A's tail unless you maintained one separately. Circular lists make “sew two rings into one” feel almost trivial.


Python Implementation

The class below maintains the singly-circular invariant, exposes append / prepend / delete-head / iterate, and includes a Josephus simulator that we'll dissect in the next section.

CircularLinkedList — singly-circular, tail-handle, with Josephus
🐍python
1Slim Node class

__slots__ tells CPython not to allocate a per-instance __dict__, dropping a Node from ~56 to ~32 bytes. Useful when the ring has millions of nodes (e.g., a session table).

8Why tail, not head?

Holding the tail is the cleanest single-handle representation: tail.next is the head (O(1) to read), and tail itself is also O(1). With only a head pointer, append would cost O(n).

11Empty-list sentinel

self.tail = None is the agreed-upon spelling of 'empty ring'. We deliberately do NOT use a dummy node here to keep the contrast with non-circular lists clean — but kernels often add one (see the doubly-circular section).

18head() helper, O(1)

By definition, the node that comes right after the tail IS the head. Every operation that needs the head reads it through tail.next — never stored separately, so the two can never go out of sync.

21append: the empty-ring branch

When the ring is empty, the new node must point to itself so that the cyclic invariant 'tail.next is head' still holds. Forgetting this self-loop is the #1 bug in circular-list code.

EXAMPLE
before: tail = None
after:  tail -> [v] -> (back to itself)
25append: the general case (3 wires)

Wire 1: new node's next aims at the current head (so the new node is the last 'real' element, but still loops to head). Wire 2: old tail forwards to the new node. Wire 3: bump the tail pointer. ORDER MATTERS — overwrite tail.next AFTER reading it, never before.

LOOP TRACE · 4 iterations
step 0
tail = [T]
head = [H]
after wire 1
node.next = -> H
after wire 2
tail.next = -> node
after wire 3
tail = -> node
32prepend reuses the same wires

Notice prepend differs from append by ONE line: we don't update self.tail. The new node becomes the head because tail.next now points at it, but the old tail is still the tail. This is the pay-off of the tail-handle representation.

41delete_head: special-case the singleton

If the only node IS the tail, removing it must also reset tail to None. Skip this check and you end up with a dangling tail pointing at a freed node — exactly the kind of bug that doesn't show up in unit tests but breaks production.

47delete_head: the bypass

Standard linked-list deletion: predecessor (here, the tail) skips over the doomed node by adopting the doomed node's successor. The doomed node becomes unreachable and the GC reclaims it.

53__iter__: the canonical traversal idiom

Cannot use 'while p is not None' — there IS no None. The trick is to remember the start node and stop when we wrap around to it. 'p is start' uses identity, not equality, which is correct: we want the same OBJECT, not a node with the same value.

56yield then advance

Yield the value FIRST so we visit every node exactly once including the start. If we advanced before yielding, we'd skip the start on the first pass.

LOOP TRACE · 4 iterations
iter 1
p = head
iter 2
p = head.next
iter n
p = tail
after
p = head (== start) -> break
67Josephus: prev as the working cursor

We track 'prev' rather than 'cur' because deletion needs the predecessor: prev.next = doomed.next is O(1), but finding the predecessor of an arbitrary cur in a singly-linked ring would cost O(n). Choosing the right cursor is the difference between O(n*k) and O(n^2*k).

69Termination: ring of one

prev.next is prev means the ring has shrunk to a single self-loop — the survivor. Identity comparison again, since two different one-element rings would still satisfy '==' if their values matched.

71Advance k-1 hops

Counting includes the starting node: position 1 is where we already are, so we hop k-1 more times to land prev on the predecessor of the k-th. Off-by-one warning: changing this to range(k) yields a different (shifted) survivor.

EXAMPLE
k=3, ring [1,2,3,4,5]:  prev=5 -> hops to 1 -> hops to 2  ⇒ doomed = prev.next = 3
75Keep the tail invariant alive

If we just unlinked the tail itself, prev is now the new tail. Skip this and the next append will splice into the middle of the ring — silent corruption. THE most common Josephus bug in production code.

62 lines without explanation
1class Node:
2    __slots__ = ("value", "next")
3    def __init__(self, value, next=None):
4        self.value = value
5        self.next  = next
6
7class CircularLinkedList:
8    """Singly-circular list with a tail pointer.
9    Invariant: if size > 0, tail.next is the head."""
10    def __init__(self):
11        self.tail = None       # tail is None  <=>  list is empty
12        self.size = 0
13
14    def __len__(self):
15        return self.size
16
17    def head(self):
18        return self.tail.next if self.tail else None
19
20    def append(self, value):
21        node = Node(value)
22        if self.tail is None:
23            node.next = node                 # ring of one: points at itself
24            self.tail = node
25        else:
26            node.next = self.tail.next       # new node points to old head
27            self.tail.next = node            # old tail points to new node
28            self.tail = node                 # new node is now the tail
29        self.size += 1
30
31    def prepend(self, value):
32        node = Node(value)
33        if self.tail is None:
34            node.next = node
35            self.tail = node
36        else:
37            node.next = self.tail.next       # new node aims at the old head
38            self.tail.next = node            # tail now points at the new head
39        self.size += 1
40
41    def delete_head(self):
42        if self.tail is None:
43            raise IndexError("delete from empty list")
44        head = self.tail.next
45        if head is self.tail:                # the ring had exactly one node
46            self.tail = None
47        else:
48            self.tail.next = head.next       # bypass the old head
49        self.size -= 1
50        return head.value
51
52    def __iter__(self):
53        if self.tail is None:
54            return
55        start = self.tail.next               # remember the head
56        p = start
57        while True:
58            yield p.value
59            p = p.next
60            if p is start:                   # we've come full circle
61                break
62
63    def josephus(self, k):
64        """Eliminate every k-th node starting from the head; return the survivor's value."""
65        if self.tail is None or k < 1:
66            raise ValueError("non-empty list and k >= 1 required")
67        # Walk a 'prev' cursor: deleting a node is O(1) once prev is known.
68        prev = self.tail
69        while prev.next is not prev:         # more than one node remains
70            for _ in range(k - 1):           # advance k-1 hops to land on the doomed
71                prev = prev.next
72            doomed = prev.next
73            prev.next = doomed.next          # unlink the doomed node
74            if doomed is self.tail:          # keep tail invariant valid
75                self.tail = prev
76            self.size -= 1
77        return prev.value
The class deliberately stores only tail and size. Resist the urge to add a separate head pointer—the first time you forget to update both in lockstep, you'll spend an afternoon hunting a phantom dangling reference. Read head through tail.next every time.

C Implementation

The same idea in C, with explicit malloc / free. Notice how destruction has its own twist: you must break the cycle before walking, or the teardown loop never terminates.

CList — circular linked list in C
📄c
9CList struct holds only the tail

8 bytes for the pointer plus 4 bytes for size plus 4 bytes of padding — 16 bytes total. The CList itself can live on the stack; only the Node objects need malloc.

17The self-loop in append

n->next = n is the C version of the empty-ring base case. Forget this and the very next traversal in clist_destroy walks off the end into uninitialized memory — segfault waiting to happen.

21Three wires, exactly as in Python

Same algorithm, but every -> dereference is explicit. Notice we read L->tail->next BEFORE overwriting it — the canonical 'wire the newcomer first' rule.

36free() inside the Josephus loop

C demands manual reclamation. Each elimination is exactly one free. If we forgot it, every Josephus run would leak n-1 nodes. Tools like Valgrind catch this; production telemetry usually doesn't until the process OOMs.

47destroy: BREAK THE CYCLE before walking

If you write 'while (p != tail->next) ...' you go round forever; if you write 'while (p != NULL) ...' you must FIRST sever the ring. Setting tail->next = NULL converts it back into a normal NULL-terminated list, then the standard teardown loop applies.

EXAMPLE
Before: head -> n1 -> n2 -> ... -> tail -> head
After cut: head -> n1 -> n2 -> ... -> tail -> NULL
50Save next BEFORE freeing

Same dangling-pointer dance from section 6.1: read p->next into a temporary, then free p. Reverse the order and you read freed memory — undefined behavior.

52 lines without explanation
1#include <stdio.h>
2#include <stdlib.h>
3
4typedef struct Node {
5    int value;
6    struct Node* next;
7} Node;
8
9typedef struct {
10    Node* tail;     /* tail->next is the head; NULL means empty */
11    int   size;
12} CList;
13
14void clist_init(CList* L) { L->tail = NULL; L->size = 0; }
15
16void clist_append(CList* L, int v) {
17    Node* n = (Node*) malloc(sizeof(Node));
18    n->value = v;
19    if (L->tail == NULL) {
20        n->next = n;            /* the ring of one points at itself */
21        L->tail = n;
22    } else {
23        n->next     = L->tail->next;   /* new node aims at old head */
24        L->tail->next = n;             /* old tail forwards to new   */
25        L->tail       = n;             /* new node IS the tail        */
26    }
27    L->size++;
28}
29
30int clist_josephus(CList* L, int k) {
31    Node* prev = L->tail;
32    while (prev->next != prev) {
33        for (int i = 0; i < k - 1; i++) prev = prev->next;
34        Node* doomed = prev->next;
35        prev->next   = doomed->next;
36        if (doomed == L->tail) L->tail = prev;
37        free(doomed);                   /* manual reclamation */
38        L->size--;
39    }
40    int survivor = prev->value;
41    free(prev);
42    L->tail = NULL;
43    L->size = 0;
44    return survivor;
45}
46
47void clist_destroy(CList* L) {
48    if (L->tail == NULL) return;
49    Node* p = L->tail->next;            /* start at the head */
50    L->tail->next = NULL;               /* BREAK THE CYCLE first! */
51    while (p != NULL) {
52        Node* doomed = p;
53        p = p->next;
54        free(doomed);
55    }
56    L->tail = NULL;
57    L->size = 0;
58}
Reference-counting GCs hate cycles. CPython's primary collector is reference counting (with a backup cycle collector); a circular list is the textbook adversary—every node has refcount ≥ 1 even after the list goes out of scope, so the backup collector has to detect and reclaim the ring. It does, but later than you think. In real-time code (audio callbacks, game loops) you should explicitly break the cycle (tail.next = None) before dropping the only handle.

Worked Example: The Josephus Problem

The Josephus problem is a 1st-century puzzle attributed to the historian Flavius Josephus: n people stand in a circle; starting from position 1, every k-th person is eliminated, and the count continues from the next position. Find the position of the last survivor.

Why is it the canonical worked example for circular lists? Because the data structure mirrors the problem statement perfectly: nodes are people, the cursor is the executioner, and elimination is a pointer rewrite. No translation tax.

The simulation

We track a predecessor cursor (rather than a current cursor) so that elimination is a single pointer rewrite without searching. At each step:

  1. Hop k1k - 1 times to advance prev to the predecessor of the doomed node.
  2. doomed = prev.next; prev.next = doomed.next unhooks the doomed node in O(1)O(1).
  3. If doomed happened to be the tail, set tail = prev to keep the invariant alive.
  4. Repeat until the ring shrinks to one self-loop—the survivor.

Total work: (n1)(n - 1) elimination rounds, each costing O(k)O(k) hops, so simulation runs in O(nk)O(n \cdot k) time and O(1)O(1) extra space.

Closed form (preview)

The Josephus problem also has a beautiful O(n)O(n) closed-form recurrence: J(1,k)=0,  J(n,k)=(J(n1,k)+k)modnJ(1, k) = 0,\; J(n, k) = (J(n-1, k) + k) \bmod n. The simulation above is nevertheless the right teaching example because it makes the circular pointer dance vivid; the recurrence belongs in a chapter on dynamic programming.

Sanity check

For (n,k)=(7,3)(n, k) = (7, 3), eliminations occur in the order 3,6,2,7,5,13, 6, 2, 7, 5, 1, leaving position 44 as the survivor. Try it yourself in the playground below.


Doubly-Circular Variant

Add a prev field and the ring becomes doubly-circular. The structural invariant grows to two equations:

vi.next=v(i+1)modn,vi.prev=v(i1)modn.v_i.\text{next} = v_{(i+1) \bmod n}, \qquad v_i.\text{prev} = v_{(i-1) \bmod n}.

This is the data structure that powers an enormous amount of production systems software:

  • Linux kernel: struct list_head is a 16-byte intrusive doubly-circular node embedded in every kernel object that needs to live on a list (tasks, file descriptors, inodes, free pages, …). The kernel uses a sentinel head node so that the empty list is “a node whosenext and prev point at itself.” Insertion and removal become four pointer writes with zero special cases.
  • C++ STL: std::list is a doubly-circular list with a sentinel head, which is why end() returns a valid dereferenceable iterator (it points at the sentinel) and list::splice is O(1)O(1).
  • LRU caches: a hash map plus a doubly-circular list ordered by recency. “Touch” is unlink-and-prepend (constant time given the node pointer); “evict” is delete-tail (also constant time).
  • Editor undo / redo rings: bounded history is naturally a doubly-circular buffer, with the oldest entry being overwritten when capacity is reached.
📄c
1/* Linux-style intrusive doubly-circular list (paraphrased). */
2struct list_head { struct list_head *next, *prev; };
3
4static inline void INIT_LIST_HEAD(struct list_head *h) {
5    h->next = h;                  /* empty == self-loop both ways */
6    h->prev = h;
7}
8
9static inline void list_add(struct list_head *new, struct list_head *head) {
10    /* insert 'new' between 'head' and 'head->next' — 4 writes, no branches */
11    new->next       = head->next;
12    new->prev       = head;
13    head->next->prev = new;
14    head->next       = new;
15}
16
17static inline void list_del(struct list_head *entry) {
18    entry->prev->next = entry->next;
19    entry->next->prev = entry->prev;
20    /* (no free here — the embedding object owns the storage) */
21}
The four-pointer-rewrites-no-branches pattern is the reason kernel hackers prefer doubly-circular lists with sentinel heads. There is literally no “is the list empty?” or “am I at the head?” special case anywhere in the code path—and that's exactly the kind of correctness you want when the same routine runs a hundred million times per second under a spinlock.

Interactive: Circular List Lab

The lab below maintains a real singly-circular list. Each circle is a node; cyan arrows show next; the amber tail label points to tail; and toggling Doubly-circular reveals the violet prev arrows. Slide kk, hit Run Josephus, and watch a ring of nn nodes melt down to one.

eliminated: []
tailhead1234567

The seven nodes form a ring: every next arrow is cyan, and tail.next loops back to the head. Toggle Doubly-circular to draw the violet prev arrows. Run Josephus animates eliminating every kk-th node until one survives.

Things to try:

  1. Start fresh, set k=3k = 3, click Run Josephus. You should see eliminations in the order 3,6,2,7,5,13, 6, 2, 7, 5, 1 with survivor 44.
  2. Click Rotate by 1 a few times. The ring is unchanged; only the chosen head moves. This is identically the operation a round-robin scheduler performs to advance to the next runnable thread.
  3. Toggle Doubly-circular. Notice that the ring's topology hasn't changed—only its accessibility. prev is purely an algorithmic affordance, not a structural one.
  4. Change kk to 11. The eliminator stands still, killing every node in order—survivor is the last one inserted at the head. Edge case worth thinking about.

Where Circular Lists Live in Real Systems

SystemWhere the ring livesOperation that exploits the cycle
Linux O(1) scheduler (legacy)Per-priority runqueues, each a doubly-circular list of tasksPop head, run for a tick, push tail — all O(1), no special-casing the queue boundary
Token Ring networks (IEEE 802.5)Physical loop of stations; the token is a small frameToken forwards station-to-station; a station may transmit only when it holds the token
JACK / ALSA audioLock-free SPSC ring buffer (often power-of-two sized)Producer (audio callback) writes; consumer (mixer) reads; wraparound is free
Concurrent queues (Vyukov MPMC)Cells indexed mod N, each holding a sequence number for synchronizationEnqueue/dequeue are CAS on a single cell; index wraparound is free
Game enginesEntity turn order, AI patrol routes, animation cycles&quot;Next turn&quot; is one pointer hop; cycles loop forever by construction
Music / video playersRepeat-all playlistLast track loops to first; UI never has to ask &quot;am I at the end?&quot;
Slideshow / carousel UIDOM linked list of slidesNext/prev buttons hop one node; reaching the end wraps invisibly
Cryptographic rotors (Enigma)Physical 26-position wheels — circular by constructionEach keypress advances the rotor mod 26; the ring is the cipher state
Memory allocatorsPer-size-class free lists; some implementations link them as rings&quot;Next free chunk&quot; never needs a NULL check; recycled chunks splice in O(1)
Real-time DNS cachesTTL-ordered ring with a sweeping eviction cursorSweeper walks forever; expired entries get unlinked, new entries spliced in

Intended Cycle vs. Bug Cycle

Section 6.5 (next) introduces Floyd's tortoise-and-hare algorithm for detecting cycles in a list. It's critical to understand the conceptual difference between the cycle in this section and the cycle Floyd detects.

Intended cycle (this section)Bug cycle (next section)
OriginConstructor establishes tail.next = head as part of the type's invariantSome buggy code wrote a back-pointer that wasn't supposed to exist
Number of cyclesExactly one, of length nPossibly anywhere; the chain looks linear up to the entry point, then loops
Right algorithmWalk it; loop n times or break on identity check against startDetect it (Floyd O(n) / O(1)-space) and refuse to walk
Memory consequenceRefcount GC needs the cycle collector; manual code must break the cycle before freeSame — a leaked cycle is a leak in every reference-counted runtime

In practice, when you accept a list at an API boundary you can't always tell which kind of cycle you have. Defensive code does Floyd's detection first, then dispatches: ring ⇒ bounded traversal, no cycle ⇒ standard traversal, illegal cycle ⇒ reject.


Pitfalls

  • Forgetting the self-loop on insert-into-empty. The very first node of a circular list must have node.next = node. Skip this and the next traversal walks off the end into uninitialized memory (C) or hits an AttributeError (Python).
  • Using while p is not None. There is no None. The loop runs forever. Always remember a start node and break on identity.
  • Off-by-one in Josephus. Whether the count includes the starting node or not changes the answer. The convention here (count starts at 1 from the current cursor) yields the classical sequence;range(k) instead of range(k - 1) shifts the survivor by one position.
  • Not updating tail after removing tail. If the deletion happens to take out the tail itself, you must set tail = prev. Skip this and the next append splices into the middle of the ring; the symptom is silent data corruption, not a crash.
  • Memory leaks under reference counting. Every node in a ring has refcount ≥ 1 from its predecessor, so dropping the only external handle does not free anything until the cycle collector runs. In real-time loops, break the cycle (tail.next = None) before letting the list go out of scope.
  • Destroying a ring without breaking it first (C). A teardown loop expects a NULL terminator. Either rewrite the loop to count to nn, or sever the ring once at the start and use the standard NULL-terminated walk.
  • Treating the empty ring with a sentinel head as “empty” via size == 0. In the kernel list_head style, “empty” is head.next == &head. Write a helper (list_empty()) and use it everywhere; ad-hoc checks drift apart.

Quick Checks

Quick Check

n = 7 people stand in a circle numbered 1..7. Starting from person 1, you eliminate every 3rd person (so the first to die is person 3). Who is the last survivor?

Quick Check

Why is `while p is not None: ... ; p = p.next` an infinite loop on a non-empty circular list?


The ring is the simplest non-trivial topology after the line. With the cyclic invariant in hand, you have a data structure that is the natural home for round-robin schedulers, ring buffers, and any process that repeats forever. The next section introduces Floyd's algorithm, which lets us tell the difference between a ring we built on purpose and a cycle that snuck in by accident.

Loading comments...