Toolmingo
Guides13 min read

Linked List Data Structure: Complete Guide with Code Examples

Master linked lists — singly, doubly, and circular types, all core operations, time complexity, comparison with arrays, and common interview problems with working code in Python, JavaScript, Java, and Go.

A linked list is a linear data structure where elements (called nodes) are stored in separate memory locations and connected via pointers. Unlike an array, a linked list does not store elements in contiguous memory — each node holds the data and a reference to the next node.

Linked lists are a fundamental CS topic that appear in every data structures course and in roughly 20 % of coding interview questions at major tech companies.

Quick reference

Operation Singly LL Doubly LL Array
Access by index O(n) O(n) O(1)
Insert at head O(1) O(1) O(n)
Insert at tail O(n) / O(1)* O(1) O(1) amortised
Delete at head O(1) O(1) O(n)
Delete in middle O(n) O(n) O(n)
Search O(n) O(n) O(n)
Memory overhead One pointer/node Two pointers/node None

* O(1) tail insert when you maintain a tail pointer.


What is a node?

Every linked list is built from nodes. A node contains two things:

  1. Data — the value stored (integer, string, object, etc.)
  2. Next pointer — a reference to the next node (or null/None at the end)
[ data | next ] → [ data | next ] → [ data | next ] → null
   head                                  tail

Types of linked lists

Singly linked list

Each node has one pointer: next. Traversal goes only forward.

head
 ↓
[10 | •] → [20 | •] → [30 | null]

Doubly linked list

Each node has two pointers: prev and next. Traversal goes both ways.

null ← [10 | •] ↔ [20 | •] ↔ [30 | •] → null
       head                     tail

Circular linked list

The tail's next pointer points back to the head instead of null.

[10 | •] → [20 | •] → [30 | •]
 ↑                          |
 └──────────────────────────┘
Type Pros Cons Use cases
Singly Simple, less memory Forward-only, no O(1) tail delete Stacks, queues, hash chaining
Doubly Bidirectional, O(1) head+tail insert/delete Extra pointer per node Browsers (back/forward), LRU cache
Circular No null end, continuous traversal Complex to terminate loops Round-robin schedulers, playlists

Singly linked list — implementation

Python

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    # Insert at head — O(1)
    def prepend(self, data):
        node = Node(data)
        node.next = self.head
        self.head = node

    # Insert at tail — O(n)
    def append(self, data):
        node = Node(data)
        if not self.head:
            self.head = node
            return
        current = self.head
        while current.next:
            current = current.next
        current.next = node

    # Delete first occurrence — O(n)
    def delete(self, data):
        if not self.head:
            return
        if self.head.data == data:
            self.head = self.head.next
            return
        current = self.head
        while current.next:
            if current.next.data == data:
                current.next = current.next.next
                return
            current = current.next

    # Search — O(n)
    def search(self, data):
        current = self.head
        while current:
            if current.data == data:
                return True
            current = current.next
        return False

    # Print all nodes — O(n)
    def display(self):
        elements = []
        current = self.head
        while current:
            elements.append(str(current.data))
            current = current.next
        print(" → ".join(elements) + " → null")


# Usage
ll = LinkedList()
ll.append(10)
ll.append(20)
ll.append(30)
ll.prepend(5)
ll.display()  # 5 → 10 → 20 → 30 → null
ll.delete(20)
ll.display()  # 5 → 10 → 30 → null

JavaScript

class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
  }

  prepend(data) {
    const node = new Node(data);
    node.next = this.head;
    this.head = node;
  }

  append(data) {
    const node = new Node(data);
    if (!this.head) { this.head = node; return; }
    let cur = this.head;
    while (cur.next) cur = cur.next;
    cur.next = node;
  }

  delete(data) {
    if (!this.head) return;
    if (this.head.data === data) { this.head = this.head.next; return; }
    let cur = this.head;
    while (cur.next) {
      if (cur.next.data === data) { cur.next = cur.next.next; return; }
      cur = cur.next;
    }
  }

  toArray() {
    const arr = [];
    let cur = this.head;
    while (cur) { arr.push(cur.data); cur = cur.next; }
    return arr;
  }
}

const ll = new LinkedList();
ll.append(10); ll.append(20); ll.append(30);
console.log(ll.toArray()); // [10, 20, 30]

Java

public class LinkedList {
    static class Node {
        int data;
        Node next;
        Node(int data) { this.data = data; }
    }

    private Node head;

    public void prepend(int data) {
        Node node = new Node(data);
        node.next = head;
        head = node;
    }

    public void append(int data) {
        Node node = new Node(data);
        if (head == null) { head = node; return; }
        Node cur = head;
        while (cur.next != null) cur = cur.next;
        cur.next = node;
    }

    public void delete(int data) {
        if (head == null) return;
        if (head.data == data) { head = head.next; return; }
        Node cur = head;
        while (cur.next != null) {
            if (cur.next.data == data) { cur.next = cur.next.next; return; }
            cur = cur.next;
        }
    }

    public void display() {
        Node cur = head;
        while (cur != null) {
            System.out.print(cur.data + " → ");
            cur = cur.next;
        }
        System.out.println("null");
    }
}

Go

package main

import "fmt"

type Node struct {
    Data int
    Next *Node
}

type LinkedList struct {
    Head *Node
}

func (l *LinkedList) Prepend(data int) {
    l.Head = &Node{Data: data, Next: l.Head}
}

func (l *LinkedList) Append(data int) {
    node := &Node{Data: data}
    if l.Head == nil { l.Head = node; return }
    cur := l.Head
    for cur.Next != nil { cur = cur.Next }
    cur.Next = node
}

func (l *LinkedList) Delete(data int) {
    if l.Head == nil { return }
    if l.Head.Data == data { l.Head = l.Head.Next; return }
    cur := l.Head
    for cur.Next != nil {
        if cur.Next.Data == data { cur.Next = cur.Next.Next; return }
        cur = cur.Next
    }
}

func (l *LinkedList) Display() {
    cur := l.Head
    for cur != nil {
        fmt.Printf("%d → ", cur.Data)
        cur = cur.Next
    }
    fmt.Println("nil")
}

Doubly linked list — implementation

class DNode:
    def __init__(self, data):
        self.data = data
        self.prev = None
        self.next = None

class DoublyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, data):          # O(1) with tail pointer
        node = DNode(data)
        if not self.tail:
            self.head = self.tail = node
            return
        node.prev = self.tail
        self.tail.next = node
        self.tail = node

    def prepend(self, data):         # O(1)
        node = DNode(data)
        if not self.head:
            self.head = self.tail = node
            return
        node.next = self.head
        self.head.prev = node
        self.head = node

    def delete(self, node: DNode):   # O(1) when you have the node reference
        if node.prev:
            node.prev.next = node.next
        else:
            self.head = node.next    # deleting head
        if node.next:
            node.next.prev = node.prev
        else:
            self.tail = node.prev    # deleting tail

    def display_forward(self):
        cur, out = self.head, []
        while cur:
            out.append(str(cur.data))
            cur = cur.next
        print(" ↔ ".join(out))

    def display_backward(self):
        cur, out = self.tail, []
        while cur:
            out.append(str(cur.data))
            cur = cur.prev
        print(" ↔ ".join(out))

Core operations — time and space complexity

Operation Singly LL Doubly LL Notes
Prepend O(1) O(1) Update head pointer
Append (with tail ptr) O(1) O(1) Update tail pointer
Append (no tail ptr) O(n) O(n) Traverse to end
Insert at index k O(k) O(min(k, n−k)) DLL can start from tail
Delete head O(1) O(1)
Delete tail (with tail ptr) O(n)* O(1) *Singly needs to find prev
Delete at index k O(k) O(min(k, n−k))
Delete by reference O(n)* O(1) *Singly needs to find prev
Search O(n) O(n) No random access
Access by index O(n) O(n)
Space per node O(1) extra O(1) extra (2 ptrs)

Linked list vs array

Array Linked List
Memory layout Contiguous Scattered
Access by index O(1) O(n)
Insert / delete at start O(n) — shift elements O(1)
Insert / delete at end O(1) amortised O(1) with tail ptr
Insert / delete in middle O(n) O(n) — traverse, then O(1)
Cache performance Excellent (cache line) Poor (pointer chasing)
Memory overhead None 1–2 pointers per node
Dynamic sizing Requires resize/copy Naturally dynamic
Binary search O(log n) Not practical — O(n)

Use an array when: you need fast index access, binary search, or cache-friendly iteration.

Use a linked list when: you have frequent head/tail insertions or deletions, need O(1) splicing, or are building higher-level structures (stack, queue, deque, LRU cache).


Common linked list interview problems

1. Reverse a singly linked list

def reverse(head):
    prev, cur = None, head
    while cur:
        nxt = cur.next
        cur.next = prev
        prev = cur
        cur = nxt
    return prev          # new head

Time: O(n) · Space: O(1)

2. Detect a cycle (Floyd's tortoise and hare)

def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False

Time: O(n) · Space: O(1)

3. Find middle node

def find_middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow           # middle (or second middle for even length)

4. Merge two sorted linked lists

def merge_sorted(l1, l2):
    dummy = Node(0)
    cur = dummy
    while l1 and l2:
        if l1.data <= l2.data:
            cur.next = l1; l1 = l1.next
        else:
            cur.next = l2; l2 = l2.next
        cur = cur.next
    cur.next = l1 or l2
    return dummy.next

5. Remove Nth node from end

def remove_nth_from_end(head, n):
    dummy = Node(0); dummy.next = head
    fast = slow = dummy
    for _ in range(n + 1):
        fast = fast.next
    while fast:
        slow = slow.next
        fast = fast.next
    slow.next = slow.next.next
    return dummy.next

6. Check if linked list is palindrome

def is_palindrome(head):
    # Step 1: find middle
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    # Step 2: reverse second half
    prev, cur = None, slow
    while cur:
        nxt = cur.next; cur.next = prev; prev = cur; cur = nxt
    # Step 3: compare
    left, right = head, prev
    while right:
        if left.data != right.data:
            return False
        left = left.next; right = right.next
    return True

7. Find start of cycle

def cycle_start(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            # Move one pointer to head
            slow = head
            while slow is not fast:
                slow = slow.next
                fast = fast.next
            return slow   # start of cycle
    return None

Linked list as stack and queue

Stack (LIFO) — prepend + delete head

class Stack:
    def __init__(self):
        self._list = LinkedList()

    def push(self, data):
        self._list.prepend(data)   # O(1)

    def pop(self):
        if not self._list.head:
            raise IndexError("empty")
        val = self._list.head.data
        self._list.head = self._list.head.next
        return val                 # O(1)

    def peek(self):
        return self._list.head.data if self._list.head else None

Queue (FIFO) — append + delete head

class Queue:
    def __init__(self):
        self.head = self.tail = None

    def enqueue(self, data):        # O(1) with tail pointer
        node = Node(data)
        if self.tail:
            self.tail.next = node
        self.tail = node
        if not self.head:
            self.head = node

    def dequeue(self):              # O(1)
        if not self.head:
            raise IndexError("empty")
        val = self.head.data
        self.head = self.head.next
        if not self.head:
            self.tail = None
        return val

Real-world uses of linked lists

Use case Type Why LL?
Browser history back/forward Doubly O(1) navigate both ways
Undo/redo in editors Doubly O(1) push + pop
Music playlist Singly / Circular Append song O(1), loop-back
OS process scheduling (round-robin) Circular Continuous rotation
LRU cache Doubly + Hash map O(1) move-to-front on access
Hash table chaining Singly Bucket collision lists
Memory allocators (free list) Singly Track free blocks
Implementing other structures Any Deque, adjacency list

LRU cache — linked list + hash map

The most famous linked-list-in-production pattern is the LRU (Least Recently Used) cache. It achieves O(1) get and O(1) put by combining a doubly linked list with a hash map.

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = OrderedDict()  # preserves insertion order

    def get(self, key):
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)   # mark as recently used
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)  # evict LRU (first item)

Common mistakes

Mistake Problem Fix
Forgetting to update tail tail points to freed node Always update tail on append/delete
Off-by-one in two-pointer problems Wrong middle, wrong Nth from end Use dummy node + test edge cases
Mutating head inside a method Loses reference to the list Save head to cur, mutate cur
Infinite loop in cycle code fast.next.next on null Check fast and fast.next in while condition
Not handling empty list NullPointerException / AttributeError Check if not head at the top of each method
Forgetting prev links in doubly LL Broken backward traversal Update both prev and next on every insert/delete
Using linked list where array is better 5–10× slower due to cache misses Prefer array for indexed access or binary search
Leaking nodes (in languages without GC) Memory leak Set next = null on deleted node in C/C++

Linked list vs array vs deque

Array / list Linked list collections.deque
Index access O(1) O(n) O(n)
Append / prepend O(1) / O(n) O(1) / O(1) O(1) / O(1)
Insert middle O(n) O(n) O(n)
Delete head O(n) O(1) O(1)
Memory Compact Scattered Doubly LL internally
Cache friendly Yes No No
In production Default choice Rarely explicitly Queue / deque

Practical note: In Python, collections.deque IS a doubly linked list under the hood. In Java, LinkedList implements both List and Deque. You almost never implement a linked list from scratch in production — you implement them to demonstrate understanding in interviews.


FAQ

Q: When should I use a linked list instead of an array? Use a linked list when you have very frequent insertions or deletions at the head or tail and don't need index access. In practice, collections.deque (Python) or ArrayDeque (Java) are often better choices because they provide O(1) operations at both ends with better cache performance than a custom linked list.

Q: Why is binary search not practical on a linked list? Binary search requires O(1) random access to jump to the middle element. A linked list can only reach the middle by traversing from the head — O(n) — making the algorithm O(n log n) overall, worse than linear search O(n).

Q: What is the difference between a singly and doubly linked list in terms of memory? A doubly linked list stores two pointers per node (prev and next) instead of one. On a 64-bit system, each pointer is 8 bytes, so a doubly linked list uses 8 extra bytes per node. For a list of 1 million integers, that's ~8 MB of extra overhead.

Q: How do I detect a cycle in a linked list? Use Floyd's cycle detection algorithm (two pointers, slow and fast). The slow pointer advances one step; the fast pointer advances two steps. If they ever meet, there is a cycle. Time O(n), space O(1). See the code example above.

Q: How is a linked list used in a hash table? Most hash tables handle collisions with separate chaining: each bucket holds a linked list of all key-value pairs that hash to that bucket. Lookup traverses the list at the target bucket — O(1) average, O(n) worst case.

Q: What is the difference between a linked list and a binary tree? A linked list is a linear structure — each node has at most one successor. A binary tree is a hierarchical structure — each node has at most two children (left and right). Both use nodes with pointers, but binary trees support O(log n) search when balanced, which a linked list cannot.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools