zig-skills/references/std-linked-list.md

3.6 KiB

std.DoublyLinkedList / std.SinglyLinkedList

Intrusive linked lists for O(1) insertion/removal. Nodes are embedded in user structs via @fieldParentPtr.

When to Use

  • O(1) insertion/removal anywhere in list
  • Elements that need to be in multiple lists
  • Preallocated/arena-allocated nodes
  • No allocation on insert (nodes already exist)

DoublyLinkedList

Bidirectional traversal, O(1) removal of any node.

const std = @import("std");

const Item = struct {
    data: u32,
    node: std.DoublyLinkedList.Node = .{},  // embed node
};

var list: std.DoublyLinkedList = .{};

// Create items (you manage memory)
var a: Item = .{ .data = 1 };
var b: Item = .{ .data = 2 };
var c: Item = .{ .data = 3 };

// Insert
list.append(&a.node);         // add to end
list.prepend(&b.node);        // add to start
list.insertAfter(&a.node, &c.node);   // insert c after a
list.insertBefore(&a.node, &c.node);  // insert c before a

// Remove
list.remove(&a.node);         // O(1) remove specific node
const last = list.pop();      // remove and return last
const first = list.popFirst(); // remove and return first

// Get data from node
if (list.first) |node| {
    const item: *Item = @fieldParentPtr("node", node);
    std.debug.print("data: {}\n", .{item.data});
}

// Traverse forward
var it = list.first;
while (it) |node| : (it = node.next) {
    const item: *Item = @fieldParentPtr("node", node);
    // use item.data
}

// Traverse backward
var it = list.last;
while (it) |node| : (it = node.prev) {
    const item: *Item = @fieldParentPtr("node", node);
    // use item.data
}

// Concatenate (moves all from list2 to end of list1)
list1.concatByMoving(&list2);

// Length (O(n) - consider tracking separately)
const n = list.len();

SinglyLinkedList

Forward-only, minimal memory (one pointer per node).

const Item = struct {
    data: u32,
    node: std.SinglyLinkedList.Node = .{},
};

var list: std.SinglyLinkedList = .{};

var a: Item = .{ .data = 1 };
var b: Item = .{ .data = 2 };

// Insert (only at front or after existing node)
list.prepend(&a.node);         // add to front
a.node.insertAfter(&b.node);   // insert b after a

// Remove
const first = list.popFirst(); // remove and return first
_ = a.node.removeNext();       // remove node after a
list.remove(&b.node);          // O(n) - must find predecessor

// Traverse (forward only)
var it = list.first;
while (it) |node| : (it = node.next) {
    const item: *Item = @fieldParentPtr("node", node);
    // use item.data
}

// Find last (O(n))
if (list.first) |first| {
    const last = first.findLast();
}

// Reverse in place
std.SinglyLinkedList.Node.reverse(&list.first);

// Length (O(n))
const n = list.len();

Node Methods

// DoublyLinkedList.Node
node.prev  // ?*Node
node.next  // ?*Node

// SinglyLinkedList.Node
node.next           // ?*Node
node.insertAfter(new_node)
node.removeNext()   // ?*Node - removes and returns next
node.findLast()     // *Node
node.countChildren() // usize
node.reverse(&optional_ptr)

Common Pattern: LRU Cache

const Entry = struct {
    key: []const u8,
    value: Value,
    node: std.DoublyLinkedList.Node = .{},
};

var lru_list: std.DoublyLinkedList = .{};
var entries: std.StringHashMap(*Entry) = .init(allocator);

fn access(key: []const u8) ?*Entry {
    const entry = entries.get(key) orelse return null;
    // Move to front (most recently used)
    lru_list.remove(&entry.node);
    lru_list.prepend(&entry.node);
    return entry;
}

fn evictOldest() void {
    if (lru_list.pop()) |node| {
        const entry: *Entry = @fieldParentPtr("node", node);
        _ = entries.remove(entry.key);
        // free entry
    }
}