From 24f2cb7cfc72e6cf11aae1a083ea728e670a1c33 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 2 Feb 2026 13:31:52 +0300 Subject: [PATCH] add a pairing heap implementation --- src/heap.zig | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/heap.zig diff --git a/src/heap.zig b/src/heap.zig new file mode 100644 index 00000000..26d61d2f --- /dev/null +++ b/src/heap.zig @@ -0,0 +1,180 @@ +//! https://github.com/mitchellh/libxev/blob/main/src/heap.zig + +const std = @import("std"); + +const lp = @import("lightpanda"); + +/// An intrusive heap implementation backed by a pairing heap[1] implementation. +/// +/// Why? Intrusive data structures require the element type to hold the metadata +/// required for the structure, rather than an additional container structure. +/// There are numerous pros/cons that are documented well by Boost[2]. For Zig, +/// I think the primary benefits are making data structures allocation free +/// (rather, shifting allocation up to the consumer which can choose how they +/// want the memory to be available). There are various costs to this such as +/// the costs of pointer chasing, larger memory overhead, requiring the element +/// type to be aware of its container, etc. But for certain use cases an intrusive +/// data structure can yield much better performance. +/// +/// Usage notes: +/// - The element T is expected to have a field "heap" of type InstrusiveHeapField. +/// See the tests for a full example of how to set this. +/// - You can easily make this a min or max heap by inverting the result of +/// "less" below. +/// +/// [1]: https://en.wikipedia.org/wiki/Pairing_heap +/// [2]: https://www.boost.org/doc/libs/1_64_0/doc/html/intrusive/intrusive_vs_nontrusive.html +pub fn Intrusive( + comptime T: type, + comptime Context: type, + comptime less: *const fn (ctx: Context, a: *T, b: *T) bool, +) type { + lp.assert(@FieldType(T, "heap") == IntrusiveField(T), "heap: unexpected type", .{ .type = @typeName(T) }); + + return struct { + const Self = @This(); + + root: ?*T = null, + context: Context, + + /// Insert a new element v into the heap. An element v can only + /// be a member of a single heap at any given time. When compiled + /// with runtime-safety, assertions will help verify this property. + pub fn insert(self: *Self, v: *T) void { + self.root = if (self.root) |root| self.meld(v, root) else v; + } + + /// Look at the next minimum value but do not remove it. + pub fn peek(self: *Self) ?*T { + return self.root; + } + + /// Delete the minimum value from the heap and return it. + pub fn deleteMin(self: *Self) ?*T { + const root = self.root orelse return null; + self.root = if (root.heap.child) |child| + self.combine_siblings(child) + else + null; + + // Clear pointers with runtime safety so we can verify on + // insert that values aren't incorrectly being set multiple times. + root.heap = .{}; + + return root; + } + + /// Remove the value v from the heap. + pub fn remove(self: *Self, v: *T) void { + // If v doesn't have a previous value, this must be the root + // element. If it is NOT the root element, v can't be in this + // heap and we trigger an assertion failure. + const prev = v.heap.prev orelse { + lp.assert(self.root.? == v, "heap: remove", .{}); + _ = self.deleteMin(); + return; + }; + + // Detach "v" from the tree and clean up any links so it + // is as if this node never nexisted. The previous value + // must point to the proper next value and the pointers + // must all be cleaned up. + if (v.heap.next) |next| next.heap.prev = prev; + if (prev.heap.child == v) + prev.heap.child = v.heap.next + else + prev.heap.next = v.heap.next; + v.heap.prev = null; + v.heap.next = null; + + // If we have children, then we need to merge them back in. + const child = v.heap.child orelse return; + v.heap.child = null; + const x = self.combine_siblings(child); + self.root = self.meld(x, self.root.?); + } + + /// Meld (union) two heaps together. This isn't a generalized + /// union. It assumes that a.heap.next is null so this is only + /// meant in specific scenarios in the pairing heap where meld + /// is expected. + /// + /// For example, when melding a new value "v" with an existing + /// root "root", "v" must always be the first param. + fn meld(self: *Self, a: *T, b: *T) *T { + lp.assert(a.heap.next == null, "heap: meld", .{}); + + if (less(self.context, a, b)) { + // B points back to A + b.heap.prev = a; + + // If B has siblings, then A inherits B's siblings + // and B's immediate sibling must point back to A to + // maintain the doubly linked list. + if (b.heap.next) |b_next| { + a.heap.next = b_next; + b_next.heap.prev = a; + b.heap.next = null; + } + + // If A has a child, then B becomes the leftmost sibling + // of that child. + if (a.heap.child) |a_child| { + b.heap.next = a_child; + a_child.heap.prev = b; + } + + // B becomes the leftmost child of A + a.heap.child = b; + + return a; + } + + // Replace A with B in the tree. Any of B's children + // become siblings of A. A becomes the leftmost child of B. + // A points back to B + b.heap.prev = a.heap.prev; + a.heap.prev = b; + if (b.heap.child) |b_child| { + a.heap.next = b_child; + b_child.heap.prev = a; + } + b.heap.child = a; + return b; + } + + /// Combine the siblings of the leftmost value "left" into a single + /// new rooted with the minimum value. + fn combine_siblings(self: *Self, left: *T) *T { + left.heap.prev = null; + + // Merge pairs right + var root: *T = root: { + var a: *T = left; + while (true) { + var b = a.heap.next orelse break :root a; + a.heap.next = null; + b = self.meld(a, b); + a = b.heap.next orelse break :root b; + } + }; + + // Merge pairs left + while (true) { + var b = root.heap.prev orelse return root; + b.heap.next = null; + root = self.meld(b, root); + } + } + }; +} + +/// The state that is required for IntrusiveHeap element types. This +/// should be set as the "heap" field in the type T. +pub fn IntrusiveField(comptime T: type) type { + return struct { + child: ?*T = null, + prev: ?*T = null, + next: ?*T = null, + }; +}