liveBook · Manning

Doubly linked list

As explained in Grokking Data Structures

A doubly linked list is a linked list in which each node contains links to both its previous and next nodes, supporting traversal in both directions.

When going back matters

Imagine a game in which a player moves through rooms in order. A singly linked list can point from each room to the next, but the player also needs to retrace their steps. A doubly linked list supplies the links needed to move in either direction. The same need arises when moving through document layers or undoing and redoing actions. section 6.16

A doubly linked list is made of nodes. Each node holds an element, a link to the next node, and a link to the previous node. Starting at the head, you can follow next links to the tail; starting at the tail, you can follow previous links back. A list wrapper can keep links to both ends. Unlike an array, though, a linked list cannot jump directly to an arbitrary index. section 6.15 section 6.14

class Node:
    def __init__(self, data):
        self._data = data
        self._next = None
        self._prev = None
        
    def data(self):
        return self._data

    def next(self):
        return self._next

    def has_next(self):
        return self._next is not None

    def append(self, next_node):
        self._next = next_node
        if next_node is not None:
            next_node._prev = self

    def prev(self):
        return self._prev

    def has_prev(self):
        return self._prev is not None
        
    def prepend(self, prev_node):
        self._prev = prev_node
        if prev_node is not None:
            prev_node._next = self

The Python node implementation shows the two links. Its append and prepend methods update the adjoining node’s link too, so the connection works in both directions. section 6.15

The extra link helps with some operations, but it does not make every operation faster:

  • Insert at either end in constant time when the wrapper tracks the head and tail. Inserting between known neighboring nodes also takes constant time; finding an unknown position still requires traversal. section 6.17
  • Search by value one node at a time. Following links backward is possible, but without a reason to start near the tail, it does not improve a general search. section 6.18
  • Delete a found node by linking its neighbors around it, updating the head or tail when necessary. Deleting by value remains O(n) because finding the node can require a full search. section 6.19
  • Join two nonempty lists by linking the first tail to the second head. This concatenation takes constant time, unlike merging arrays by allocating a larger array and moving their elements. section 6.20
def delete(self, target):
    node = self._search(target)
    if node is None:
        raise ValueError(f'{target} not found in the list.')
    if node.prev() is None:            #1
        self._head = node.next()
        if self._head is None:         #2
            self._tail = None
        else:
            self._head.prepend(None)
    elif node.next() is None:          #3
        self._tail = node.prev()
        self._tail.append(None)
    else:                              #4
        node.prev().append(node.next())
        del node
#1 Delete the node at the beginning of the list.
#2 In this case, the list’s head was the only element in the list.
#3 Delete the node at the end of the list.
#4 General case

The delete method separates head, tail and middle cases. Notice the one-node case: removing the head must leave both wrapper links empty. section 6.19

Choose a doubly linked list when moving backward is important or when you often work at both ends. The tradeoff is an extra link in every node and more links to maintain after changes. If you only need forward movement, a singly linked list is simpler; if you need direct indexed access, the extra link will not provide it. section 6.15 section 6.24

How Grokking Data Structures covers it

How deep it goes
Works through Python node links, insertion, search, deletion and concatenation, including edge cases and running-time tradeoffs.
Examples
Uses a room-navigation game, document layers, undo/redo actions and ordered task lists to illustrate backward movement and concatenation.
What it assumes
Builds on singly linked lists and arrays while defining the added predecessor link and showing how Python nodes store it.
What you can do after
Build it from scratch with Python nodes and head–tail links, then choose it when bidirectional traversal justifies extra storage and maintenance.
How it connects
Connects singly linked lists, arrays, circular linked lists and undo/redo through traversal, access and storage tradeoffs.

Also explained in

Compare how 2 books explain doubly linked list →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Grokking Data Structures. The section links point to the parts of the book it draws on.