Skip to content

Merge Linked Lists & LRU Cache ​

Advanced linked list applications


Visual Guides ​

Merge Two Sorted Lists ​

Merge Lists

LRU Cache Structure ​

LRU Cache


Merge Two Sorted Lists ​

Problem Statement ​

Given the heads of two sorted linked lists, merge them into one sorted list by splicing together the nodes of the first two lists.

Solution ​

python
def mergeTwoLists(l1, l2):
    dummy = ListNode(0)
    curr = dummy

    while l1 and l2:
        if l1.val <= l2.val:
            curr.next = l1
            l1 = l1.next
        else:
            curr.next = l2
            l2 = l2.next
        curr = curr.next

    curr.next = l1 or l2
    return dummy.next
java
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;

    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            curr.next = l1;
            l1 = l1.next;
        } else {
            curr.next = l2;
            l2 = l2.next;
        }
        curr = curr.next;
    }

    curr.next = (l1 != null) ? l1 : l2;
    return dummy.next;
}

Complexity: Time O(n + m) · Space O(1)

  • Time: O(n + m) because we traverse each node in both lists exactly once, comparing and linking at each step
  • Space: O(1) because we only use a constant number of pointers (dummy, curr) and rearrange existing nodes in-place

Complexity ​

  • Time: O(n + m) where n and m are the lengths of the two lists
  • Space: O(1) - only pointer manipulation, no extra space

Merge K Sorted Lists ​

Problem Statement ​

You are given an array of k linked lists, where each linked list is sorted in ascending order. Merge all the linked lists into one sorted linked list and return its head.

Example:

  • Input: lists = [[1,4,5], [1,3,4], [2,6]]
  • Output: [1,1,2,3,4,4,5,6]

Approach 1: Divide and Conquer ​

This approach uses the Merge Sort paradigm - divide the problem into smaller units, solve them, then merge the results.

How it works:

  1. Split k lists into two halves: lists[0...mid] and lists[mid+1...end]
  2. Recursively merge the left half to get first sorted list
  3. Recursively merge the right half to get second sorted list
  4. Merge the two sorted lists using two-pointer approach
python
def mergeKLists(lists: list) -> ListNode:
    if not lists:
        return None

    while len(lists) > 1:
        merged = []
        for i in range(0, len(lists), 2):
            l1 = lists[i]
            l2 = lists[i + 1] if i + 1 < len(lists) else None
            merged.append(mergeTwoLists(l1, l2))
        lists = merged

    return lists[0]
java
public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) return null;

    List<ListNode> listArr = new ArrayList<>(Arrays.asList(lists));

    while (listArr.size() > 1) {
        List<ListNode> merged = new ArrayList<>();
        for (int i = 0; i < listArr.size(); i += 2) {
            ListNode l1 = listArr.get(i);
            ListNode l2 = (i + 1 < listArr.size()) ? listArr.get(i + 1) : null;
            merged.add(mergeTwoLists(l1, l2));
        }
        listArr = merged;
    }

    return listArr.get(0);
}

Complexity: Time O(N log k) · Space O(1)

  • Time: O(N log k) where N is total nodes across all lists; we perform log k merge rounds, each processing all N nodes
  • Space: O(1) because we only store the merged list array and rearrange pointers in-place (not counting output)

Approach 2: Min Heap ​

A min heap (priority queue) keeps the smallest node at the top, allowing efficient extraction of the next smallest element across all lists.

How it works:

  1. Push the head of each non-null linked list into the heap
  2. Pop the smallest node, append to result list
  3. If the popped node has a next, push that next node into the heap
  4. Repeat until heap is empty
python
import heapq

def mergeKLists_heap(lists: list) -> ListNode:
    heap = []

    # Add first node from each list
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst.val, i, lst))

    dummy = ListNode(0)
    curr = dummy

    while heap:
        val, i, node = heapq.heappop(heap)
        curr.next = node
        curr = curr.next

        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))

    return dummy.next

Complexity: Time O(N log k) · Space O(k)

  • Time: O(N log k) because each of the N nodes is pushed and popped from the heap once, and heap operations are O(log k)
  • Space: O(k) because the heap maintains at most k nodes (one from each list) at any time

Complexity Comparison ​

ApproachTime ComplexitySpace Complexity
Divide & ConquerO(N log k)O(1)
Min HeapO(N log k)O(k)

Where N is the total number of nodes across all lists, and k is the number of lists.


LRU Cache ​

Problem Statement ​

Design a data structure that follows the constraints of a Least Recently Used (LRU) cache with O(1) average time complexity for both get and put operations.

Operations:

  • LRUCache(int capacity) - Initialize the cache with positive size capacity
  • int get(int key) - Return the value if key exists, otherwise return -1
  • void put(int key, int value) - Update value if key exists, otherwise add the key-value pair. If capacity is exceeded, evict the least recently used key.

Approach ​

The optimal solution combines a Hash Map and a Doubly Linked List:

  • Hash Map: O(1) lookup of key to node location
  • Doubly Linked List: Maintains access order - most recently used at head, least recently used at tail

Key Insight: When accessing a node, remove it from its current position and reinsert at the head. The tail always contains the LRU item for eviction.

Mermaid Diagram ​

Solution ​

python
class DLLNode:
    def __init__(self, key=0, val=0):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = {}  # key -> node

        # Dummy head and tail
        self.head = DLLNode()
        self.tail = DLLNode()
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_to_front(self, node):
        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.cache:
            return -1

        node = self.cache[key]
        self._remove(node)
        self._add_to_front(node)
        return node.val

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self._remove(self.cache[key])

        node = DLLNode(key, value)
        self.cache[key] = node
        self._add_to_front(node)

        if len(self.cache) > self.capacity:
            # Remove LRU (node before tail)
            lru = self.tail.prev
            self._remove(lru)
            del self.cache[lru.key]
java
class LRUCache {
    private static class DLLNode {
        int key, value;
        DLLNode prev, next;
        DLLNode() {}
        DLLNode(int key, int value) { this.key = key; this.value = value; }
    }

    private final int capacity;
    private final Map<Integer, DLLNode> cache = new HashMap<>();
    private final DLLNode head = new DLLNode(), tail = new DLLNode();

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head.next = tail;
        tail.prev = head;
    }

    private void remove(DLLNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void addToFront(DLLNode node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    public int get(int key) {
        if (!cache.containsKey(key)) return -1;
        DLLNode node = cache.get(key);
        remove(node);
        addToFront(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (cache.containsKey(key)) {
            DLLNode node = cache.get(key);
            node.value = value;
            remove(node);
            addToFront(node);
        } else {
            DLLNode node = new DLLNode(key, value);
            cache.put(key, node);
            addToFront(node);
            if (cache.size() > capacity) {
                DLLNode lru = tail.prev;
                remove(lru);
                cache.remove(lru.key);
            }
        }
    }
}

Complexity: Time O(1) for get/put · Space O(capacity)

  • Time: O(1) for both operations because hash map provides O(1) lookup, and doubly linked list provides O(1) insertion/deletion
  • Space: O(capacity) because we store at most capacity nodes in the hash map and doubly linked list

Using OrderedDict (Simpler) ​

Python's OrderedDict maintains insertion order and provides move_to_end() for O(1) reordering.

python
from collections import OrderedDict

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

    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)

Complexity: Time O(1) for get/put · Space O(capacity)

  • Time: O(1) because OrderedDict's move_to_end() and popitem() are O(1) operations in Python 3.7+
  • Space: O(capacity) because the OrderedDict stores at most capacity key-value pairs

Complexity ​

  • Time: O(1) for both get and put
  • Space: O(capacity)

Interview Applications ​

These problems are frequently asked in technical interviews because they test:

Merge K Sorted Lists ​

  • System Design: Merging sorted data streams in distributed systems
  • External Sorting: Merging sorted chunks that don't fit in memory
  • Real-world: Merging search results from multiple servers
  • Follow-ups: What if lists are on different machines? Handle duplicates?

LRU Cache ​

  • Memory Management: Page replacement in operating systems
  • Database Systems: Buffer pool management
  • Web Services: API response caching, session management
  • CDN Design: Content caching at edge servers
  • Follow-ups:
    • How to make it thread-safe?
    • How to handle distributed LRU cache?
    • What about TTL (time-to-live) for entries?

Common Interview Variations ​

ProblemVariationKey Difference
Merge K ListsMerge K Sorted ArraysUse arrays instead of linked lists
LRU CacheLFU CacheEvict least frequently used
LRU CacheTTL CacheEntries expire after time limit
LRU CacheMulti-level CacheL1/L2 cache hierarchy

Sources ​