Algorithm Search⌘K

XOR linked list in Python

Doubly linked list storing the XOR of the two neighbour addresses in one field.

File
xor_linked_list.py, 120 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
xor_linked_list.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/xor-linked-list/. The authors are listed in prov/GE847R.jsonl.

from typing import Generic, TypeVar

T = TypeVar("T")


class XorLinkedList(Generic[T]):
    """A doubly linked list that keeps one link field per node: prev XOR next.

    Memory is simulated by the arrays key and np, indexed by integer
    addresses. Address 0 is nil and is never given to a node. For the nodes
    x_1, ..., x_n in list order, np[x_i] = x_(i-1) XOR x_(i+1), where x_0 and
    x_(n+1) are 0. push_front, push_back, pop_front, pop_back and reverse
    cost O(1); to_list costs Theta(n). Space: n keys and n link words.
    """

    def __init__(self) -> None:
        self.key: list[T | None] = [None]  # cell 0 is nil
        self.np: list[int] = [0]
        self.free: list[int] = []  # freed addresses, reused first
        self.head = 0
        self.tail = 0
        self.size = 0

    def __len__(self) -> int:
        return self.size

    def _allocate(self, k: T) -> int:
        if self.free:
            x = self.free.pop()
            self.key[x] = k
        else:
            x = len(self.key)
            self.key.append(k)
            self.np.append(0)
        return x

    def push_front(self, k: T) -> int:
        """Insert k at the front and return the address of its node. Cost O(1)."""
        x = self._allocate(k)  # @line L1
        self.np[x] = self.head  # @line L2: 0 XOR head
        if self.head != 0:  # @line L3
            self.np[self.head] ^= x
        else:
            self.tail = x  # @line L4
        self.head = x  # @line L5
        self.size += 1
        return x

    def pop_front(self) -> T:
        """Remove and return the front key. Precondition: the list is not empty. Cost O(1)."""
        if self.head == 0:
            raise IndexError("pop from an empty list")
        x = self.head  # @line L6
        nxt = self.np[x]
        if nxt != 0:  # @line L7
            self.np[nxt] ^= x
        else:
            self.tail = 0  # @line L8
        self.head = nxt  # @line L9
        k = self.key[x]  # @line L10
        self.key[x] = None
        self.np[x] = 0
        self.free.append(x)
        self.size -= 1
        return k  # type: ignore[return-value]

    def to_list(self) -> list[T]:
        """Return the keys from the front to the back. Cost Theta(n)."""
        out: list[T] = []
        prev, cur = 0, self.head  # @line L11
        while cur != 0:  # @line L12
            out.append(self.key[cur])  # type: ignore[arg-type]  # @line L13
            nxt = self.np[cur] ^ prev  # @line L14
            prev, cur = cur, nxt  # @line L15
        return out

    def reverse(self) -> None:
        """Reverse the list by swapping head and tail. Cost O(1)."""
        self.head, self.tail = self.tail, self.head

    def push_back(self, k: T) -> int:
        """Insert k at the back and return the address of its node. Cost O(1)."""
        self.reverse()
        x = self.push_front(k)
        self.reverse()
        return x

    def pop_back(self) -> T:
        """Remove and return the back key. Precondition: the list is not empty. Cost O(1)."""
        self.reverse()
        try:
            return self.pop_front()
        finally:
            self.reverse()

    def insert_between(self, p: int, q: int, k: T) -> int:
        """Insert k between the adjacent nodes p and q (p before q). Cost O(1)."""
        x = self._allocate(k)
        self.np[x] = p ^ q
        self.np[p] ^= q ^ x
        self.np[q] ^= p ^ x
        self.size += 1
        return x

    def check_invariant(self) -> None:
        """Check np[x_i] = x_(i-1) XOR x_(i+1) for each node, and the ends and the size."""
        order: list[int] = []
        prev, cur = 0, self.head
        while cur != 0:
            order.append(cur)
            prev, cur = cur, self.np[cur] ^ prev
            assert len(order) <= len(self.key), "cycle"
        assert len(order) == self.size
        assert (self.tail == order[-1]) if order else (self.head == 0 and self.tail == 0)
        ext = [0, *order, 0]
        for i in range(1, len(ext) - 1):
            assert self.np[ext[i]] == ext[i - 1] ^ ext[i + 1]
test_xor_linked_list.py 82 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/xor-linked-list/. The authors are listed in prov/GE847R.jsonl.

import random
from collections import deque

import pytest

from xor_linked_list import XorLinkedList


def test_example() -> None:
    xs: XorLinkedList[int] = XorLinkedList()
    assert xs.push_front(10) == 1
    assert xs.push_back(20) == 2
    assert xs.push_back(30) == 3
    assert xs.push_front(5) == 4
    assert xs.to_list() == [5, 10, 20, 30]
    assert [xs.np[a] for a in (4, 1, 2, 3)] == [1, 6, 2, 2]
    xs.check_invariant()
    assert xs.pop_front() == 5
    xs.reverse()
    assert xs.to_list() == [30, 20, 10]
    assert xs.pop_front() == 30
    assert xs.push_front(7) == 3  # the freed address 3 is reused
    assert xs.to_list() == [7, 20, 10]
    xs.check_invariant()


def test_edges() -> None:
    xs: XorLinkedList[int] = XorLinkedList()
    assert xs.to_list() == [] and len(xs) == 0
    with pytest.raises(IndexError):
        xs.pop_front()
    with pytest.raises(IndexError):
        xs.pop_back()
    xs.reverse()
    xs.check_invariant()
    xs.push_back(1)
    xs.reverse()
    assert xs.to_list() == [1]
    assert xs.pop_back() == 1
    assert xs.head == 0 and xs.tail == 0
    xs.check_invariant()


def test_insert_between() -> None:
    xs: XorLinkedList[str] = XorLinkedList()
    a = xs.push_back("a")
    c = xs.push_back("c")
    xs.insert_between(a, c, "b")
    assert xs.to_list() == ["a", "b", "c"]
    xs.check_invariant()
    xs.reverse()
    assert xs.to_list() == ["c", "b", "a"]
    xs.check_invariant()


def test_random_against_deque() -> None:
    rng = random.Random(4711)
    for _ in range(500):
        xs: XorLinkedList[int] = XorLinkedList()
        model: deque[int] = deque()
        for _ in range(rng.randint(0, 60)):
            op = rng.randrange(5)
            v = rng.randint(-10**18, 10**18)
            if op == 0:
                xs.push_front(v)
                model.appendleft(v)
            elif op == 1:
                xs.push_back(v)
                model.append(v)
            elif op == 2 and model:
                assert xs.pop_front() == model.popleft()
            elif op == 3 and model:
                assert xs.pop_back() == model.pop()
            elif op == 4:
                xs.reverse()
                model.reverse()
            xs.check_invariant()
            assert len(xs) == len(model)
        assert xs.to_list() == list(model)

What the next level needs

  • the tests did not run in this build

Read the full page about XOR linked list