Skip to main content
Zig’s standard library provides both doubly and singly linked list implementations.

DoublyLinkedList

pub fn DoublyLinkedList(comptime T: type) type
A doubly-linked list where each node has pointers to both the next and previous nodes.

Node Structure

pub const Node = struct {
    prev: ?*Node,
    next: ?*Node,
    data: T,
};
prev
?*Node
Pointer to the previous node, or null if this is the first node.
next
?*Node
Pointer to the next node, or null if this is the last node.
data
T
The stored value.

List Structure

pub const DoublyLinkedList(T) = struct {
    first: ?*Node,
    last: ?*Node,
    len: usize,
};

Methods

prepend

pub fn prepend(list: *DoublyLinkedList(T), node: *Node) void
Inserts a node at the beginning of the list.

append

pub fn append(list: *DoublyLinkedList(T), node: *Node) void
Inserts a node at the end of the list.

insertAfter

pub fn insertAfter(list: *DoublyLinkedList(T), node: *Node, new_node: *Node) void
Inserts new_node after the specified node.

insertBefore

pub fn insertBefore(list: *DoublyLinkedList(T), node: *Node, new_node: *Node) void
Inserts new_node before the specified node.

remove

pub fn remove(list: *DoublyLinkedList(T), node: *Node) void
Removes a node from the list.

pop

pub fn pop(list: *DoublyLinkedList(T)) ?*Node
Removes and returns the last node, or null if empty.

popFirst

pub fn popFirst(list: *DoublyLinkedList(T)) ?*Node
Removes and returns the first node, or null if empty.

Usage Example

const std = @import("std");
const DoublyLinkedList = std.DoublyLinkedList;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var list = DoublyLinkedList(i32){};

    // Create and add nodes
    var node1 = try allocator.create(DoublyLinkedList(i32).Node);
    node1.data = 1;
    list.append(node1);

    var node2 = try allocator.create(DoublyLinkedList(i32).Node);
    node2.data = 2;
    list.append(node2);

    var node3 = try allocator.create(DoublyLinkedList(i32).Node);
    node3.data = 3;
    list.prepend(node3);

    // Iterate forward
    var it = list.first;
    while (it) |node| : (it = node.next) {
        std.debug.print("{} ", .{node.data});
    }
    // Output: 3 1 2

    // Iterate backward
    it = list.last;
    while (it) |node| : (it = node.prev) {
        std.debug.print("{} ", .{node.data});
    }
    // Output: 2 1 3

    // Clean up
    while (list.popFirst()) |node| {
        allocator.destroy(node);
    }
}

SinglyLinkedList

pub fn SinglyLinkedList(comptime T: type) type
A singly-linked list where each node only has a pointer to the next node.

Node Structure

pub const Node = struct {
    next: ?*Node,
    data: T,
};
next
?*Node
Pointer to the next node, or null if this is the last node.
data
T
The stored value.

List Structure

pub const SinglyLinkedList(T) = struct {
    first: ?*Node,
};

Methods

prepend

pub fn prepend(list: *SinglyLinkedList(T), node: *Node) void
Inserts a node at the beginning of the list. O(1) operation.

popFirst

pub fn popFirst(list: *SinglyLinkedList(T)) ?*Node
Removes and returns the first node, or null if empty. O(1) operation.

insertAfter

pub fn insertAfter(list: *SinglyLinkedList(T), node: *Node, new_node: *Node) void
Inserts new_node after the specified node. O(1) operation.

removeAfter

pub fn removeAfter(list: *SinglyLinkedList(T), node: *Node) ?*Node
Removes and returns the node after node, or null if there is none. O(1) operation.

findFirst

pub fn findFirst(list: SinglyLinkedList(T)) ?*Node
Returns the first node, or null if empty.

Usage Example

const std = @import("std");
const SinglyLinkedList = std.SinglyLinkedList;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var list = SinglyLinkedList(i32){};

    // Create and add nodes
    var node1 = try allocator.create(SinglyLinkedList(i32).Node);
    node1.data = 1;
    list.prepend(node1);

    var node2 = try allocator.create(SinglyLinkedList(i32).Node);
    node2.data = 2;
    list.prepend(node2);

    // Iterate
    var it = list.first;
    while (it) |node| : (it = node.next) {
        std.debug.print("{} ", .{node.data});
    }
    // Output: 2 1

    // Clean up
    while (list.popFirst()) |node| {
        allocator.destroy(node);
    }
}

When to Use

DoublyLinkedList

✓ Need to traverse in both directions
✓ Need to remove nodes efficiently from anywhere in the list
✓ Implementing a deque or LRU cache

SinglyLinkedList

✓ Only need forward traversal
✓ Memory is constrained (smaller node size)
✓ Implementing a stack or queue
✓ Need simple LIFO ordering

Performance

OperationDoublyLinkedListSinglyLinkedList
PrependO(1)O(1)
AppendO(1)O(n)
Insert AfterO(1)O(1)
RemoveO(1)O(n)
Access by indexO(n)O(n)
Memory per node2 pointers + data1 pointer + data
For most use cases requiring a dynamic collection, consider ArrayList which provides better cache locality and performance.
  • ArrayList - Contiguous dynamic array (usually preferred)
  • HashMap - Hash-based key-value storage

Build docs developers (and LLMs) love