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 with nodes :
That single change of the modular index is the entire definition. The structural invariant is that starting from any node and following 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
nextpointer per node, ring oriented in one direction. - Doubly-circular: every node holds
prevandnext, with and . The Linux kernel'sstruct list_headis 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:
| Domain | Use | Why circular |
|---|---|---|
| Operating systems | Round-robin process scheduler | Each process gets a quantum, then control passes to next; no end-of-list |
| Networking | Token Ring, fair-queue schedulers | Token passes around forever; no node is privileged |
| Multimedia | Audio / video ring buffers (JACK, ALSA) | Producer and consumer cursors lap each other endlessly |
| Games / UI | Turn order, image carousels, repeat-all playlists | Last player loops back to first; UX is naturally circular |
| Cryptography | Enigma rotors (literally circular wheels of substitutions) | Each keypress advances the rotor by one position, mod 26 |
| Concurrency | Lock-free MPMC queues built on circular buffers | Index arithmetic mod N; producer/consumer never collide |
| Caching | DNS / CDN caches with TTL-driven rotation | Eviction 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 append, also a tail pointer. In a circular list, holding only the tail is strictly better:
- Head is one hop away. By the cyclic invariant,
tail.nextis the head. So head access is free. - Append is O(1). The tail is in your hand; just splice a new node between
tailandtail.next, then movetail. - Prepend is O(1). Same splice, but don't move the tail—the new node simply becomes the new head.
- Delete head is O(1). Rewrite
tail.nextto skip the old head. - One pointer of state, two endpoints accessible. Half the bookkeeping, no consistency bugs between separate head/tail pointers.
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:
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 onceTwo 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:
1p = head
2for _ in range(n): # exactly n hops, no identity check needed
3 visit(p.value)
4 p = p.nextThis shape is preferable inside hot loops where the identity check would prevent loop-unrolling, and is the standard idiom inside ring-buffer code where is a power of two and the compiler can replace the increment with a bitwise & (mask).
Core Operations and Their Cost
| Operation | Singly-circular (tail handle) | Doubly-circular | Notes |
|---|---|---|---|
| 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 head | O(1) | O(1) | rewrite tail.next |
| delete tail | O(n) | O(1) | singly: must walk to find predecessor |
| delete given node | O(1) if you have prev | O(1) | doubly: cur.prev and cur.next are at hand |
| traverse | O(n) | O(n) | stop when p is start, OR loop n times |
| rotate by k | O(k) | O(k mod n) | tail = tail.next, k times |
| split in half | O(n) | O(n) | fast/slow pointers; sever and re-tie |
| concat L1 ++ L2 | O(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:
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 ATwo pointer rewrites, no copying, no walking either ring. With arrays this would be . 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.
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.
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:
- Hop times to advance
prevto the predecessor of the doomed node. doomed = prev.next;prev.next = doomed.nextunhooks the doomed node in .- If
doomedhappened to be the tail, settail = prevto keep the invariant alive. - Repeat until the ring shrinks to one self-loop—the survivor.
Total work: elimination rounds, each costing hops, so simulation runs in time and extra space.
Closed form (preview)
Sanity check
For , eliminations occur in the order , leaving position 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:
This is the data structure that powers an enormous amount of production systems software:
- Linux kernel:
struct list_headis 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 whosenextandprevpoint at itself.” Insertion and removal become four pointer writes with zero special cases. - C++ STL:
std::listis a doubly-circular list with a sentinel head, which is whyend()returns a valid dereferenceable iterator (it points at the sentinel) andlist::spliceis . - 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.
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}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 , hit Run Josephus, and watch a ring of nodes melt down to one.
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 -th node until one survives.
Things to try:
- Start fresh, set , click Run Josephus. You should see eliminations in the order with survivor .
- 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.
- Toggle Doubly-circular. Notice that the ring's topology hasn't changed—only its accessibility.
previs purely an algorithmic affordance, not a structural one. - Change to . 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
| System | Where the ring lives | Operation that exploits the cycle |
|---|---|---|
| Linux O(1) scheduler (legacy) | Per-priority runqueues, each a doubly-circular list of tasks | Pop 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 frame | Token forwards station-to-station; a station may transmit only when it holds the token |
| JACK / ALSA audio | Lock-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 synchronization | Enqueue/dequeue are CAS on a single cell; index wraparound is free |
| Game engines | Entity turn order, AI patrol routes, animation cycles | "Next turn" is one pointer hop; cycles loop forever by construction |
| Music / video players | Repeat-all playlist | Last track loops to first; UI never has to ask "am I at the end?" |
| Slideshow / carousel UI | DOM linked list of slides | Next/prev buttons hop one node; reaching the end wraps invisibly |
| Cryptographic rotors (Enigma) | Physical 26-position wheels — circular by construction | Each keypress advances the rotor mod 26; the ring is the cipher state |
| Memory allocators | Per-size-class free lists; some implementations link them as rings | "Next free chunk" never needs a NULL check; recycled chunks splice in O(1) |
| Real-time DNS caches | TTL-ordered ring with a sweeping eviction cursor | Sweeper 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) | |
|---|---|---|
| Origin | Constructor establishes tail.next = head as part of the type's invariant | Some buggy code wrote a back-pointer that wasn't supposed to exist |
| Number of cycles | Exactly one, of length n | Possibly anywhere; the chain looks linear up to the entry point, then loops |
| Right algorithm | Walk it; loop n times or break on identity check against start | Detect it (Floyd O(n) / O(1)-space) and refuse to walk |
| Memory consequence | Refcount GC needs the cycle collector; manual code must break the cycle before free | Same — 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 anAttributeError(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 ofrange(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 , 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 kernellist_headstyle, “empty” ishead.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.