Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Circular linked lists: keep one tail and a valid cycle

Last updated: 5 Oct 20269 min read
tutorial
IntermediateBy AITrove Editorial

A circular singly linked list has no null link at its end: the tail node points back to the first node. Keeping only a tail reference is enough to reach both ends because tail.next is the head. This dispatch ring uses that invariant for constant-time append, one-step rotation, and removal of the current head. It tracks length so a snapshot stops after exactly that many links, avoiding an accidental infinite traversal. In the empty state tail is None and length is zero; in the singleton state tail.next points to tail itself. Removal severs the detached node's link so a caller cannot mistake it for a member of the live cycle.

Operational case

Append D-19, D-47, and D-61. The first snapshot begins at D-19. Rotating once advances the tail reference from D-61 to D-19, which makes D-47 the new head without changing any node links. Removing that head returns D-47; the remaining ring reads D-61 followed by D-19. This convention implements fair turn order only if callers rotate or remove at the intended scheduling boundary. A ring by itself has no thread safety, job acknowledgment, or protection against a caller holding an old node reference.

Working Python program

python
"""Single-tail circular list for a small round-robin dispatch ring."""


class DispatchNode:
    def __init__(self, depot_id):
        self.depot_id = depot_id
        self.next = None


class DispatchRing:
    def __init__(self):
        self.tail = None
        self.length = 0

    def append(self, depot_id):
        node = DispatchNode(depot_id)
        if self.tail is None:
            node.next = node
        else:
            node.next = self.tail.next
            self.tail.next = node
        self.tail = node
        self.length += 1

    def rotate_once(self):
        if self.tail is not None:
            self.tail = self.tail.next

    def remove_front(self):
        if self.tail is None:
            raise IndexError("empty dispatch ring")
        front = self.tail.next
        if front is self.tail:
            self.tail = None
        else:
            self.tail.next = front.next
        self.length -= 1
        front.next = None
        return front.depot_id

    def snapshot(self):
        if self.tail is None:
            return []
        result = []
        current = self.tail.next
        for _ in range(self.length):
            result.append(current.depot_id)
            current = current.next
        return result


dispatch = DispatchRing()
for depot in ("D-19", "D-47", "D-61"):
    dispatch.append(depot)
print(dispatch.snapshot())
dispatch.rotate_once()
print(dispatch.remove_front())
print(dispatch.snapshot())

Output

Output
['D-19', 'D-47', 'D-61']
D-47
['D-61', 'D-19']

Time, space, and tradeoff

Append, one-step rotation, and head removal each take O(1) time and use O(1) extra space. Capturing all N members takes O(N) time and O(N) output space. A rotation by K positions would take O(K mod N) link steps in this representation; it is not a constant-time arbitrary jump. Every member adds one node and one next link, so the ring uses O(N) space. Python's cyclic garbage collector can reclaim an unreachable ring, but a live reference to any node still keeps reachable nodes alive. Use a built-in deque when the application only needs ordinary end operations and no explicit node ownership.

Common Mistakes

  • Do not leave a singleton pointing to None; it must point to itself.
  • Do not use a null-terminated traversal condition on a cycle.
  • Do not forget to repair tail.next when removing the head.
  • Do not claim constant-time rotation by an arbitrary number of positions from this code.

Connected lessons

Use this invariant in the dispatch audit project, then check the operations quiz.

data structures
range-query-structures
Storage details