Zig Linked Lists

A linked list is a collection where each element holds its value and a pointer to the next element. Unlike arrays that store everything in a continuous block of memory, linked list nodes can live anywhere in memory and connect through pointers. This makes inserting or removing elements at any position fast — no shifting required.

The Concept

  Array:
  [10][20][30][40][50]   ← all together, fixed positions

  Linked list:
  [10|→] → [20|→] → [30|→] → [40|→] → [50|null]
   head                                  tail

  Each box = one node: value + pointer to next
  Last node points to null (end of list)

Building a Singly Linked List

const std = @import("std");

fn LinkedList(comptime T: type) type {
    return struct {
        const Node = struct {
            value: T,
            next:  ?*Node = null,
        };

        head:      ?*Node = null,
        allocator: std.mem.Allocator,
        len:       usize = 0,

        const Self = @This();

        fn init(allocator: std.mem.Allocator) Self {
            return .{ .allocator = allocator };
        }

        fn deinit(self: *Self) void {
            var current = self.head;
            while (current) |node| {
                const next = node.next;
                self.allocator.destroy(node);
                current = next;
            }
        }

        fn prepend(self: *Self, value: T) !void {
            const node = try self.allocator.create(Node);
            node.* = .{ .value = value, .next = self.head };
            self.head = node;
            self.len += 1;
        }

        fn append(self: *Self, value: T) !void {
            const node = try self.allocator.create(Node);
            node.* = .{ .value = value, .next = null };
            if (self.head == null) {
                self.head = node;
            } else {
                var current = self.head.?;
                while (current.next) |next| current = next;
                current.next = node;
            }
            self.len += 1;
        }

        fn printAll(self: *Self) void {
            var current = self.head;
            while (current) |node| {
                std.debug.print("{any} → ", .{node.value});
                current = node.next;
            }
            std.debug.print("null\n", .{});
        }
    };
}

Using the Linked List

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

    var list = LinkedList(i32).init(alloc);
    defer list.deinit();

    try list.append(10);
    try list.append(20);
    try list.append(30);
    try list.prepend(5);

    list.printAll();
    std.debug.print("Length: {d}\n", .{list.len});
}

Output:

5 → 10 → 20 → 30 → null
Length: 4

Insertion and Removal

        // Insert after a specific value
        fn insertAfter(self: *Self, target: T, value: T) !bool {
            var current = self.head;
            while (current) |node| {
                if (node.value == target) {
                    const new_node = try self.allocator.create(Node);
                    new_node.* = .{ .value = value, .next = node.next };
                    node.next = new_node;
                    self.len += 1;
                    return true;
                }
                current = node.next;
            }
            return false;
        }

        // Remove the first node with a specific value
        fn remove(self: *Self, value: T) bool {
            if (self.head == null) return false;
            if (self.head.?.value == value) {
                const old = self.head.?;
                self.head = old.next;
                self.allocator.destroy(old);
                self.len -= 1;
                return true;
            }
            var current = self.head.?;
            while (current.next) |next| {
                if (next.value == value) {
                    current.next = next.next;
                    self.allocator.destroy(next);
                    self.len -= 1;
                    return true;
                }
                current = next;
            }
            return false;
        }
  Insert 15 after 10:
  Before: 5 → 10 → 20 → 30 → null
  After:  5 → 10 → 15 → 20 → 30 → null

  Remove 20:
  Before: 5 → 10 → 15 → 20 → 30 → null
          skip 10 → 15 → link 15 to 30 → free 20
  After:  5 → 10 → 15 → 30 → null

The Standard Library SinglyLinkedList

Zig's standard library provides std.SinglyLinkedList for production use. It uses an intrusive design — the node is embedded inside your own struct, avoiding a separate allocation per node:

const std = @import("std");

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

    const List = std.SinglyLinkedList(u32);
    var list = List{};

    var n1 = try alloc.create(List.Node);
    var n2 = try alloc.create(List.Node);
    var n3 = try alloc.create(List.Node);
    defer alloc.destroy(n1);
    defer alloc.destroy(n2);
    defer alloc.destroy(n3);

    n1.* = .{ .data = 100 };
    n2.* = .{ .data = 200 };
    n3.* = .{ .data = 300 };

    list.prepend(n3);
    list.prepend(n2);
    list.prepend(n1);

    var it = list.first;
    while (it) |node| {
        std.debug.print("{d}\n", .{node.data});
        it = node.next;
    }
}

Array vs Linked List Trade-offs

  Operation         │ Array ([]T)   │ Linked List
  ──────────────────┼───────────────┼─────────────────
  Access by index   │ O(1) fast     │ O(n) slow
  Append to end     │ O(1) amortized│ O(n) (or O(1) with tail ptr)
  Insert at front   │ O(n) shift    │ O(1) fast
  Insert at middle  │ O(n) shift    │ O(1) once found
  Remove at front   │ O(n) shift    │ O(1) fast
  Memory use        │ Compact       │ Extra pointer per node
  Cache friendliness│ Excellent     │ Poor (scattered memory)

Use a linked list when you frequently insert or remove from the front or middle of a list, and access by index is rare. Use an array or std.ArrayList when you need fast random access and mostly append to the end. In practice, arrays outperform linked lists in most real workloads because of CPU cache locality — array elements sit next to each other in memory, so reading one often preloads nearby elements automatically.

Leave a Comment

Your email address will not be published. Required fields are marked *