Skip to content

Both Ends Have to Remember

For once, I washed a dish and slid it carefully into place—plate ← plate → plate—a small arrangement I would later recognize in a doubly linked list, just before the rack began to tip. That extra memory makes operations at both ends efficient, but it also doubles the number of relationships I have to preserve whenever the list changes.


Today’s Codewars problem asked me to implement a doubly linked list with an array-like interface: push, pop, shift, and unshift.

The names make the operations sound familiar. The real work is underneath them.

In a singly linked list, each node holds a value and a pointer to the next node. A doubly linked node also knows which node came before it:

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

The list itself keeps references to both ends:

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

That gives the structure a useful symmetry:

None ← head: A ⇄ B ⇄ C :tail → None

The head has no previous node. The tail has no next node. Every node in between has a valid neighbor in both directions.

Those relationships are the list’s invariants. The methods work when they preserve those rules after every change.

Adding at Either End

unshift inserts at the front. If the list is empty, the new node becomes both the head and the tail. Otherwise, the new node points forward to the old head, the old head points backward to the new node, and the head reference moves.

def unshift(self, value):
    new_node = Node(value)

    if self.head is None:
        self.head = new_node
        self.tail = new_node
        return

    new_node.next = self.head
    self.head.prev = new_node
    self.head = new_node

push is the mirror image at the back:

def push(self, value):
    new_node = Node(value)

    if self.tail is None:
        self.head = new_node
        self.tail = new_node
        return

    new_node.prev = self.tail
    self.tail.next = new_node
    self.tail = new_node

This is one of the benefits of storing both head and tail. Adding to either end takes constant time. I do not have to start at the head and traverse the entire list just to find the back.

Removing Means Moving the Boundary

My insertion logic was mostly right. The larger problem was in shift and pop.

Removing a node is not only a matter of finding its value or deleting a variable. The list’s boundary has to move. shift must update self.head, while pop must update self.tail. Both methods also need to return the value they removed.

def shift(self):
    removed_node = self.head
    removed_value = removed_node.value

    if self.head is self.tail:
        self.head = None
        self.tail = None
    else:
        self.head = removed_node.next
        self.head.prev = None
        removed_node.next = None

    return removed_value

The one-node list is the important special case. Its only node is simultaneously the head and the tail. Removing it means both references must become None.

For a longer list, the second node becomes the head. Its prev pointer must be cleared because nothing comes before the head anymore.

pop performs the same operation from the other direction:

def pop(self):
    removed_node = self.tail
    removed_value = removed_node.value

    if self.head is self.tail:
        self.head = None
        self.tail = None
    else:
        self.tail = removed_node.prev
        self.tail.next = None
        removed_node.prev = None

    return removed_value

Because the tail already has a pointer to its predecessor, pop does not need to traverse the list. That is the practical advantage of the backward link.

There is also no need to use del on the removed node. Once the list no longer holds references to it, Python’s garbage collector can reclaim it. Clearing the detached link makes the separation explicit, but changing the list’s live references is what actually removes the node from the structure.

Python Already Has a Double-Ended Queue

There is an extremely compact way to satisfy the behavioral requirements:

from collections import deque


class DoublyLinkedList(deque):
    push, shift, unshift = deque.append, deque.popleft, deque.appendleft

deque, pronounced “deck,” is Python’s built-in double-ended queue. It efficiently adds and removes values from both sides. The assignments simply give its existing methods the names the exercise expects:

push(value)    → append(value)
shift()        → popleft()
unshift(value) → appendleft(value)
pop()          → inherited pop()

It is clever, valid Python, and probably the implementation I would prefer if I only needed the behavior in an application.

It also avoids the point of this particular lesson.

If the tests only call the four methods and inspect their returned values, the wrapper may pass. If they expect nodes with next and prev pointers or inspect head and tail, it will fail. More importantly, delegating everything to deque means I do not practice maintaining the structure myself.

The shortest solution and the most useful learning exercise are not always the same solution.

Closing the Loop

We have been promised a circular linked-list problem this afternoon. The easiest way to think about it is as a small change to the invariants I already know.

Instead of the ends pointing to None, they point to each other:

self.tail.next = self.head
self.head.prev = self.tail
head → A ⇄ B ⇄ C ← tail
       ↑         ↓
       └─────────┘

An empty list still has no head or tail. In a one-node list, the node’s next and prev pointers both point back to itself. In a larger list, every insertion or removal has to restore the connection between the head and tail.

That changes traversal too. This loop would never finish:

while current:
    current = current.next

There is no None waiting at the end of a circular list. Traversal has to stop when it returns to the starting node:

def values(self):
    if self.head is None:
        return []

    result = []
    current = self.head

    while True:
        result.append(current.value)
        current = current.next

        if current is self.head:
            break

    return result

That is the recurring lesson with linked lists: the code only makes sense in relation to the structure’s promises. A normal doubly linked list promises that its ends point to None. A circular one promises that its ends point back into the list. Once I know which promise I am maintaining, each pointer assignment has a reason.

Code Platoon - Week 13 - Day 60 - Reflection

Today was the first day that I spent a significant amount of time socializing with my team. Usually it’s cameras off all day until standdown. But we had some good discussions about struggles with the project, general gripes with our various projects, and just overall wellness chickens with each other.

I think we’re all over the project and the course at this point. Going this hard to learn full stack development for three months straight is brutal. And for many of us, life is increasingly placing heavy demands on our time and attention. It’s becoming more and more difficult to stay engaged.

So today was a necessary pressure release valve. Tomorrow the personal project is due. Since I completely finished, I considered each team member’s relative strengths and proposed roles for the final group project. Most seemed to agree with the ideas. Or perhaps they’re too exhausted to protest? It’s probably some of both.

Many team members haven’t been present for the last few days. Hopefully they’re able to finish with us.