Circular Linked List
A linked list whose last node points back to the first, so traversal wraps around and a single tail pointer gives O(1) access to both ends.
Definition
In a circular linked list the final node's next refers to the head instead of null (a doubly linked variant also sets head.prev = tail). There is no natural end: starting from any node and following next eventually returns to the start.
The practical payoff is that keeping just a `tail` pointer gives O(1) access to both ends — the head is tail.next — so a queue needs only one pointer. The other payoff is fairness: round-robin schedulers, turn-based games, and the Josephus problem all naturally "go around the table".
Circular lists are less common in interviews as a required structure, but they explain why cycle detection matters: a list that accidentally becomes circular makes every naive while node != null loop run forever. Fast & Slow Pointers (Floyd) is the standard way to detect it.
Intuition
A mental model before the formal terms.
A group of people holding hands in a circle. Whoever you start from, walking to the left eventually brings you back. If you know the last person in "line", you also know the first — they are holding hands.
A clock face is a circular list of twelve numbers: after 12 comes 1. Modular arithmetic ((i + 1) % n) is the array analogue; a circular list gives the same wrap-around with O(1) insertion anywhere.
How it works
- Keep a single
tailpointer. Empty list:tail = null. One node:tail.next = tail. pushBack(v):node.next = tail.next; tail.next = node; tail = node. Head staystail.next.pushFront(v): same aspushBackbut do not advancetail— the new node becomestail.next, i.e. the head.popFront():head = tail.next; tail.next = head.next(ifhead == tail, settail = null).rotate():tail = tail.next— the old head becomes the new tail inO(1). This is round-robin.- Traversal:
do { visit(cur); cur = cur.next } while (cur != tail.next)— ado/whilebecause the stop condition is the start node, notnull. - Cycle detection on an arbitrary list:
slowmoves 1,fastmoves 2; if they meet the list is circular (or contains a cycle).
Why it works
Because tail.next is the head by construction, both ends are one pointer dereference away, which is why a queue needs only tail.
Traversal terminates because the list is a single cycle containing every node: starting at tail.next and stopping when we return to it visits each node exactly once.
Rotation is O(1) because the "end" is a matter of which node we call tail; the physical ring never changes.
Operations
| Operation | Description | Cost |
|---|---|---|
| pushBack(v) | Link after tail, then advance tail. | O(1) |
| pushFront(v) | Link after tail without advancing tail. | O(1) |
| popFront() | Unlink tail.next. | O(1) |
| peekFront() / peekBack() | tail.next.value / tail.value. | O(1) |
| rotate() | Advance tail one step; old head becomes the tail. | O(1) |
| search(v) | Walk the ring once starting at the head. | O(n) |
| deleteAfter(node) | Skip node.next; adjust tail if it was removed. | O(1) |
| hasCycle(head) | Floyd slow/fast pointer check on an arbitrary list. | O(n) |
Recognition
How to tell a problem wants this.
- Round-robin scheduling, turn taking, "the next player after the last is the first".
- The Josephus problem or "eliminate every k-th person in a circle".
- Circular buffers/queues described in terms of wrap-around (compare Circular Queue on an array).
- A linked-list problem that hints the list may contain a cycle — reach for Floyd's algorithm.
Interactive demo
Play, step, change the input. ← → and space work too.
Showing the closely related Singly Linked List visualization.
1append(v): walk to the tail; tail.next = Node(v)2prepend(v): node = Node(v); node.next = head; head = node3insert(v, i): walk to node i-1; node.next = prev.next; prev.next = node4delete(v): walk until curr.value == v; prev.next = curr.next5reverse(): prev = null; curr = head6 while curr: next = curr.next; curr.next = prev; prev = curr; curr = next7 head = prevPseudocode
1class Node: value, next2class CircularLinkedList: tail = null, size = 03pushBack(v): n = Node(v)4 if tail == null: n.next = n; tail = n5 else: n.next = tail.next; tail.next = n; tail = n6 size++7pushFront(v): pushBack(v) but do not move tail (tail stays the old tail)8popFront(): head = tail.next9 if head == tail: tail = null else tail.next = head.next10 size--; return head.value11rotate(): if tail: tail = tail.next12traverse(): if tail == null return; cur = tail.next13 do: visit(cur); cur = cur.next while cur != tail.nextImplementation
1from typing import Generic, Iterator, Optional, TypeVar2 3T = TypeVar("T")4 5 61 · Node and tail-only handle7class RingNode(Generic[T]):8 __slots__ = ("value", "next")9 10 def __init__(self, value: T, next: "Optional[RingNode[T]]" = None) -> None:11 self.value = value12 self.next = next13 14 15class CircularLinkedList(Generic[T]):16 def __init__(self) -> None:17 self.tail: Optional[RingNode[T]] = None # tail.next is the head18 self._n = 019 20 def __len__(self) -> int:21 return self._n22 23 def is_empty(self) -> bool:24 return self.tail is None25 262 · Push back (link into the ring, advance tail)27 def push_back(self, v: T) -> None:28 if self.tail is None:29 node: RingNode[T] = RingNode(v)30 node.next = node31 self.tail = node32 else:33 node = RingNode(v, self.tail.next)34 self.tail.next = node35 self.tail = node36 self._n += 137 383 · Push front (same link, tail stays)39 def push_front(self, v: T) -> None:40 if self.tail is None:41 self.push_back(v)42 return43 self.tail.next = RingNode(v, self.tail.next)44 self._n += 145 464 · Pop front47 def pop_front(self) -> T:48 if self.tail is None:49 raise IndexError("pop from empty ring")50 head = self.tail.next51 assert head is not None52 if head is self.tail:53 self.tail = None54 else:55 self.tail.next = head.next56 self._n -= 157 return head.value58 595 · Rotate (O(1): head becomes tail)60 def rotate(self) -> None:61 if self.tail is not None:62 self.tail = self.tail.next63 646 · Traverse once around the ring65 def __iter__(self) -> Iterator[T]:66 if self.tail is None:67 return68 head = self.tail.next69 cur = head70 while True:71 assert cur is not None72 yield cur.value73 cur = cur.next74 if cur is head:75 break- Only
tailis stored;tail.nextis the head. push_backself-links on an empty ring, otherwise splices aftertailand advances it.push_frontsplices in the same position but leavestailunchanged.pop_frontremoves the head; a one-node ring collapses toNone.rotateis one assignment;__iter__is a generator that yields once around the ring with an explicitbreakwhen it returns tohead.
A ring holds a reference cycle, so nodes are freed by the cycle collector, not immediately by refcounting.
collections.deque.rotate(k)gives O(k) rotation on a ready-made structure; for round-robin over a fixed set it beats a hand-rolled ring.itertools.cycle(iterable)yields elements forever — the functional take on a ring.- Defining
__iter__as a generator makesfor v in ringandlist(ring)work.
while cur is not Nonetraversal never ends on a ring.print(ring)orrepron nodes that reference each other can recurse without a custom__repr__.- Forgetting the single-node case in
pop_front.
- Iteration protocol: JS/TS
[Symbol.iterator]and Python__iter__make the ring usable inforloops; C++ would need a custom iterator type or the visitor callback shown. - Memory: C++ must pop every node explicitly (following
nextnever reaches null); JS collects cycles; Python needs the cycle collector because refcounts never hit zero. - Built-in alternatives: Python
deque.rotateanditertools.cycle; nothing equivalent in C++ or JS beyond modular indexing over an array. - Serialisation:
JSON.stringify(JS) and defaultrepr(Python) choke on the cycle; C++ has no default printing to worry about.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(n) | O(n) | O(1) for head (tail.next) and tail. |
| Search | O(n) | O(n) | |
| Insert | O(1) | O(1) | At either end or after a known node. |
| Delete | O(1) | O(1) | Front, or after a known node; O(n) to delete the tail in a singly circular list. |
| Update | O(n) | O(n) | O(1) once located. |
| Rotate | O(1) | O(1) | |
| Cycle detection | O(n) | O(n) | Floyd, O(1) space. |
| Space | O(n) | Only one list-level pointer (tail) is needed for queue behaviour. | |
Advantages & disadvantages
O(1)access to both ends with a singletailpointer — a compact queue.O(1)rotation makes round-robin iteration trivial.- Traversal can begin at any node and still cover the whole list.
- No
nullat the end; every node always has a valid successor.
- Infinite loops if the termination condition is written as
!= null. - Requires a
do/whileor explicit start-node check for traversal; harder to reason about than a terminated list. - Same
O(n)access and search as any linked list; no cache locality. - Singly circular lists still need the predecessor for deletion; doubly circular lists cost two pointers per node.
Use cases
- Round-robin CPU/process schedulers and load balancers cycling through workers.
- Multiplayer turn order; music playlists on repeat.
- Josephus problem and similar elimination games.
- Fibonacci heap root lists and other structures that splice rings in
O(1). - Simple queues with one pointer in memory-constrained embedded code.
- Round-robin iteration over a set that changes over time (schedulers, turn order).
- A queue where you want
O(1)enqueue/dequeue with a single pointer. - Elimination games (Josephus) where the "next" after the last is the first.
- Ordinary sequential processing — a terminated Singly Linked List is simpler and safer.
- Fixed-capacity ring buffers — a Circular Queue on an array is faster and cache-friendly.
- Any situation where accidental infinite loops would be costly and the wrap-around is not needed.
Alternatives
Common mistakes
- Writing
while (cur != null)— the loop never ends; use ado/whilethat stops at the start node. - Forgetting the single-node case where
tail.next == tail, so popping must settail = null. - Advancing
tailonpushFront(turns it intopushBack) or not advancing it onpushBack. - Deleting the tail node without updating
tail, leaving a dangling reference into freed memory. - Building a ring by mistake in a normal list (e.g. reorder/rotate problems) and not terminating with
null.
Interview patterns
- Josephus problem: simulate with a ring in
O(n·k), or derive theO(n)recurrenceJ(n) = (J(n-1) + k) mod n. - Rotate a list right by
k: connect tail to head, then break the ring atn - k mod n. - Detect a cycle and find its entry point with Floyd's algorithm.
- Insert into a sorted circular list given any node (handle wrap-around and all-equal cases).
- Split a circular list into two halves using slow/fast pointers.
- Array versus linked listBeginner