A Beginner's Guide to Linked List

EJ JungEJ Jung
7 min read

1. Introduction to Linked Lists

A Linked List is a linear data structure where elements, called nodes, are connected using pointers. Unlike arrays, linked lists do not require contiguous memory, making insertion and deletion operations efficient.

Each node typically contains:

  • Data: The value stored in the node.

  • Pointer (Next): A reference to the next node in the sequence.


2. Types of Linked Lists

2.1 Singly Linked List

Each node points only to the next node.

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

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

    def append(self, data):
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            return
        curr = self.head
        while curr.next:
            curr = curr.next
        curr.next = new_node

    def display(self):
        curr = self.head
        while curr:
            print(curr.data, end=" -> ")
            curr = curr.next
        print("None")

# Example
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.display()  # Output: 1 -> 2 -> 3 -> None

2.2 Doubly Linked List

Each node has two pointers: one pointing to the next node and another pointing to the previous node.

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

This allows traversal in both directions.

2.3 Circular Linked List

The last node points back to the head, forming a circle.


3. Operations on Linked Lists

3.1 Insertion

  • At the beginning

  • At the end

  • After a specific node

def insert_at_beginning(self, data):
    new_node = Node(data)
    new_node.next = self.head
    self.head = new_node

3.2 Deletion

  • Delete first node

  • Delete by value

def delete_by_value(self, key):
    curr = self.head
    if curr and curr.data == key:
        self.head = curr.next
        return
    prev = None
    while curr and curr.data != key:
        prev = curr
        curr = curr.next
    if curr:
        prev.next = curr.next

3.3 Searching

def search(self, key):
    curr = self.head
    while curr:
        if curr.data == key:
            return True
        curr = curr.next
    return False

4. Dummy Node Usage Example

In some problems, especially when modifying the list (like swapping pairs), a dummy node helps simplify edge cases.

Example: LeetCode 24. Swap Nodes in Pairs

from typing import Optional

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0, head)
        prev = dummy

        while head and head.next:
            first = head
            second = head.next

            # switch
            prev.next = second
            first.next = second.next
            second.next = first

            # move pointer
            prev = first
            head = first.next

        return dummy.next

5. Advanced Topics

5.1 Cycle Detection (Floyd’s Tortoise and Hare)

Cycle detection in a linked list is done using fast and slow pointers.

def hasCycle(head: Optional[ListNode]) -> bool:
    slow, fast = head, head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False

5.2 LRU Cache Implementation

An LRU (Least Recently Used) Cache evicts the least recently used element when capacity is full. It is commonly implemented using a HashMap + Doubly Linked List.

  • HashMap: Provides O(1) lookup for nodes.

  • Doubly Linked List: Maintains usage order (most recent at head, least recent at tail).

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity: int):
        self.cache = OrderedDict()
        self.capacity = capacity

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)
        return self.cache[key]

    def put(self, key: int, value: int) -> None:
        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)

6. Advantages and Disadvantages

Advantages

  • Dynamic memory allocation (no fixed size).

  • Efficient insertions and deletions (O(1) when pointer is known).

Disadvantages

  • Random access is not allowed (O(n) traversal).

  • Extra memory for storing pointers.

  • Cache-unfriendly compared to arrays.


7. Applications of Linked Lists

  • Stacks and Queues

  • Polynomial Representation

  • Hash Chaining

  • Dynamic Memory Allocation

  • LRU Cache


8. Common Pitfalls

  • Forgetting to update pointers during insertions or deletions.

  • Memory leaks due to lost references.

  • Infinite loops when dealing with circular linked lists.


7. Advanced Linked List Patterns

7.1 Swap Nodes in Pairs (Dummy Node Pattern) — LeetCode 24

Using a dummy (sentinel) node simplifies head manipulations and reduces edge cases.

# LeetCode 24. Swap Nodes in Pairs
# Dummy (sentinel) node pattern to simplify pointer rewiring
from typing import Optional

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0, head)   # sentinel that points to head
        prev = dummy                # "prev" trails the pair being swapped

        while head and head.next:   # there is a pair to swap
            first = head
            second = head.next

            # rewire: prev -> second -> first -> rest
            prev.next = second
            first.next = second.next
            second.next = first

            # advance pointers for the next pair
            prev = first
            head = first.next

        return dummy.next

7.2 Cycle Detection (Floyd’s Tortoise & Hare) — LeetCode 141/142

Two pointers (slow + fast) move at different speeds. If a cycle exists, they will meet.

# Detect if a cycle exists
from typing import Optional

def hasCycle(head: Optional[ListNode]) -> bool:
    slow, fast = head, head
    while fast and fast.next:
        slow = slow.next          # move 1 step
        fast = fast.next.next     # move 2 steps
        if slow is fast:
            return True
    return False

To find the entry node of the cycle (LeetCode 142): after a meeting, reset one pointer to head; move both 1 step until they meet again.

# Return node where the cycle begins; None if no cycle
from typing import Optional

def detectCycle(head: Optional[ListNode]) -> Optional[ListNode]:
    slow, fast = head, head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            # Found a meeting point; find entry
            ptr1, ptr2 = head, slow
            while ptr1 is not ptr2:
                ptr1 = ptr1.next
                ptr2 = ptr2.next
            return ptr1
    return None

7.3 LRU Cache (What it is and a Simple Implementation) — LeetCode 146

What it is: An LRU (Least Recently Used) Cache evicts the item that was used the longest time ago when capacity is full. Operations must be O(1) for get and put.

How it works: Combine

  • a hash map (key → node) for O(1) access, and

  • a doubly linked list to maintain recency order (most-recent at head, least-recent at tail).

7.3.1 Quick solution using collections.OrderedDict

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity: int):
        self.cap = capacity
        self.od = OrderedDict()

    def get(self, key: int) -> int:
        if key not in self.od:
            return -1
        self.od.move_to_end(key, last=False)  # mark as most-recent
        return self.od[key]

    def put(self, key: int, value: int) -> None:
        if key in self.od:
            self.od.move_to_end(key, last=False)
        self.od[key] = value
        if len(self.od) > self.cap:
            self.od.popitem(last=True)  # remove least-recent (tail)

7.3.2 Full design with a custom doubly linked list

class Node2:
    def __init__(self, k=0, v=0):
        self.k, self.v = k, v
        self.prev = None
        self.next = None

class LRUCacheDLL:
    def __init__(self, capacity: int):
        self.cap = capacity
        self.map = {}
        # sentinel head/tail to avoid edge cases
        self.head, self.tail = Node2(), Node2()
        self.head.next = self.tail
        self.tail.prev = self.head

    # helper: remove a node
    def _remove(self, node: Node2):
        p, n = node.prev, node.next
        p.next = n
        n.prev = p

    # helper: insert right after head (mark most-recent)
    def _add_front(self, node: Node2):
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

    def get(self, key: int) -> int:
        if key not in self.map:
            return -1
        node = self.map[key]
        self._remove(node)
        self._add_front(node)
        return node.v

    def put(self, key: int, value: int) -> None:
        if key in self.map:
            node = self.map[key]
            node.v = value
            self._remove(node)
            self._add_front(node)
        else:
            if len(self.map) == self.cap:
                # evict least-recent (node before tail)
                lru = self.tail.prev
                self._remove(lru)
                self.map.pop(lru.k)
            node = Node2(key, value)
            self.map[key] = node
            self._add_front(node)

✨ Conclusion

Linked Lists provide flexibility and efficiency for dynamic data storage. While arrays are better for random access, linked lists excel at insertion and deletion. Advanced problems like cycle detection and LRU cache design are common in interviews, making them essential topics for preparation.


🔗 References

0
Subscribe to my newsletter

Read articles from EJ Jung directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

EJ Jung
EJ Jung