<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Linear data structures
All lessons in this course

Lesson 9 of 50 14 minBeginner

Doubly and circular linked lists

Add a Prev reference to remove any node in O(1), use .NET’s LinkedList<T>, close the list into a circle, and build an LRU cache.

A singly linked list can only walk forward, which makes some operations awkward: to remove a node you first need the node before it, and removing the last node means walking the whole list to find the new tail. A doubly linked list gives every node two references: Next and Prev.

DNode.cs
public class DNode<T>{    public T Value;    public DNode<T>? Prev, Next;    public DNode(T value) => Value = value;} // null ← A ⇄ B ⇄ C ⇄ D → null      (head = A, tail = D)

With Prev, a node knows both neighbours. If you have a reference to a node, you can unlink it in O(1) without searching. Step through the pointer changes:

Interactive

Rewiring Prev and Next

Pick an operation and step through it. Each step changes one reference; the changed field lights up.

DoublyLinkedList.cs
void Remove(Node node)          // node = C{    node.Prev.Next = node.Next;  // B.Next = D    node.Next.Prev = node.Prev;  // D.Prev = B}
Following head → Next
head⇄A⇄B⇄C⇄D→ null
◀ nullAB ▶
head
◀ ABC ▶
◀ BCD ▶
◀ CDnull ▶
tail
Every node has a Prev and a Next reference. Press Step.
OperationSingly linkedDoubly linked
Add / remove firstO(1)O(1)
Add last (with tail)O(1)O(1)
Remove lastO(n): need the node before the tailO(1): tail.Prev
Remove a node you hold a reference toO(n): find its predecessorO(1)
Walk backwardsNot possibleO(n)
Extra memory per node1 reference2 references

LinkedList<T> in .NET

.NET’s LinkedList<T> is a doubly linked list. Its nodes are LinkedListNode<T> objects, and methods that take a node are O(1):

Playlist.cs
var playlist = new LinkedList<string>();playlist.AddLast("Intro");playlist.AddLast("Song A");var outro = playlist.AddLast("Outro");        // returns the node var b = playlist.AddBefore(outro, "Song B");  // O(1): we hold the nodeplaylist.AddFirst("Warm-up");playlist.Remove(b);                           // O(1) unlink by node Console.WriteLine(string.Join(" → ", playlist));Console.WriteLine($"First: {playlist.First!.Value}, Last: {playlist.Last!.Value}");Console.WriteLine($"Before Outro: {outro.Previous!.Value}");
Output
Warm-up → Intro → Song A → Outro
First: Warm-up, Last: Outro
Before Outro: Song A

Circular linked lists

In a circular list the last node’s Next points back to the first node (and in a doubly circular list, the first node’s Prev points to the last). There is no null at the end; you stop when you are back where you started. Circular lists are handy for things that go round and round: players taking turns, round-robin scheduling of tasks, or a repeating animation sequence.

Turns.cs
// A circular singly linked list of playersvar a = new Node<string>("Asha");var b = new Node<string>("Bala");var c = new Node<string>("Chen");a.Next = b; b.Next = c; c.Next = a;          // close the circle var turn = a;for (int i = 0; i < 7; i++){    Console.Write(turn.Value + " ");    turn = turn.Next!;                       // never null}
Output
Asha Bala Chen Asha Bala Chen Asha 

Sentinel (dummy) nodes

Many edge cases come from head or tail being null. A common trick is a permanent sentinel node that sits before the first real node (and often after the last). The list is never truly empty, so every insert and remove can use the same code with no special cases for the ends.

Real use: an LRU cache

An LRU (least recently used) cache keeps the N most recently used items and throws away the oldest when it is full. Browsers, databases and BIM viewers that load model files on demand all use this idea. It needs two things at once: find an item by key quickly (a Dictionary) and move items to the front or drop the oldest quickly (a doubly linked list). Together, every operation is O(1).

LruCache.cs
var cache = new LruCache(2);cache.Put("A", "Tower A.ifc");cache.Put("B", "Tower B.ifc");Console.WriteLine(cache.Get("A"));                  // A is now most recentcache.Put("C", "Tower C.ifc");                      // full: evicts BConsole.WriteLine(cache.Get("B") ?? "B was evicted");Console.WriteLine(cache.Get("C")); public class LruCache{    private readonly int _capacity;    private readonly Dictionary<string, LinkedListNode<(string Key, string Value)>> _map = new();    private readonly LinkedList<(string Key, string Value)> _order = new();   // front = most recent     public LruCache(int capacity) => _capacity = capacity;     public string? Get(string key)    {        if (!_map.TryGetValue(key, out var node)) return null;        _order.Remove(node);          // O(1): we have the node        _order.AddFirst(node);        // mark as most recently used        return node.Value.Value;    }     public void Put(string key, string value)    {        if (_map.TryGetValue(key, out var existing))            _order.Remove(existing);        else if (_map.Count == _capacity)        {            var oldest = _order.Last!;            // least recently used            _order.RemoveLast();            _map.Remove(oldest.Value.Key);        }        _map[key] = _order.AddFirst((key, value));    }}
Output
Tower A.ifc
B was evicted
Tower C.ifc
Quick check

You hold a reference to a node in the middle of a doubly linked list. How long does it take to remove it?

Quick check

How do you know when to stop walking a circular linked list?

Practice

Using LinkedList<int> with the numbers 1 to 8, remove every second node (2, 4, 6, 8) in a single pass, using node references so each removal is O(1).

Show a hint

Walk with var node = list.First;. Save node.Next before removing it, because a removed node’s Next becomes null.

Show the solution
Solution.cs
var list = new LinkedList<int>(Enumerable.Range(1, 8)); var node = list.First;while (node?.Next != null){    var toRemove = node.Next;     // the second of each pair    node = toRemove.Next;         // move on first: toRemove.Next becomes null after Remove    list.Remove(toRemove);        // O(1) by node} Console.WriteLine(string.Join(" ", list));
Output
1 3 5 7

Remember

  • A doubly linked list adds a Prev reference, so any node you hold can be removed in O(1).
  • .NET’s LinkedList<T> is doubly linked; only the node-based methods are O(1).
  • In a circular list the last node links back to the first; stop when you return to the start.
  • Dictionary + doubly linked list = LRU cache with O(1) get and put.
0 of 50 lessons completed