Linked List Types — Singly, Doubly & Circular Visualized
Compare singly, doubly, and circular linked lists — pointer layouts, use cases, trade-offs, and interactive type-by-type visualizer.
Introduction
Linked list types differ by how many pointers each node carries and whether the chain loops. All build on the fundamentals — this guide compares singly, doubly, and circular variants with an interactive side-by-side visualizer.
Quick index
| # | Section |
|---|---|
| 1 | Type comparison |
| 2 | Interactive visualizer |
| 3 | Full implementation |
| 4 | Choosing a type |
| 5 | Real-life examples |
1. Type comparison
| Type | Pointers | Traverse | Typical use |
|---|---|---|---|
| Singly | next | Forward only | Stack, hash chains |
| Doubly | prev + next | Both directions | LRU cache, undo stack |
| Circular | next → head | Loop until start | Round-robin, ring buffer |
2. Interactive visualizer
Linked List Types
Singly, doubly, and circular variants — same node idea, different pointer layouts.
Singly
One next pointer per node — traverse forward only.
Singly linked list (SLL)
node { value, next }
// used in stacks, hash map chainsSimplest form — O(1) head insert, O(n) tail insert without tail pointer.
Singly
1 pointer/node
Doubly
2 pointers/node
Circular
tail → head
3. Full implementation
// ── Singly linked list ──
class SNode {
constructor(val) {
this.val = val
this.next = null
}
}
class SinglyLinkedList {
constructor() {
this.head = null
}
prepend(val) {
const n = new SNode(val)
n.next = this.head
this.head = n
}
toArray() {
const out = []
let c = this.head
while (c) {
out.push(c.val)
c = c.next
}
return out
}
}
// ── Doubly linked list ──
class DNode {
constructor(val) {
this.val = val
this.prev = null
this.next = null
}
}
class DoublyLinkedList {
constructor() {
this.head = this.tail = null
}
append(val) {
const n = new DNode(val)
if (!this.head) {
this.head = this.tail = n
return
}
this.tail.next = n
n.prev = this.tail
this.tail = n
}
toArray() {
const out = []
let c = this.head
while (c) {
out.push(c.val)
c = c.next
}
return out
}
}
// ── Circular (singly) ──
class CircularLinkedList {
constructor() {
this.head = null
this.tail = null
}
append(val) {
const n = new SNode(val)
if (!this.head) {
this.head = this.tail = n
n.next = n
return
}
n.next = this.head
this.tail.next = n
this.tail = n
}
}Function calls with output
// ── Demo calls — output shown on the right ──
const singly = new SinglyLinkedList()
singly.prepend(10) // → [10]
singly.prepend(5) // → [5, 10]
const doubly = new DoublyLinkedList()
doubly.append('A') // → ['A']
doubly.append('B') // → ['A', 'B']
const ring = new CircularLinkedList()
ring.append(1) // → head → 1 ↺
ring.append(2) // → head → 1 → 2 ↺4. Choosing a type
| Need | Pick |
|---|---|
| Minimal memory | Singly |
| Delete given node ref in O(1) | Doubly |
| Round-robin iteration | Circular |
| Browser back/forward | Doubly |
5. Real-life examples
| Type | Real product / system |
|---|---|
| Singly | Hash map collision chains (Java HashMap), call stack frames |
| Doubly | Browser back/forward history, LRU cache in Redis/Memcached |
| Circular | OS round-robin CPU scheduler, multiplayer turn order |
| Doubly + sentinel | Linux kernel list_head — used throughout the kernel |
| Circular buffer | Audio stream ring buffer — fixed-size, overwrite oldest |
LRU cache (doubly linked)
HashMap + doubly linked list — O(1) get, move-to-front, and evict LRU tail.
Sentinel head/tail nodes — doubly linked with prev pointers.
Real-world implementation
class LRUNode {
constructor(key, val) {
this.key = key; this.val = val
this.prev = this.next = null
}
}
class LRUCache {
constructor(cap) {
this.cap = cap; this.map = new Map()
this.head = new LRUNode(0, 0); this.tail = new LRUNode(0, 0)
this.head.next = this.tail; this.tail.prev = this.head
}
get(key) {
if (!this.map.has(key)) return -1
const node = this.map.get(key)
this._moveToHead(node)
return node.val
}
put(key, val) {
if (this.map.has(key)) { this.map.get(key).val = val; this._moveToHead(this.map.get(key)); return }
const node = new LRUNode(key, val)
this.map.set(key, node); this._addToHead(node)
if (this.map.size > this.cap) { const lru = this.tail.prev; this._remove(lru); this.map.delete(lru.key) }
}
_moveToHead(node) { this._remove(node); this._addToHead(node) }
_addToHead(node) { node.prev = this.head; node.next = this.head.next; this.head.next.prev = node; this.head.next = node }
_remove(node) { node.prev.next = node.next; node.next.prev = node.prev }
}Full working code for the scenario above.
Function calls with output
| // ── Function calls with output ── | |
| const cache = new LRUCache(3) | → capacity: 3 |
Side output shows return value after each call — step through to trace execution.
Summary
Singly is simplest; doubly enables backward traversal and O(1) middle delete; circular removes the null terminator for ring structures.
Next reads: Linked List Operations · Linked Lists in Memory
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime