From 935bccfff5e4151ca7e9109dfba0a9e279b2e507 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Wed, 8 Oct 2025 23:05:26 +0200 Subject: [PATCH 1/6] The base commit --- README.md | 33 ++++++++++++++------------------- build.zig.zon | 2 +- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a55c66f..4d65ba0 100644 --- a/README.md +++ b/README.md @@ -24,25 +24,20 @@ A collection of data structures that keep data in order in pure Zig Ordered is a Zig library that provides fast and efficient implementations of various popular data structures including B-tree, skip list, trie, and red-black tree for Zig programming language. -### Supported Data Structures - -Currently supported data structures include: - -- [B-tree](src/ordered/btree_map.zig): A self-balancing search tree where nodes can have many children. -- [Sorted Set](src/ordered/sorted_set.zig): A data structure that stores a collection of unique elements in a consistently sorted order. -- [Skip List](src/ordered/skip_list.zig): A probabilistic data structure that uses multiple linked lists to create "express lanes" for fast, tree-like search. -- [Trie](src/ordered/trie.zig): A tree where paths from the root represent prefixes which makes it extremely fast for tasks like text autocomplete. -- [Red-black Tree](src/ordered/red_black_tree.zig): A self-balancing binary search tree that uses node colors to guarantee efficient operations. -- [Cartesian Tree](src/ordered/cartesian_tree.zig): A binary tree that uniquely combines a binary search tree property for its keys with a heap** property for its values. - -| # | Data Structure | Build Complexity | Memory Complexity | Search Complexity | -|---|----------------|------------------|-------------------|----------------------| -| 1 | B-tree | $O(\log n)$ | $O(n)$ | $O(\log n)$ | -| 2 | Cartesian tree | $O(\log n)$\* | $O(n)$ | $O(\log n)$\* | -| 3 | Red-black tree | $O(\log n)$ | $O(n)$ | $O(\log n)$ | -| 4 | Skip list | $O(\log n)$\* | $O(n)$ | $O(\log n)$\* | -| 5 | Sorted set | $O(n)$ | $O(n)$ | $O(\log n)$ | -| 6 | Trie | $O(m)$ | $O(n \cdot m)$ | $O(m)$ | +### Features + +- Fast and efficient implementations + +### Data Structures + +| Data Structure | Build Complexity | Memory Complexity | Search Complexity | +|------------------------------------------------------------------------|------------------|-------------------|-------------------| +| [B-tree](https://en.wikipedia.org/wiki/B-tree) | $O(\log n)$ | $O(n)$ | $O(\log n)$ | +| [Cartesian tree](https://en.wikipedia.org/wiki/Cartesian_tree) | $O(\log n)$\* | $O(n)$ | $O(\log n)$\* | +| [Red-black tree](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) | $O(\log n)$ | $O(n)$ | $O(\log n)$ | +| [Skip list](https://en.wikipedia.org/wiki/Skip_list) | $O(\log n)$\* | $O(n)$ | $O(\log n)$\* | +| Sorted set | $O(n)$ | $O(n)$ | $O(\log n)$ | +| [Trie](https://en.wikipedia.org/wiki/Trie) | $O(m)$ | $O(n \cdot m)$ | $O(m)$ | - $n$: number of stored elements - $m$: maximum length of a key diff --git a/build.zig.zon b/build.zig.zon index 015df48..be1e460 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ordered, - .version = "0.1.0-alpha.2", + .version = "0.1.0-alpha.3", .fingerprint = 0xc3121f99b0352e1b, // Changing this has security and trust implications. .minimum_zig_version = "0.15.1", .paths = .{ From bcd47b8225892f45a0578032f4977dbef5ff4807 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Mon, 27 Oct 2025 09:05:38 +0100 Subject: [PATCH 2/6] Update Zig version to `0.15.2` --- Makefile | 2 +- README.md | 2 +- build.zig.zon | 2 +- pyproject.toml | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 8b4f788..cacd24e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # ################################################################################ # # Configuration and Variables # ################################################################################ -ZIG ?= $(shell which zig || echo ~/.local/share/zig/0.15.1/zig) +ZIG ?= $(shell which zig || echo ~/.local/share/zig/0.15.2/zig) BUILD_TYPE ?= Debug BUILD_OPTS = -Doptimize=$(BUILD_TYPE) JOBS ?= $(shell nproc || echo 2) diff --git a/README.md b/README.md index 4d65ba0..e6696ab 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![CodeFactor](https://img.shields.io/codefactor/grade/github/CogitatorTech/ordered?label=quality&style=flat&labelColor=282c34&logo=codefactor)](https://www.codefactor.io/repository/github/CogitatorTech/ordered) [![Docs](https://img.shields.io/badge/docs-view-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/ordered/) [![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/ordered/tree/main/examples) -[![Zig Version](https://img.shields.io/badge/Zig-0.15.1-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) +[![Zig Version](https://img.shields.io/badge/Zig-0.15.2-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) [![Release](https://img.shields.io/github/release/CogitatorTech/ordered.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/releases/latest) [![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/CogitatorTech/ordered/blob/main/LICENSE) diff --git a/build.zig.zon b/build.zig.zon index be1e460..77e0f54 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .ordered, .version = "0.1.0-alpha.3", .fingerprint = 0xc3121f99b0352e1b, // Changing this has security and trust implications. - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.15.2", .paths = .{ "build.zig", "build.zig.zon", diff --git a/pyproject.toml b/pyproject.toml index a877ee1..ba681e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,11 +11,11 @@ dependencies = [ [project.optional-dependencies] dev = [ - "pytest>=8.0.1", - "pytest-cov>=6.0.0", - "pytest-mock>=3.14.0", + "pytest (>=8.0.1,<9.0.0)", + "pytest-cov (>=6.0.0,<7.0.0)", + "pytest-mock (>=3.14.0,<4.0.0)", "pytest-asyncio (>=0.26.0,<0.27.0)", - "mypy>=1.11.1", - "ruff>=0.9.3", + "mypy (>=1.11.1,<2.0.0)", + "ruff (>=0.9.3,<1.0.0)", "icecream (>=2.1.4,<3.0.0)" ] From dd9dfeac695de960e1ddb578420b3f5d5c1f755a Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Mon, 27 Oct 2025 09:31:01 +0100 Subject: [PATCH 3/6] Fix bugs --- src/ordered/btree_map.zig | 69 +++++++++++++++++++++++++++------- src/ordered/cartesian_tree.zig | 20 +++++----- src/ordered/sorted_set.zig | 55 +++++++++++++++------------ 3 files changed, 98 insertions(+), 46 deletions(-) diff --git a/src/ordered/btree_map.zig b/src/ordered/btree_map.zig index 58a4634..c34dc5e 100644 --- a/src/ordered/btree_map.zig +++ b/src/ordered/btree_map.zig @@ -77,14 +77,28 @@ pub fn BTreeMap( return null; } - /// Inserts a key-value pair. If the key exists, the value is updated. - pub fn put(self: *Self, key: K, value: V) !void { - // Check if key exists and just update the value in place - if (self.get(key) != null) { - _ = self.remove(key); - // Don't increment len here, it will be incremented below + /// Retrieves a mutable pointer to the value associated with `key`. + pub fn getPtr(self: *Self, key: K) ?*V { + var current = self.root; + while (current) |node| { + const res = std.sort.binarySearch(K, node.keys[0..node.len], key, compareFn); + if (res) |index| return &node.values[index]; + + if (node.is_leaf) return null; + + const insertion_point = std.sort.lowerBound(K, node.keys[0..node.len], key, compareFn); + current = node.children[insertion_point]; } + return null; + } + + /// Returns true if the map contains the given key. + pub fn contains(self: *const Self, key: K) bool { + return self.get(key) != null; + } + /// Inserts a key-value pair. If the key exists, the value is updated. + pub fn put(self: *Self, key: K, value: V) !void { var root_node = if (self.root) |r| r else { const new_node = try self.createNode(); new_node.keys[0] = key; @@ -104,8 +118,10 @@ pub fn BTreeMap( root_node = new_root; } - self.insertNonFull(root_node, key, value); - self.len += 1; + const is_new = self.insertNonFull(root_node, key, value); + if (is_new) { + self.len += 1; + } } fn splitChild(self: *Self, parent: *Node, index: u16) void { @@ -153,9 +169,20 @@ pub fn BTreeMap( parent.len += 1; } - fn insertNonFull(self: *Self, node: *Node, key: K, value: V) void { + fn insertNonFull(self: *Self, node: *Node, key: K, value: V) bool { var i = node.len; if (node.is_leaf) { + // Check if key already exists + var j: u16 = 0; + while (j < node.len) : (j += 1) { + if (compare(key, node.keys[j]) == .eq) { + // Update existing value + node.values[j] = value; + return false; // Not a new insertion + } + } + + // Insert new key while (i > 0 and compare(key, node.keys[i - 1]) == .lt) : (i -= 1) { node.keys[i] = node.keys[i - 1]; node.values[i] = node.values[i - 1]; @@ -163,7 +190,18 @@ pub fn BTreeMap( node.keys[i] = key; node.values[i] = value; node.len += 1; + return true; // New insertion } else { + // Check if key exists in current node + var j: u16 = 0; + while (j < node.len) : (j += 1) { + if (compare(key, node.keys[j]) == .eq) { + // Update existing value + node.values[j] = value; + return false; // Not a new insertion + } + } + while (i > 0 and compare(key, node.keys[i - 1]) == .lt) : (i -= 1) {} if (node.children[i].?.len == BRANCHING_FACTOR - 1) { self.splitChild(node, i); @@ -171,7 +209,7 @@ pub fn BTreeMap( i += 1; } } - self.insertNonFull(node.children[i].?, key, value); + return self.insertNonFull(node.children[i].?, key, value); } } @@ -227,18 +265,23 @@ pub fn BTreeMap( const pred = self.getPredecessor(node, index); node.keys[index] = pred.key; node.values[index] = pred.value; + // deleteFromNode already decremented self.len, so increment it back + // because we're replacing, not actually removing + const old_len = self.len; _ = self.deleteFromNode(node.children[index].?, pred.key); - self.len += 1; + self.len = old_len; } else if (node.children[index + 1].?.len > MIN_KEYS) { const succ = self.getSuccessor(node, index); node.keys[index] = succ.key; node.values[index] = succ.value; + const old_len = self.len; _ = self.deleteFromNode(node.children[index + 1].?, succ.key); - self.len += 1; + self.len = old_len; } else { self.merge(node, index); + const old_len = self.len; _ = self.deleteFromNode(node.children[index].?, key); - self.len += 1; + self.len = old_len; } } diff --git a/src/ordered/cartesian_tree.zig b/src/ordered/cartesian_tree.zig index 03eb555..48e710c 100644 --- a/src/ordered/cartesian_tree.zig +++ b/src/ordered/cartesian_tree.zig @@ -220,12 +220,12 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { stack: std.ArrayList(*Node), allocator: Allocator, - pub fn init(allocator: Allocator, root: ?*Node) Iterator { + pub fn init(allocator: Allocator, root: ?*Node) !Iterator { var it = Iterator{ - .stack = .{}, + .stack = std.ArrayList(*Node){}, .allocator = allocator, }; - it.pushLeft(root); + try it.pushLeft(root); return it; } @@ -233,23 +233,23 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { self.stack.deinit(self.allocator); } - fn pushLeft(self: *Iterator, node: ?*Node) void { + fn pushLeft(self: *Iterator, node: ?*Node) !void { var current = node; while (current) |n| { - self.stack.append(self.allocator, n) catch return; // Handle potential allocation failure + try self.stack.append(self.allocator, n); current = n.left; } } // src/cartesian_tree.zig - pub fn next(self: *Iterator) ?struct { key: K, value: V } { + pub fn next(self: *Iterator) !?struct { key: K, value: V } { // self.stack.pop() returns `?*Node`. // The `if` statement correctly handles the optional, unwrapping it into `node`. if (self.stack.pop()) |node| { // 'node' is now a valid `*Node` pointer. if (node.right) |right_node| { - self.pushLeft(right_node); + try self.pushLeft(right_node); } return .{ .key = node.key, .value = node.value }; } else { @@ -259,7 +259,7 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { }; /// Create iterator for in-order traversal - pub fn iterator(self: *const Self, allocator: Allocator) Iterator { + pub fn iterator(self: *const Self, allocator: Allocator) !Iterator { return Iterator.init(allocator, self.root); } }; @@ -424,14 +424,14 @@ test "CartesianTree: iterator traversal" { try tree.putWithPriority(20, 20, 20); try tree.putWithPriority(5, 5, 5); - var iter = tree.iterator(testing.allocator); + var iter = try tree.iterator(testing.allocator); defer iter.deinit(); // Should iterate in sorted key order (BST property) const expected = [_]i32{ 5, 10, 20, 30 }; var idx: usize = 0; - while (iter.next()) |entry| : (idx += 1) { + while (try iter.next()) |entry| : (idx += 1) { try testing.expectEqual(expected[idx], entry.key); } try testing.expectEqual(@as(usize, 4), idx); diff --git a/src/ordered/sorted_set.zig b/src/ordered/sorted_set.zig index 4435ed7..3f84cb7 100644 --- a/src/ordered/sorted_set.zig +++ b/src/ordered/sorted_set.zig @@ -16,7 +16,7 @@ pub fn SortedSet( pub fn init(allocator: std.mem.Allocator) Self { return .{ - .items = .{}, + .items = std.ArrayList(T){}, .allocator = allocator, }; } @@ -29,10 +29,16 @@ pub fn SortedSet( return compare(key, item); } - /// Adds a value to the vector, maintaining sort order. - pub fn add(self: *Self, value: T) !void { + /// Adds a value to the set, maintaining sort order. + /// Returns true if the value was added, false if it already existed. + pub fn add(self: *Self, value: T) !bool { const index = std.sort.lowerBound(T, self.items.items, value, compareFn); + // Check if value already exists + if (index < self.items.items.len and compare(self.items.items[index], value) == .eq) { + return false; + } try self.items.insert(self.allocator, index, value); + return true; } /// Removes an element at a given index. @@ -61,9 +67,9 @@ test "SortedSet basic functionality" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - try vec.add(100); - try vec.add(50); - try vec.add(75); + _ = try vec.add(100); + _ = try vec.add(50); + _ = try vec.add(75); try std.testing.expectEqualSlices(i32, &.{ 50, 75, 100 }, vec.items.items); try std.testing.expect(vec.contains(75)); @@ -89,7 +95,7 @@ test "SortedSet: single element" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - try vec.add(42); + _ = try vec.add(42); try std.testing.expect(vec.contains(42)); try std.testing.expectEqual(@as(usize, 1), vec.items.items.len); @@ -98,17 +104,20 @@ test "SortedSet: single element" { try std.testing.expectEqual(@as(usize, 0), vec.items.items.len); } -test "SortedSet: duplicate values" { +test "SortedSet: duplicate values rejected" { const allocator = std.testing.allocator; var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - try vec.add(10); - try vec.add(10); - try vec.add(10); + const added1 = try vec.add(10); + const added2 = try vec.add(10); + const added3 = try vec.add(10); - // Duplicates are allowed in this implementation - try std.testing.expectEqual(@as(usize, 3), vec.items.items.len); + // Duplicates should be rejected in a proper Set + try std.testing.expect(added1); + try std.testing.expect(!added2); + try std.testing.expect(!added3); + try std.testing.expectEqual(@as(usize, 1), vec.items.items.len); } test "SortedSet: negative numbers" { @@ -116,10 +125,10 @@ test "SortedSet: negative numbers" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - try vec.add(-5); - try vec.add(-10); - try vec.add(0); - try vec.add(5); + _ = try vec.add(-5); + _ = try vec.add(-10); + _ = try vec.add(0); + _ = try vec.add(5); try std.testing.expectEqualSlices(i32, &.{ -10, -5, 0, 5 }, vec.items.items); } @@ -132,7 +141,7 @@ test "SortedSet: large dataset" { // Insert in reverse order var i: i32 = 100; while (i >= 0) : (i -= 1) { - try vec.add(i); + _ = try vec.add(i); } // Verify sorted @@ -147,11 +156,11 @@ test "SortedSet: remove boundary cases" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - try vec.add(1); - try vec.add(2); - try vec.add(3); - try vec.add(4); - try vec.add(5); + _ = try vec.add(1); + _ = try vec.add(2); + _ = try vec.add(3); + _ = try vec.add(4); + _ = try vec.add(5); // Remove first _ = vec.remove(0); From e40cea576fdabed282b01438ae0cc8ca5fcb78d1 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Mon, 27 Oct 2025 09:37:05 +0100 Subject: [PATCH 4/6] Make the API consistent --- benches/b2_sorted_set.zig | 6 +- benches/b3_red_black_tree.zig | 10 +-- benches/b4_skip_list.zig | 2 +- benches/b5_trie.zig | 14 ++-- benches/b6_cartesian_tree.zig | 4 +- examples/e1_btree_map.zig | 6 +- examples/e2_sorted_set.zig | 12 ++-- examples/e3_red_black_tree.zig | 19 ++--- examples/e4_skip_list.zig | 7 +- examples/e5_trie.zig | 24 +++---- examples/e6_cartesian_tree.zig | 8 ++- src/ordered/btree_map.zig | 22 +++++- src/ordered/cartesian_tree.zig | 67 +++++++++++++----- src/ordered/red_black_tree.zig | 102 ++++++++++++++------------ src/ordered/skip_list.zig | 43 ++++++++--- src/ordered/sorted_set.zig | 55 +++++++++----- src/ordered/trie.zig | 126 ++++++++++++++++++++++++++------- 17 files changed, 357 insertions(+), 170 deletions(-) diff --git a/benches/b2_sorted_set.zig b/benches/b2_sorted_set.zig index ece558f..282dde7 100644 --- a/benches/b2_sorted_set.zig +++ b/benches/b2_sorted_set.zig @@ -32,7 +32,7 @@ fn benchmarkAdd(allocator: std.mem.Allocator, size: usize) !void { var i: i32 = 0; while (i < size) : (i += 1) { - try set.add(i); + _ = try set.put(i); } const elapsed = timer.read() - start; @@ -51,7 +51,7 @@ fn benchmarkContains(allocator: std.mem.Allocator, size: usize) !void { var i: i32 = 0; while (i < size) : (i += 1) { - try set.add(i); + _ = try set.put(i); } var timer = try Timer.start(); @@ -80,7 +80,7 @@ fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { var i: i32 = 0; while (i < size) : (i += 1) { - try set.add(i); + _ = try set.put(i); } var timer = try Timer.start(); diff --git a/benches/b3_red_black_tree.zig b/benches/b3_red_black_tree.zig index d571e63..1666c9a 100644 --- a/benches/b3_red_black_tree.zig +++ b/benches/b3_red_black_tree.zig @@ -36,7 +36,7 @@ fn benchmarkInsert(allocator: std.mem.Allocator, size: usize) !void { var i: i32 = 0; while (i < size) : (i += 1) { - try tree.insert(i); + try tree.put(i); } const elapsed = timer.read() - start; @@ -55,7 +55,7 @@ fn benchmarkFind(allocator: std.mem.Allocator, size: usize) !void { var i: i32 = 0; while (i < size) : (i += 1) { - try tree.insert(i); + try tree.put(i); } var timer = try Timer.start(); @@ -64,7 +64,7 @@ fn benchmarkFind(allocator: std.mem.Allocator, size: usize) !void { i = 0; var found: usize = 0; while (i < size) : (i += 1) { - if (tree.find(i) != null) found += 1; + if (tree.get(i) != null) found += 1; } const elapsed = timer.read() - start; @@ -84,7 +84,7 @@ fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { var i: i32 = 0; while (i < size) : (i += 1) { - try tree.insert(i); + try tree.put(i); } var timer = try Timer.start(); @@ -111,7 +111,7 @@ fn benchmarkIterator(allocator: std.mem.Allocator, size: usize) !void { var i: i32 = 0; while (i < size) : (i += 1) { - try tree.insert(i); + try tree.put(i); } var timer = try Timer.start(); diff --git a/benches/b4_skip_list.zig b/benches/b4_skip_list.zig index a534f9c..3b3c424 100644 --- a/benches/b4_skip_list.zig +++ b/benches/b4_skip_list.zig @@ -88,7 +88,7 @@ fn benchmarkDelete(allocator: std.mem.Allocator, size: usize) !void { i = 0; while (i < size) : (i += 1) { - _ = list.delete(i); + _ = list.remove(i); } const elapsed = timer.read() - start; diff --git a/benches/b5_trie.zig b/benches/b5_trie.zig index deb0c14..8a54774 100644 --- a/benches/b5_trie.zig +++ b/benches/b5_trie.zig @@ -144,14 +144,14 @@ fn benchmarkPrefixSearch(allocator: std.mem.Allocator, size: usize) !void { i = 0; while (i < num_searches) : (i += 1) { - var keys = try trie.keysWithPrefix(allocator, "key_"); - defer { - for (keys.items) |key| { - allocator.free(key); - } - keys.deinit(allocator); + var iter = try trie.keysWithPrefix(allocator, "key_"); + defer iter.deinit(); + + var count: usize = 0; + while (try iter.next()) |_| { + count += 1; } - total_found += keys.items.len; + total_found += count; } const elapsed = timer.read() - start; diff --git a/benches/b6_cartesian_tree.zig b/benches/b6_cartesian_tree.zig index 377f8a0..26ea7cd 100644 --- a/benches/b6_cartesian_tree.zig +++ b/benches/b6_cartesian_tree.zig @@ -110,11 +110,11 @@ fn benchmarkIterator(allocator: std.mem.Allocator, size: usize) !void { var timer = try Timer.start(); const start = timer.lap(); - var iter = tree.iterator(allocator); + var iter = try tree.iterator(allocator); defer iter.deinit(); var count: usize = 0; - while (iter.next()) |_| { + while (try iter.next()) |_| { count += 1; } diff --git a/examples/e1_btree_map.zig b/examples/e1_btree_map.zig index 463cf7d..fd70565 100644 --- a/examples/e1_btree_map.zig +++ b/examples/e1_btree_map.zig @@ -22,6 +22,8 @@ pub fn main() !void { std.debug.print("Found key '{s}': value is {d}\n", .{ key_to_find, value_ptr.* }); } - _ = map.remove("banana"); - std.debug.print("Contains 'banana' after delete? {any}\n\n", .{map.get("banana") != null}); + const removed = map.remove("banana"); + std.debug.print("Removed 'banana' with value: {?d}\n", .{if (removed) |v| v else null}); + std.debug.print("Contains 'banana' after remove? {any}\n", .{map.contains("banana")}); + std.debug.print("Map count: {d}\n\n", .{map.count()}); } diff --git a/examples/e2_sorted_set.zig b/examples/e2_sorted_set.zig index 1e10957..a807020 100644 --- a/examples/e2_sorted_set.zig +++ b/examples/e2_sorted_set.zig @@ -12,9 +12,13 @@ pub fn main() !void { var sorted_set = ordered.SortedSet(i32, i32Compare).init(allocator); defer sorted_set.deinit(); - try sorted_set.add(100); - try sorted_set.add(25); - try sorted_set.add(50); + _ = try sorted_set.put(100); + _ = try sorted_set.put(25); + _ = try sorted_set.put(50); + const duplicate = try sorted_set.put(50); // Try adding duplicate - std.debug.print("SortedSet contents: {any}\n\n", .{sorted_set.items.items}); + std.debug.print("SortedSet count: {d}\n", .{sorted_set.count()}); + std.debug.print("Added duplicate 50? {any}\n", .{duplicate}); + std.debug.print("SortedSet contents: {any}\n", .{sorted_set.items.items}); + std.debug.print("Contains 100? {any}\n\n", .{sorted_set.contains(100)}); } diff --git a/examples/e3_red_black_tree.zig b/examples/e3_red_black_tree.zig index dbaefc5..ad3f10a 100644 --- a/examples/e3_red_black_tree.zig +++ b/examples/e3_red_black_tree.zig @@ -16,21 +16,22 @@ pub fn main() !void { var rbt = ordered.RedBlackTree(i32, I32Context).init(allocator, .{}); defer rbt.deinit(); - try rbt.insert(40); - try rbt.insert(20); - try rbt.insert(60); - try rbt.insert(10); - try rbt.insert(30); + try rbt.put(40); + try rbt.put(20); + try rbt.put(60); + try rbt.put(10); + try rbt.put(30); - // Update is handled by insert - try rbt.insert(30); + // Update is handled by put + try rbt.put(30); std.debug.print("RBT count: {d}\n", .{rbt.count()}); std.debug.print("RBT contains 20? {any}\n", .{rbt.contains(20)}); std.debug.print("RBT contains 99? {any}\n", .{rbt.contains(99)}); - if (rbt.remove(60)) { - std.debug.print("Removed 60\n", .{}); + const removed = rbt.remove(60); + if (removed) |val| { + std.debug.print("Removed value: {d}\n", .{val}); } std.debug.print("RBT count after remove: {d}\n\n", .{rbt.count()}); diff --git a/examples/e4_skip_list.zig b/examples/e4_skip_list.zig index cddf519..04b349f 100644 --- a/examples/e4_skip_list.zig +++ b/examples/e4_skip_list.zig @@ -17,7 +17,7 @@ pub fn main() !void { try skip_list.put("mango", 250); try skip_list.put("banana", 150); - std.debug.print("SkipList length: {d}\n", .{skip_list.len}); + std.debug.print("SkipList count: {d}\n", .{skip_list.count()}); if (skip_list.get("mango")) |value_ptr| { std.debug.print("Found 'mango': value is {d}\n", .{value_ptr.*}); @@ -29,6 +29,7 @@ pub fn main() !void { std.debug.print(" {s}: {d}\n", .{ entry.key, entry.value }); } - _ = skip_list.delete("apple"); - std.debug.print("Contains 'apple' after delete? {any}\n\n", .{skip_list.contains("apple")}); + const removed = skip_list.remove("apple"); + std.debug.print("Removed 'apple' with value: {?d}\n", .{removed}); + std.debug.print("Contains 'apple' after remove? {any}\n\n", .{skip_list.contains("apple")}); } diff --git a/examples/e5_trie.zig b/examples/e5_trie.zig index b086970..f3711f4 100644 --- a/examples/e5_trie.zig +++ b/examples/e5_trie.zig @@ -14,7 +14,7 @@ pub fn main() !void { try trie.put("care", "to look after"); try trie.put("careful", "cautious"); - std.debug.print("Trie length: {d}\n", .{trie.len}); + std.debug.print("Trie count: {d}\n", .{trie.count()}); if (trie.get("car")) |value_ptr| { std.debug.print("Found 'car': {s}\n", .{value_ptr.*}); @@ -23,21 +23,19 @@ pub fn main() !void { std.debug.print("Has prefix 'car'? {any}\n", .{trie.hasPrefix("car")}); std.debug.print("Contains 'ca'? {any}\n", .{trie.contains("ca")}); - var keys = try trie.keysWithPrefix(allocator, "car"); - defer { - for (keys.items) |key| { - allocator.free(key); - } - keys.deinit(allocator); - } - std.debug.print("Keys with prefix 'car': ", .{}); - for (keys.items, 0..) |key, i| { - if (i > 0) std.debug.print(", ", .{}); + var prefix_iter = try trie.keysWithPrefix(allocator, "car"); + defer prefix_iter.deinit(); + + var first = true; + while (try prefix_iter.next()) |key| { + if (!first) std.debug.print(", ", .{}); std.debug.print("'{s}'", .{key}); + first = false; } std.debug.print("\n", .{}); - _ = trie.delete("card"); - std.debug.print("Contains 'card' after delete? {any}\n", .{trie.contains("card")}); + const removed = trie.remove("card"); + std.debug.print("Removed 'card' with value: {?s}\n", .{removed}); + std.debug.print("Contains 'card' after remove? {any}\n\n", .{trie.contains("card")}); } diff --git a/examples/e6_cartesian_tree.zig b/examples/e6_cartesian_tree.zig index 6cf1e4e..bad0cda 100644 --- a/examples/e6_cartesian_tree.zig +++ b/examples/e6_cartesian_tree.zig @@ -19,9 +19,11 @@ pub fn main() !void { std.debug.print("Found key {d}: value is '{s}'\n", .{ search_key, value }); } - if (cartesian_tree.remove(30)) { - std.debug.print("Successfully deleted key {d}\n", .{search_key}); + const removed = cartesian_tree.remove(30); + if (removed) |value| { + std.debug.print("Removed key {d} with value: '{s}'\n", .{ search_key, value }); } - std.debug.print("Size after deletion: {d}\n\n", .{cartesian_tree.count()}); + std.debug.print("Size after deletion: {d}\n", .{cartesian_tree.count()}); + std.debug.print("Contains {d}? {any}\n\n", .{ search_key, cartesian_tree.contains(search_key) }); } diff --git a/src/ordered/btree_map.zig b/src/ordered/btree_map.zig index c34dc5e..32d0e1a 100644 --- a/src/ordered/btree_map.zig +++ b/src/ordered/btree_map.zig @@ -2,6 +2,14 @@ //! This is a workhorse for most ordered map use cases. B-Trees are extremely //! cache-friendly due to their high branching factor, making them faster than //! binary search trees for larger datasets. +//! +//! ## Thread Safety +//! This data structure is not thread-safe. External synchronization is required +//! for concurrent access. +//! +//! ## Iterator Invalidation +//! WARNING: Modifying the map (via put/remove/clear) while iterating will cause +//! undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); @@ -32,11 +40,23 @@ pub fn BTreeMap( return .{ .allocator = allocator }; } + /// Returns the number of elements in the map. + pub fn count(self: *const Self) usize { + return self.len; + } + pub fn deinit(self: *Self) void { - if (self.root) |r| self.deinitNode(r); + self.clear(); self.* = undefined; } + /// Removes all elements from the map. + pub fn clear(self: *Self) void { + if (self.root) |r| self.deinitNode(r); + self.root = null; + self.len = 0; + } + fn deinitNode(self: *Self, node: *Node) void { if (!node.is_leaf) { for (node.children[0 .. node.len + 1]) |child| { diff --git a/src/ordered/cartesian_tree.zig b/src/ordered/cartesian_tree.zig index 48e710c..45ab754 100644 --- a/src/ordered/cartesian_tree.zig +++ b/src/ordered/cartesian_tree.zig @@ -6,6 +6,14 @@ const Order = std.math.Order; /// A Cartesian Tree implementation that maintains both BST property for keys /// and heap property for priorities. Useful for range minimum queries and /// as a treap data structure. +/// +/// ## Thread Safety +/// This data structure is not thread-safe. External synchronization is required +/// for concurrent access. +/// +/// ## Iterator Invalidation +/// WARNING: Modifying the tree (via put/remove/clear) while iterating will cause +/// undefined behavior. Complete all iterations before modifying the structure. pub fn CartesianTree(comptime K: type, comptime V: type) type { return struct { const Self = @This(); @@ -37,10 +45,17 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { } pub fn deinit(self: *Self) void { - self.destroySubtree(self.root); + self.clear(); self.* = undefined; } + /// Removes all elements from the tree. + pub fn clear(self: *Self) void { + self.destroySubtree(self.root); + self.root = null; + self.len = 0; + } + fn destroySubtree(self: *Self, node: ?*Node) void { if (node) |n| { self.destroySubtree(n.left); @@ -146,41 +161,60 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { }; } - /// Remove key from tree - pub fn remove(self: *Self, key: K) bool { + /// Get mutable pointer to value by key + pub fn getPtr(self: *Self, key: K) ?*V { + return self.getNodePtr(self.root, key); + } + + fn getNodePtr(_: *Self, root: ?*Node, key: K) ?*V { + if (root == null) return null; + + const node = root.?; + const key_cmp = std.math.order(key, node.key); + + return switch (key_cmp) { + .eq => &node.value, + .lt => getNodePtr(undefined, node.left, key), + .gt => getNodePtr(undefined, node.right, key), + }; + } + + /// Remove key from tree and return its value if it existed + pub fn remove(self: *Self, key: K) ?V { const result = self.removeNode(self.root, key); self.root = result.root; - return result.removed; + return result.value; } const RemoveResult = struct { root: ?*Node, - removed: bool, + value: ?V, }; fn removeNode(self: *Self, root: ?*Node, key: K) RemoveResult { if (root == null) { - return RemoveResult{ .root = null, .removed = false }; + return RemoveResult{ .root = null, .value = null }; } const node = root.?; const key_cmp = std.math.order(key, node.key); if (key_cmp == .eq) { + const value = node.value; const merged = self.merge(node.left, node.right); self.allocator.destroy(node); self.len -= 1; - return RemoveResult{ .root = merged, .removed = true }; + return RemoveResult{ .root = merged, .value = value }; } if (key_cmp == .lt) { const result = self.removeNode(node.left, key); node.left = result.root; - return RemoveResult{ .root = root, .removed = result.removed }; + return RemoveResult{ .root = root, .value = result.value }; } else { const result = self.removeNode(node.right, key); node.right = result.root; - return RemoveResult{ .root = root, .removed = result.removed }; + return RemoveResult{ .root = root, .value = result.value }; } } @@ -288,7 +322,8 @@ test "CartesianTree basic operations" { try testing.expect(!tree.contains(99)); // Test remove - try testing.expect(tree.remove(3)); + const removed = tree.remove(3); + try testing.expect(removed != null); try testing.expectEqual(@as(usize, 3), tree.count()); try testing.expect(!tree.contains(3)); } @@ -301,7 +336,7 @@ test "CartesianTree: empty tree operations" { try testing.expectEqual(@as(usize, 0), tree.count()); try testing.expect(tree.get(42) == null); try testing.expect(!tree.contains(42)); - try testing.expect(!tree.remove(42)); + try testing.expect(tree.remove(42) == null); } test "CartesianTree: single element" { @@ -313,7 +348,7 @@ test "CartesianTree: single element" { try testing.expectEqual(@as(i32, 100), tree.get(42).?); const removed = tree.remove(42); - try testing.expect(removed); + try testing.expect(removed != null); try testing.expect(tree.isEmpty()); try testing.expect(tree.root == null); } @@ -382,7 +417,7 @@ test "CartesianTree: remove non-existent key" { try tree.putWithPriority(20, 20, 20); const removed = tree.remove(15); - try testing.expect(!removed); + try testing.expect(removed == null); try testing.expectEqual(@as(usize, 2), tree.count()); } @@ -394,9 +429,9 @@ test "CartesianTree: remove all elements" { try tree.putWithPriority(2, 2, 2); try tree.putWithPriority(3, 3, 3); - try testing.expect(tree.remove(1)); - try testing.expect(tree.remove(2)); - try testing.expect(tree.remove(3)); + _ = tree.remove(1); + _ = tree.remove(2); + _ = tree.remove(3); try testing.expect(tree.isEmpty()); try testing.expect(tree.get(2) == null); diff --git a/src/ordered/red_black_tree.zig b/src/ordered/red_black_tree.zig index 77e9605..054be82 100644 --- a/src/ordered/red_black_tree.zig +++ b/src/ordered/red_black_tree.zig @@ -4,7 +4,15 @@ const testing = std.testing; const assert = std.debug.assert; /// Red-Black Tree implementation -/// A self-balancing binary search tree with O(log n) operations +/// A self-balancing binary search tree with O(log n) operations. +/// +/// ## Thread Safety +/// This data structure is not thread-safe. External synchronization is required +/// for concurrent access. +/// +/// ## Iterator Invalidation +/// WARNING: Modifying the tree (via insert/remove/clear) while iterating will +/// cause undefined behavior. Complete all iterations before modifying the structure. pub fn RedBlackTree(comptime T: type, comptime Context: type) type { return struct { const Self = @This(); @@ -63,9 +71,10 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { return self.size; } - pub fn insert(self: *Self, data: T) !void { + /// Inserts or updates a value in the tree. + pub fn put(self: *Self, data: T) !void { // Check if key already exists first to avoid unnecessary allocation - if (self.find(data)) |existing| { + if (self.get(data)) |existing| { existing.data = data; return; } @@ -167,11 +176,13 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { if (self.root) |root| root.color = .black; // Root is always black } - pub fn remove(self: *Self, data: T) bool { - const node = self.find(data) orelse return false; + /// Removes a value from the tree and returns it if it existed. + pub fn remove(self: *Self, data: T) ?T { + const node = self.get(data) orelse return null; + const value = node.data; self.removeNode(node); self.size -= 1; - return true; + return value; } fn removeNode(self: *Self, node: *Node) void { @@ -348,7 +359,8 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { node.parent = left; } - pub fn find(self: Self, data: T) ?*Node { + /// Returns a pointer to the node containing the data. + pub fn get(self: Self, data: T) ?*Node { var current = self.root; while (current) |node| { @@ -365,7 +377,7 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { } pub fn contains(self: Self, data: T) bool { - return self.find(data) != null; + return self.get(data) != null; } fn findMinimum(self: Self, node: *Node) *Node { @@ -459,9 +471,9 @@ test "RedBlackTree: basic operations" { var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(10); - try tree.insert(20); - try tree.insert(5); + try tree.put(10); + try tree.put(20); + try tree.put(5); try std.testing.expectEqual(@as(usize, 3), tree.count()); try std.testing.expect(tree.contains(10)); @@ -476,7 +488,7 @@ test "RedBlackTree: empty tree operations" { try std.testing.expect(!tree.contains(42)); try std.testing.expectEqual(@as(usize, 0), tree.count()); - try std.testing.expect(!tree.remove(42)); + try std.testing.expect(tree.remove(42) == null); } test "RedBlackTree: single element" { @@ -484,13 +496,13 @@ test "RedBlackTree: single element" { var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(42); + try tree.put(42); try std.testing.expectEqual(@as(usize, 1), tree.count()); try std.testing.expect(tree.contains(42)); try std.testing.expect(tree.root.?.color == .black); const removed = tree.remove(42); - try std.testing.expect(removed); + try std.testing.expect(removed != null); try std.testing.expectEqual(@as(usize, 0), tree.count()); try std.testing.expect(tree.root == null); } @@ -500,9 +512,9 @@ test "RedBlackTree: duplicate insertions" { var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(10); - try tree.insert(10); - try tree.insert(10); + try tree.put(10); + try tree.put(10); + try tree.put(10); // Duplicates update existing nodes try std.testing.expectEqual(@as(usize, 1), tree.count()); @@ -515,7 +527,7 @@ test "RedBlackTree: sequential insertion" { var i: i32 = 0; while (i < 50) : (i += 1) { - try tree.insert(i); + try tree.put(i); } try std.testing.expectEqual(@as(usize, 50), tree.count()); @@ -534,7 +546,7 @@ test "RedBlackTree: reverse insertion" { var i: i32 = 50; while (i > 0) : (i -= 1) { - try tree.insert(i); + try tree.put(i); } try std.testing.expectEqual(@as(usize, 50), tree.count()); @@ -546,14 +558,14 @@ test "RedBlackTree: remove from middle" { var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(10); - try tree.insert(5); - try tree.insert(15); - try tree.insert(3); - try tree.insert(7); + try tree.put(10); + try tree.put(5); + try tree.put(15); + try tree.put(3); + try tree.put(7); const removed = tree.remove(5); - try std.testing.expect(removed); + try std.testing.expect(removed != null); try std.testing.expectEqual(@as(usize, 4), tree.count()); try std.testing.expect(!tree.contains(5)); try std.testing.expect(tree.contains(3)); @@ -565,12 +577,12 @@ test "RedBlackTree: remove root" { var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(10); - try tree.insert(5); - try tree.insert(15); + try tree.put(10); + try tree.put(5); + try tree.put(15); const removed = tree.remove(10); - try std.testing.expect(removed); + try std.testing.expect(removed != null); try std.testing.expectEqual(@as(usize, 2), tree.count()); try std.testing.expect(tree.root.?.color == .black); } @@ -580,11 +592,11 @@ test "RedBlackTree: minimum and maximum" { var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(10); - try tree.insert(5); - try tree.insert(15); - try tree.insert(3); - try tree.insert(20); + try tree.put(10); + try tree.put(5); + try tree.put(15); + try tree.put(3); + try tree.put(20); const min = tree.minimum(null); const max = tree.maximum(null); @@ -612,9 +624,9 @@ test "RedBlackTree: clear" { var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(1); - try tree.insert(2); - try tree.insert(3); + try tree.put(1); + try tree.put(2); + try tree.put(3); tree.clear(); try std.testing.expectEqual(@as(usize, 0), tree.count()); @@ -626,25 +638,25 @@ test "RedBlackTree: negative numbers" { var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(-10); - try tree.insert(-5); - try tree.insert(0); - try tree.insert(5); + try tree.put(-10); + try tree.put(-5); + try tree.put(0); + try tree.put(5); try std.testing.expectEqual(@as(usize, 4), tree.count()); try std.testing.expect(tree.contains(-10)); try std.testing.expect(tree.contains(0)); } -test "RedBlackTree: find returns correct node" { +test "RedBlackTree: get returns correct node" { const allocator = std.testing.allocator; var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); - try tree.insert(10); - try tree.insert(20); + try tree.put(10); + try tree.put(20); - const node = tree.find(10); + const node = tree.get(10); try std.testing.expect(node != null); try std.testing.expectEqual(@as(i32, 10), node.?.data); } diff --git a/src/ordered/skip_list.zig b/src/ordered/skip_list.zig index 8f00e89..e910215 100644 --- a/src/ordered/skip_list.zig +++ b/src/ordered/skip_list.zig @@ -2,6 +2,14 @@ //! SkipList offers O(log n) performance on average and is simpler to implement //! correctly than balanced binary trees. It uses less memory per-node than B-Trees //! and has excellent concurrent-friendly properties. +//! +//! ## Thread Safety +//! This implementation is not thread-safe. External synchronization is required +//! for concurrent access. +//! +//! ## Iterator Invalidation +//! WARNING: Modifying the skip list (via put/delete/clear) while iterating will +//! cause undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); @@ -41,6 +49,11 @@ pub fn SkipList( allocator: std.mem.Allocator, rng: std.Random.DefaultPrng, + /// Returns the number of elements in the skip list. + pub fn count(self: *const Self) usize { + return self.len; + } + pub fn init(allocator: std.mem.Allocator) !Self { const header = try allocator.create(Node); header.key = undefined; @@ -60,15 +73,23 @@ pub fn SkipList( } pub fn deinit(self: *Self) void { + self.clear(); + self.allocator.free(self.header.forward); + self.allocator.destroy(self.header); + self.* = undefined; + } + + /// Removes all elements from the skip list. + pub fn clear(self: *Self) void { var current = self.header.forward[0]; while (current) |node| { const next = node.forward[0]; node.deinit(self.allocator); current = next; } - self.allocator.free(self.header.forward); - self.allocator.destroy(self.header); - self.* = undefined; + @memset(self.header.forward, null); + self.level = 0; + self.len = 0; } fn randomLevel(self: *Self) u8 { @@ -173,7 +194,7 @@ pub fn SkipList( } /// Removes a key-value pair and returns the value if it existed. - pub fn delete(self: *Self, key: K) ?V { + pub fn remove(self: *Self, key: K) ?V { var update: [MAX_LEVEL]?*Node = undefined; var current = self.header; var i = self.level; @@ -269,7 +290,7 @@ test "SkipList: basic operations" { try std.testing.expectEqual(@as(usize, 4), list.len); // Test delete - const deleted = list.delete(20); + const deleted = list.remove(20); try std.testing.expectEqualStrings("twenty", deleted.?); try std.testing.expect(list.get(20) == null); try std.testing.expectEqual(@as(usize, 3), list.len); @@ -327,7 +348,7 @@ test "SkipList: empty list operations" { try std.testing.expect(list.get(42) == null); try std.testing.expectEqual(@as(usize, 0), list.len); - try std.testing.expect(list.delete(42) == null); + try std.testing.expect(list.remove(42) == null); try std.testing.expect(!list.contains(42)); } @@ -340,7 +361,7 @@ test "SkipList: single element" { try std.testing.expectEqual(@as(usize, 1), list.len); try std.testing.expectEqual(@as(i32, 100), list.get(42).?.*); - const deleted = list.delete(42); + const deleted = list.remove(42); try std.testing.expectEqual(@as(i32, 100), deleted.?); try std.testing.expectEqual(@as(usize, 0), list.len); } @@ -392,7 +413,7 @@ test "SkipList: delete non-existent" { try list.put(10, 10); try list.put(20, 20); - const deleted = list.delete(15); + const deleted = list.remove(15); try std.testing.expect(deleted == null); try std.testing.expectEqual(@as(usize, 2), list.len); } @@ -406,9 +427,9 @@ test "SkipList: delete all elements" { try list.put(2, 2); try list.put(3, 3); - _ = list.delete(1); - _ = list.delete(2); - _ = list.delete(3); + _ = list.remove(1); + _ = list.remove(2); + _ = list.remove(3); try std.testing.expectEqual(@as(usize, 0), list.len); try std.testing.expect(list.get(2) == null); diff --git a/src/ordered/sorted_set.zig b/src/ordered/sorted_set.zig index 3f84cb7..6dcf2ac 100644 --- a/src/ordered/sorted_set.zig +++ b/src/ordered/sorted_set.zig @@ -1,6 +1,15 @@ //! A set that keeps its elements sorted at all times. //! Inserts are O(n) because elements may need to be shifted, but searching //! is O(log n) via binary search. It is cache-friendly for traversals. +//! +//! ## Thread Safety +//! This data structure is not thread-safe. External synchronization is required +//! for concurrent access. +//! +//! ## Iterator Invalidation +//! WARNING: Modifying the set (via add/remove/clear) while iterating over +//! `.items.items` will cause undefined behavior. Complete all iterations before +//! modifying the structure. const std = @import("std"); @@ -14,6 +23,11 @@ pub fn SortedSet( items: std.ArrayList(T), allocator: std.mem.Allocator, + /// Returns the number of elements in the set. + pub fn count(self: *const Self) usize { + return self.items.items.len; + } + pub fn init(allocator: std.mem.Allocator) Self { return .{ .items = std.ArrayList(T){}, @@ -25,13 +39,18 @@ pub fn SortedSet( self.items.deinit(self.allocator); } + /// Removes all elements from the set. + pub fn clear(self: *Self) void { + self.items.clearRetainingCapacity(); + } + fn compareFn(key: T, item: T) std.math.Order { return compare(key, item); } /// Adds a value to the set, maintaining sort order. /// Returns true if the value was added, false if it already existed. - pub fn add(self: *Self, value: T) !bool { + pub fn put(self: *Self, value: T) !bool { const index = std.sort.lowerBound(T, self.items.items, value, compareFn); // Check if value already exists if (index < self.items.items.len and compare(self.items.items[index], value) == .eq) { @@ -67,9 +86,9 @@ test "SortedSet basic functionality" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - _ = try vec.add(100); - _ = try vec.add(50); - _ = try vec.add(75); + _ = try vec.put(100); + _ = try vec.put(50); + _ = try vec.put(75); try std.testing.expectEqualSlices(i32, &.{ 50, 75, 100 }, vec.items.items); try std.testing.expect(vec.contains(75)); @@ -95,7 +114,7 @@ test "SortedSet: single element" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - _ = try vec.add(42); + _ = try vec.put(42); try std.testing.expect(vec.contains(42)); try std.testing.expectEqual(@as(usize, 1), vec.items.items.len); @@ -109,9 +128,9 @@ test "SortedSet: duplicate values rejected" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - const added1 = try vec.add(10); - const added2 = try vec.add(10); - const added3 = try vec.add(10); + const added1 = try vec.put(10); + const added2 = try vec.put(10); + const added3 = try vec.put(10); // Duplicates should be rejected in a proper Set try std.testing.expect(added1); @@ -125,10 +144,10 @@ test "SortedSet: negative numbers" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - _ = try vec.add(-5); - _ = try vec.add(-10); - _ = try vec.add(0); - _ = try vec.add(5); + _ = try vec.put(-5); + _ = try vec.put(-10); + _ = try vec.put(0); + _ = try vec.put(5); try std.testing.expectEqualSlices(i32, &.{ -10, -5, 0, 5 }, vec.items.items); } @@ -141,7 +160,7 @@ test "SortedSet: large dataset" { // Insert in reverse order var i: i32 = 100; while (i >= 0) : (i -= 1) { - _ = try vec.add(i); + _ = try vec.put(i); } // Verify sorted @@ -156,11 +175,11 @@ test "SortedSet: remove boundary cases" { var vec = SortedSet(i32, i32Compare).init(allocator); defer vec.deinit(); - _ = try vec.add(1); - _ = try vec.add(2); - _ = try vec.add(3); - _ = try vec.add(4); - _ = try vec.add(5); + _ = try vec.put(1); + _ = try vec.put(2); + _ = try vec.put(3); + _ = try vec.put(4); + _ = try vec.put(5); // Remove first _ = vec.remove(0); diff --git a/src/ordered/trie.zig b/src/ordered/trie.zig index 17e538d..53cdd2a 100644 --- a/src/ordered/trie.zig +++ b/src/ordered/trie.zig @@ -1,6 +1,14 @@ //! A Trie (prefix tree) data structure for efficient string storage and retrieval. //! Tries excel at prefix-based operations like autocomplete, word validation, //! and prefix matching. They provide O(m) complexity where m is the key length. +//! +//! ## Thread Safety +//! This data structure is not thread-safe. External synchronization is required +//! for concurrent access. +//! +//! ## Iterator Invalidation +//! WARNING: Modifying the trie (via put/delete/clear) while iterating will cause +//! undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); @@ -35,6 +43,11 @@ pub fn Trie(comptime V: type) type { len: usize, allocator: std.mem.Allocator, + /// Returns the number of elements in the trie. + pub fn count(self: *const Self) usize { + return self.len; + } + pub fn init(allocator: std.mem.Allocator) !Self { const root = try TrieNode.init(allocator); return Self{ @@ -49,6 +62,13 @@ pub fn Trie(comptime V: type) type { self.* = undefined; } + /// Removes all elements from the trie. + pub fn clear(self: *Self) !void { + self.root.deinit(self.allocator); + self.root = try TrieNode.init(self.allocator); + self.len = 0; + } + pub fn put(self: *Self, key: []const u8, value: V) !void { var current = self.root; @@ -88,7 +108,8 @@ pub fn Trie(comptime V: type) type { return self.findNode(prefix) != null; } - pub fn delete(self: *Self, key: []const u8) ?V { + /// Removes a key and returns its value if it existed. + pub fn remove(self: *Self, key: []const u8) ?V { const result = self.deleteRecursive(self.root, key, 0); if (result.deleted) { self.len -= 1; @@ -153,34 +174,85 @@ pub fn Trie(comptime V: type) type { return current; } - pub fn keysWithPrefix(self: *const Self, allocator: std.mem.Allocator, prefix: []const u8) !std.ArrayList([]u8) { - var results: std.ArrayList([]u8) = .{}; + /// Returns an iterator that yields all keys with the given prefix. + /// The iterator manages its own memory and will be automatically cleaned up on deinit. + pub fn keysWithPrefix(self: *const Self, allocator: std.mem.Allocator, prefix: []const u8) !PrefixIterator { + const prefix_node = self.findNode(prefix); + if (prefix_node == null) { + return PrefixIterator{ + .stack = std.ArrayList(PrefixIteratorFrame){}, + .allocator = allocator, + .current_key = std.ArrayList(u8){}, + .prefix_len = 0, + }; + } - const prefix_node = self.findNode(prefix) orelse return results; - try self.collectKeys(allocator, prefix_node, &results, prefix); + var stack = std.ArrayList(PrefixIteratorFrame){}; + try stack.append(allocator, PrefixIteratorFrame{ + .node = prefix_node.?, + .child_iter = prefix_node.?.children.iterator(), + .visited_self = false, + }); - return results; + var current_key = std.ArrayList(u8){}; + try current_key.appendSlice(allocator, prefix); + + return PrefixIterator{ + .stack = stack, + .allocator = allocator, + .current_key = current_key, + .prefix_len = prefix.len, + }; } - fn collectKeys(self: *const Self, allocator: std.mem.Allocator, node: *const TrieNode, results: *std.ArrayList([]u8), current_key: []const u8) !void { - if (node.is_end) { - const key_copy = try allocator.dupe(u8, current_key); - try results.append(allocator, key_copy); + pub const PrefixIteratorFrame = struct { + node: *const TrieNode, + child_iter: std.HashMap(u8, *TrieNode, std.hash_map.AutoContext(u8), std.hash_map.default_max_load_percentage).Iterator, + visited_self: bool, + }; + + pub const PrefixIterator = struct { + stack: std.ArrayList(PrefixIteratorFrame), + allocator: std.mem.Allocator, + current_key: std.ArrayList(u8), + prefix_len: usize, + + pub fn deinit(self: *PrefixIterator) void { + self.stack.deinit(self.allocator); + self.current_key.deinit(self.allocator); } - var iter = node.children.iterator(); - while (iter.next()) |entry| { - const char = entry.key_ptr.*; - const child = entry.value_ptr.*; + pub fn next(self: *PrefixIterator) !?[]const u8 { + while (self.stack.items.len > 0) { + var frame = &self.stack.items[self.stack.items.len - 1]; + + if (!frame.visited_self and frame.node.is_end) { + frame.visited_self = true; + return self.current_key.items; + } + + if (frame.child_iter.next()) |entry| { + const char = entry.key_ptr.*; + const child = entry.value_ptr.*; - var new_key = try allocator.alloc(u8, current_key.len + 1); - defer allocator.free(new_key); - @memcpy(new_key[0..current_key.len], current_key); - new_key[current_key.len] = char; + try self.current_key.append(self.allocator, char); - try self.collectKeys(allocator, child, results, new_key); + try self.stack.append(self.allocator, PrefixIteratorFrame{ + .node = child, + .child_iter = child.children.iterator(), + .visited_self = false, + }); + } else { + _ = self.stack.pop(); + // Don't pop below prefix length + if (self.current_key.items.len > self.prefix_len) { + _ = self.current_key.pop(); + } + } + } + return null; } - } + }; pub const Iterator = struct { stack: std.ArrayList(IteratorFrame), @@ -299,7 +371,7 @@ test "Trie: empty string key" { try std.testing.expectEqual(@as(usize, 1), trie.len); try std.testing.expectEqual(@as(i32, 42), trie.get("").?.*); - const deleted = trie.delete(""); + const deleted = trie.remove(""); try std.testing.expectEqual(@as(i32, 42), deleted.?); try std.testing.expectEqual(@as(usize, 0), trie.len); } @@ -330,7 +402,7 @@ test "Trie: delete with shared prefixes" { try trie.put("card", 2); try trie.put("care", 3); - const deleted = trie.delete("card"); + const deleted = trie.remove("card"); try std.testing.expectEqual(@as(i32, 2), deleted.?); try std.testing.expectEqual(@as(usize, 2), trie.len); try std.testing.expect(!trie.contains("card")); @@ -345,7 +417,7 @@ test "Trie: delete non-existent key" { try trie.put("hello", 1); - const deleted = trie.delete("world"); + const deleted = trie.remove("world"); try std.testing.expect(deleted == null); try std.testing.expectEqual(@as(usize, 1), trie.len); } @@ -357,7 +429,7 @@ test "Trie: delete prefix that is not a key" { try trie.put("testing", 1); - const deleted = trie.delete("test"); + const deleted = trie.remove("test"); try std.testing.expect(deleted == null); try std.testing.expectEqual(@as(usize, 1), trie.len); try std.testing.expect(trie.contains("testing")); @@ -433,9 +505,9 @@ test "Trie: delete all keys" { try trie.put("b", 2); try trie.put("c", 3); - _ = trie.delete("a"); - _ = trie.delete("b"); - _ = trie.delete("c"); + _ = trie.remove("a"); + _ = trie.remove("b"); + _ = trie.remove("c"); try std.testing.expectEqual(@as(usize, 0), trie.len); try std.testing.expect(!trie.hasPrefix("a")); From 011c34cafacdcb79330df12c449dcbb0c41e5021 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Mon, 27 Oct 2025 10:06:49 +0100 Subject: [PATCH 5/6] Add API documentation --- .github/workflows/benches.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/lints.yml | 2 +- .github/workflows/tests.yml | 2 +- examples/README.md | 16 ++++- examples/e1_btree_map.zig | 2 +- examples/e2_sorted_set.zig | 2 +- examples/e3_red_black_tree.zig | 2 +- src/lib.zig | 19 ++++-- src/ordered/btree_map.zig | 106 ++++++++++++++++++++++++++++++--- src/ordered/cartesian_tree.zig | 88 ++++++++++++++++++++++----- src/ordered/red_black_tree.zig | 88 ++++++++++++++++++++++++--- src/ordered/skip_list.zig | 34 +++++++++-- src/ordered/trie.zig | 79 ++++++++++++++++++++++-- 14 files changed, 392 insertions(+), 52 deletions(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 6f21c91..617e672 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -25,7 +25,7 @@ jobs: - name: Install Zig uses: goto-bus-stop/setup-zig@v2 with: - version: '0.15.1' + version: '0.15.2' - name: Install Dependencies run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 916ed06..b635e08 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Install Zig uses: goto-bus-stop/setup-zig@v2 with: - version: '0.15.1' + version: '0.15.2' - name: Install System Dependencies run: | diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index e57bae1..d84de84 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -23,7 +23,7 @@ jobs: - name: Install Zig uses: goto-bus-stop/setup-zig@v2 with: - version: '0.15.1' + version: '0.15.2' - name: Install Dependencies run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 712b7c8..369a255 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,7 +25,7 @@ jobs: - name: Install Zig uses: goto-bus-stop/setup-zig@v2 with: - version: '0.15.1' + version: '0.15.2' - name: Install Dependencies run: | diff --git a/examples/README.md b/examples/README.md index fc3efd4..1fbf780 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1,4 @@ -## Ordered Examples +### Usage Examples | # | File | Description | |---|------------------------------------------------|-----------------------------------------------------| @@ -8,3 +8,17 @@ | 4 | [e4_skip_list.zig](e4_skip_list.zig) | An example using the `SkipList` data structure | | 5 | [e5_trie.zig](e5_trie.zig) | An example using the `Trie` data structure | | 6 | [e6_cartesian_tree.zig](e6_cartesian_tree.zig) | An example using the `CartesianTree` data structure | + +### Running Examples + +To run an example, run the following command from the root of the repository: + +```zig +zig build run-{FILE_NAME_WITHOUT_EXTENSION} +``` + +For example: + +```zig +zig build run-e1_btree_map +``` diff --git a/examples/e1_btree_map.zig b/examples/e1_btree_map.zig index fd70565..aa7d969 100644 --- a/examples/e1_btree_map.zig +++ b/examples/e1_btree_map.zig @@ -9,7 +9,7 @@ pub fn main() !void { const allocator = std.heap.page_allocator; std.debug.print("## BTreeMap Example ##\n", .{}); - const B = 4; // Branching Factor + const B = 4; // Branching Factor for B-tree var map = ordered.BTreeMap([]const u8, u32, strCompare, B).init(allocator); defer map.deinit(); diff --git a/examples/e2_sorted_set.zig b/examples/e2_sorted_set.zig index a807020..9bd786f 100644 --- a/examples/e2_sorted_set.zig +++ b/examples/e2_sorted_set.zig @@ -15,7 +15,7 @@ pub fn main() !void { _ = try sorted_set.put(100); _ = try sorted_set.put(25); _ = try sorted_set.put(50); - const duplicate = try sorted_set.put(50); // Try adding duplicate + const duplicate = try sorted_set.put(50); // Try adding a duplicate std.debug.print("SortedSet count: {d}\n", .{sorted_set.count()}); std.debug.print("Added duplicate 50? {any}\n", .{duplicate}); diff --git a/examples/e3_red_black_tree.zig b/examples/e3_red_black_tree.zig index ad3f10a..6e30370 100644 --- a/examples/e3_red_black_tree.zig +++ b/examples/e3_red_black_tree.zig @@ -1,7 +1,7 @@ const std = @import("std"); const ordered = @import("ordered"); -// Context object for comparison, required by RedBlackTree +// Context object for comparison. This is needed by RedBlackTree const I32Context = struct { // This function must be public to be visible from the library code. pub fn lessThan(_: @This(), a: i32, b: i32) bool { diff --git a/src/lib.zig b/src/lib.zig index 2b21654..8c82455 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -4,12 +4,23 @@ //! Available Structures: //! - `SortedSet`: An ArrayList that maintains sort order on insertion. //! - `BTreeMap`: A cache-efficient B-Tree for mapping sorted keys to values. -//! - `SkipList`: A probabilistic data structure that maintains sorted order -//! using multiple linked lists. +//! - `SkipList`: A probabilistic data structure that maintains sorted order using multiple linked lists. //! - `Trie`: A prefix tree for efficient string operations and prefix matching. //! - `Red-Black Tree`: A self-balancing binary search tree. -//! - `Cartesian Tree`: A binary tree that maintains heap order based on a -//! secondary key, useful for priority queues. +//! - `Cartesian Tree`: A binary tree that maintains heap order based on a secondary key, useful for priority queues. +//! +//! Common API: +//! All structures has at least the following common API methods: +//! pub fn init(allocator) -> Self | !Self +//! pub fn deinit(self) void +//! pub fn clear(self) void +//! pub fn count(self) usize +//! pub fn contains(self, key) bool +//! pub fn put(self, key, [value]) !void +//! pub fn get(self, key) ?*const V +//! pub fn getPtr(self, key) ?*V +//! pub fn remove(self, key) ?V +//! pub fn iterator() -> Iterator pub const SortedSet = @import("ordered/sorted_set.zig").SortedSet; pub const BTreeMap = @import("ordered/btree_map.zig").BTreeMap; diff --git a/src/ordered/btree_map.zig b/src/ordered/btree_map.zig index 32d0e1a..d37e784 100644 --- a/src/ordered/btree_map.zig +++ b/src/ordered/btree_map.zig @@ -1,7 +1,19 @@ -//! A B-Tree based associative map. -//! This is a workhorse for most ordered map use cases. B-Trees are extremely -//! cache-friendly due to their high branching factor, making them faster than -//! binary search trees for larger datasets. +//! A B-tree based associative map with configurable branching factor. +//! +//! B-trees are self-balancing tree data structures that maintain sorted data and allow +//! searches, sequential access, insertions, and deletions in logarithmic time. They are +//! optimized for systems that read and write large blocks of data. +//! +//! ## Complexity +//! - Insert: O(log n) +//! - Remove: O(log n) +//! - Search: O(log n) +//! - Space: O(n) +//! +//! ## Use Cases +//! - Large datasets where cache efficiency matters +//! - Ordered key-value storage with frequent range queries +//! - Database indices and file systems //! //! ## Thread Safety //! This data structure is not thread-safe. External synchronization is required @@ -13,6 +25,23 @@ const std = @import("std"); +/// Creates a B-tree map type with the specified key type, value type, comparison function, +/// and branching factor. +/// +/// ## Parameters +/// - `K`: The key type. Must be comparable via the `compare` function. +/// - `V`: The value type. +/// - `compare`: Function that compares two keys and returns their ordering. +/// - `BRANCHING_FACTOR`: Number of children per node (must be >= 3). Higher values +/// improve cache efficiency but use more memory per node. Typical values: 4-16. +/// +/// ## Example +/// ```zig +/// fn i32Compare(a: i32, b: i32) std.math.Order { +/// return std.math.order(a, b); +/// } +/// var map = BTreeMap(i32, []const u8, i32Compare, 4).init(allocator); +/// ``` pub fn BTreeMap( comptime K: type, comptime V: type, @@ -36,21 +65,32 @@ pub fn BTreeMap( allocator: std.mem.Allocator, len: usize = 0, + /// Creates a new empty B-tree map. + /// + /// The map must be deinitialized with `deinit()` to free allocated memory. pub fn init(allocator: std.mem.Allocator) Self { return .{ .allocator = allocator }; } /// Returns the number of elements in the map. + /// + /// Time complexity: O(1) pub fn count(self: *const Self) usize { return self.len; } + /// Frees all memory used by the map. + /// + /// After calling this, the map is no longer usable. All references to keys + /// and values become invalid. pub fn deinit(self: *Self) void { self.clear(); self.* = undefined; } - /// Removes all elements from the map. + /// Removes all elements from the map while keeping the allocated structure. + /// + /// Time complexity: O(n) pub fn clear(self: *Self) void { if (self.root) |r| self.deinitNode(r); self.root = null; @@ -82,7 +122,18 @@ pub fn BTreeMap( return compare(key_as_context, item); } - /// Retrieves a pointer to the value associated with `key`. + /// Retrieves an immutable pointer to the value associated with the given key. + /// + /// Returns `null` if the key does not exist in the map. + /// + /// Time complexity: O(log n) + /// + /// ## Example + /// ```zig + /// if (map.get(42)) |value| { + /// std.debug.print("Value: {}\n", .{value.*}); + /// } + /// ``` pub fn get(self: *const Self, key: K) ?*const V { var current = self.root; while (current) |node| { @@ -97,7 +148,19 @@ pub fn BTreeMap( return null; } - /// Retrieves a mutable pointer to the value associated with `key`. + /// Retrieves a mutable pointer to the value associated with the given key. + /// + /// Returns `null` if the key does not exist. The returned pointer can be used + /// to modify the value in place without re-inserting. + /// + /// Time complexity: O(log n) + /// + /// ## Example + /// ```zig + /// if (map.getPtr(42)) |value_ptr| { + /// value_ptr.* += 10; // Modify in place + /// } + /// ``` pub fn getPtr(self: *Self, key: K) ?*V { var current = self.root; while (current) |node| { @@ -112,12 +175,25 @@ pub fn BTreeMap( return null; } - /// Returns true if the map contains the given key. + /// Checks whether the map contains the given key. + /// + /// Time complexity: O(log n) pub fn contains(self: *const Self, key: K) bool { return self.get(key) != null; } - /// Inserts a key-value pair. If the key exists, the value is updated. + /// Inserts a key-value pair into the map. If the key already exists, updates its value. + /// + /// Time complexity: O(log n) + /// + /// ## Errors + /// Returns `error.OutOfMemory` if allocation fails. + /// + /// ## Example + /// ```zig + /// try map.put(1, "one"); + /// try map.put(1, "ONE"); // Updates the value + /// ``` pub fn put(self: *Self, key: K, value: V) !void { var root_node = if (self.root) |r| r else { const new_node = try self.createNode(); @@ -233,6 +309,18 @@ pub fn BTreeMap( } } + /// Removes a key-value pair from the map and returns the value. + /// + /// Returns `null` if the key does not exist. + /// + /// Time complexity: O(log n) + /// + /// ## Example + /// ```zig + /// if (map.remove(42)) |value| { + /// std.debug.print("Removed value: {}\n", .{value}); + /// } + /// ``` pub fn remove(self: *Self, key: K) ?V { if (self.root == null) return null; const old_len = self.len; diff --git a/src/ordered/cartesian_tree.zig b/src/ordered/cartesian_tree.zig index 45ab754..fcf59a4 100644 --- a/src/ordered/cartesian_tree.zig +++ b/src/ordered/cartesian_tree.zig @@ -1,19 +1,41 @@ +//! A Cartesian Tree (Treap) implementation combining binary search tree and heap properties. +//! +//! A Cartesian Tree maintains two orderings simultaneously: +//! - BST property: keys are ordered (left < parent < right) +//! - Heap property: priorities determine tree structure (max-heap by default) +//! +//! This dual ordering makes it ideal for randomized balanced trees (treaps) and +//! range minimum/maximum query problems. +//! +//! ## Complexity +//! - Insert: O(log n) expected, O(n) worst case +//! - Remove: O(log n) expected, O(n) worst case +//! - Search: O(log n) expected, O(n) worst case +//! - Space: O(n) +//! +//! Note: With random priorities (using `put()`), operations are O(log n) expected. +//! Worst case O(n) occurs only with adversarial priority assignment. +//! +//! ## Use Cases +//! - Randomized balanced search trees (treaps with random priorities) +//! - Range minimum/maximum queries +//! - Persistent data structures (functional programming) +//! - When you need both ordering and priority-based structure +//! - Simpler alternative to AVL/Red-Black trees with similar performance +//! +//! ## Thread Safety +//! This data structure is not thread-safe. External synchronization is required +//! for concurrent access. +//! +//! ## Iterator Invalidation +//! WARNING: Modifying the tree (via put/remove/clear) while iterating will cause +//! undefined behavior. Complete all iterations before modifying the structure. + const std = @import("std"); const testing = std.testing; const Allocator = std.mem.Allocator; const Order = std.math.Order; -/// A Cartesian Tree implementation that maintains both BST property for keys -/// and heap property for priorities. Useful for range minimum queries and -/// as a treap data structure. -/// -/// ## Thread Safety -/// This data structure is not thread-safe. External synchronization is required -/// for concurrent access. -/// -/// ## Iterator Invalidation -/// WARNING: Modifying the tree (via put/remove/clear) while iterating will cause -/// undefined behavior. Complete all iterations before modifying the structure. pub fn CartesianTree(comptime K: type, comptime V: type) type { return struct { const Self = @This(); @@ -38,18 +60,27 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { allocator: Allocator, len: usize = 0, + /// Creates a new empty Cartesian Tree. + /// + /// ## Parameters + /// - `allocator`: Memory allocator for node allocation pub fn init(allocator: Allocator) Self { return Self{ .allocator = allocator, }; } + /// Frees all memory used by the tree. + /// + /// After calling this, the tree is no longer usable. pub fn deinit(self: *Self) void { self.clear(); self.* = undefined; } /// Removes all elements from the tree. + /// + /// Time complexity: O(n) pub fn clear(self: *Self) void { self.destroySubtree(self.root); self.root = null; @@ -64,13 +95,32 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { } } - /// Insert a key-value pair with random priority + /// Inserts a key-value pair with a random priority. + /// + /// Uses cryptographically random priorities to ensure expected O(log n) performance. + /// If the key already exists, updates its value and priority. + /// + /// Time complexity: O(log n) expected + /// + /// ## Errors + /// Returns `error.OutOfMemory` if node allocation fails. pub fn put(self: *Self, key: K, value: V) !void { const priority = std.crypto.random.int(u32); try self.putWithPriority(key, value, priority); } - /// Insert a key-value pair with explicit priority + /// Inserts a key-value pair with an explicit priority. + /// + /// Allows manual control over tree structure via priorities. Higher priorities + /// are placed closer to the root (max-heap property). Use this for testing or + /// when you need deterministic tree structure. + /// + /// If the key already exists, updates its value and priority. + /// + /// Time complexity: O(log n) expected with random priorities + /// + /// ## Errors + /// Returns `error.OutOfMemory` if node allocation fails. pub fn putWithPriority(self: *Self, key: K, value: V, priority: u32) !void { const new_node = try self.allocator.create(Node); new_node.* = Node.init(key, value, priority); @@ -143,7 +193,11 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { } } - /// Get value by key + /// Retrieves the value associated with the given key. + /// + /// Returns `null` if the key doesn't exist. + /// + /// Time complexity: O(log n) expected pub fn get(self: *const Self, key: K) ?V { return self.getNode(self.root, key); } @@ -161,7 +215,11 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { }; } - /// Get mutable pointer to value by key + /// Retrieves a mutable pointer to the value associated with the given key. + /// + /// Returns `null` if the key doesn't exist. Allows in-place modification of the value. + /// + /// Time complexity: O(log n) expected pub fn getPtr(self: *Self, key: K) ?*V { return self.getNodePtr(self.root, key); } diff --git a/src/ordered/red_black_tree.zig b/src/ordered/red_black_tree.zig index 054be82..55b3ef4 100644 --- a/src/ordered/red_black_tree.zig +++ b/src/ordered/red_black_tree.zig @@ -1,18 +1,56 @@ +//! Red-Black Tree - A self-balancing binary search tree. +//! +//! Red-Black Trees guarantee O(log n) time complexity for insert, delete, and search +//! operations by maintaining balance through color properties and rotations. They are +//! widely used in standard libraries (e.g., C++ std::map, Java TreeMap). +//! +//! ## Complexity +//! - Insert: O(log n) +//! - Remove: O(log n) +//! - Search: O(log n) +//! - Space: O(n) +//! +//! ## Properties +//! 1. Every node is either red or black +//! 2. The root is always black +//! 3. All leaves (NIL) are black +//! 4. Red nodes have black children (no two red nodes in a row) +//! 5. All paths from root to leaves contain the same number of black nodes +//! +//! ## Use Cases +//! - Ordered set/map with guaranteed O(log n) operations +//! - When worst-case performance matters more than average case +//! - Standard library implementations of associative containers +//! +//! ## Thread Safety +//! This data structure is not thread-safe. External synchronization is required +//! for concurrent access. +//! +//! ## Iterator Invalidation +//! WARNING: Modifying the tree (via put/remove/clear) while iterating will +//! cause undefined behavior. Complete all iterations before modifying the structure. + const std = @import("std"); const Allocator = std.mem.Allocator; const testing = std.testing; const assert = std.debug.assert; -/// Red-Black Tree implementation -/// A self-balancing binary search tree with O(log n) operations. + +/// Creates a Red-Black Tree type for the given data type and comparison context. /// -/// ## Thread Safety -/// This data structure is not thread-safe. External synchronization is required -/// for concurrent access. +/// ## Parameters +/// - `T`: The data type to store in the tree +/// - `Context`: A type providing a `lessThan(ctx, a, b) bool` method for comparison /// -/// ## Iterator Invalidation -/// WARNING: Modifying the tree (via insert/remove/clear) while iterating will -/// cause undefined behavior. Complete all iterations before modifying the structure. +/// ## Example +/// ```zig +/// const Context = struct { +/// pub fn lessThan(_: @This(), a: i32, b: i32) bool { +/// return a < b; +/// } +/// }; +/// var tree = RedBlackTree(i32, Context).init(allocator, .{}); +/// ``` pub fn RedBlackTree(comptime T: type, comptime Context: type) type { return struct { const Self = @This(); @@ -40,6 +78,11 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { context: Context, size: usize, + /// Creates a new empty Red-Black Tree. + /// + /// ## Parameters + /// - `allocator`: Memory allocator for node allocation + /// - `context`: Comparison context instance pub fn init(allocator: Allocator, context: Context) Self { return Self{ .root = null, @@ -49,10 +92,16 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { }; } + /// Frees all memory used by the tree. + /// + /// After calling this, the tree is no longer usable. pub fn deinit(self: *Self) void { self.clear(); } + /// Removes all elements from the tree. + /// + /// Time complexity: O(n) pub fn clear(self: *Self) void { self.clearNode(self.root); self.root = null; @@ -67,11 +116,22 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { } } + /// Returns the number of elements in the tree. + /// + /// Time complexity: O(1) pub fn count(self: Self) usize { return self.size; } /// Inserts or updates a value in the tree. + /// + /// If the value already exists (as determined by the context's lessThan method), + /// it will be updated. Otherwise, a new node is created. + /// + /// Time complexity: O(log n) + /// + /// ## Errors + /// Returns `error.OutOfMemory` if node allocation fails. pub fn put(self: *Self, data: T) !void { // Check if key already exists first to avoid unnecessary allocation if (self.get(data)) |existing| { @@ -177,6 +237,10 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { } /// Removes a value from the tree and returns it if it existed. + /// + /// Returns `null` if the value is not found. + /// + /// Time complexity: O(log n) pub fn remove(self: *Self, data: T) ?T { const node = self.get(data) orelse return null; const value = node.data; @@ -360,6 +424,11 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { } /// Returns a pointer to the node containing the data. + /// + /// Returns `null` if the data is not found. The returned node pointer can be used + /// to access or modify the data directly. + /// + /// Time complexity: O(log n) pub fn get(self: Self, data: T) ?*Node { var current = self.root; @@ -376,6 +445,9 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { return null; } + /// Checks whether the tree contains the given value. + /// + /// Time complexity: O(log n) pub fn contains(self: Self, data: T) bool { return self.get(data) != null; } diff --git a/src/ordered/skip_list.zig b/src/ordered/skip_list.zig index e910215..f5362b3 100644 --- a/src/ordered/skip_list.zig +++ b/src/ordered/skip_list.zig @@ -1,18 +1,38 @@ //! A probabilistic data structure built in layers of linked lists. -//! SkipList offers O(log n) performance on average and is simpler to implement -//! correctly than balanced binary trees. It uses less memory per-node than B-Trees -//! and has excellent concurrent-friendly properties. +//! +//! Skip lists provide O(log n) search, insertion, and deletion with high probability. +//! They are simpler to implement than balanced trees and have excellent cache locality. +//! The probabilistic nature means no complex rebalancing operations are needed. +//! +//! ## Complexity +//! - Insert: O(log n) average, O(n) worst case +//! - Remove: O(log n) average, O(n) worst case +//! - Search: O(log n) average, O(n) worst case +//! - Space: O(n log n) average +//! +//! ## Use Cases +//! - Ordered key-value storage with simpler implementation than trees +//! - Concurrent data structures (with proper synchronization) +//! - When you need fast search and insert without rebalancing overhead //! //! ## Thread Safety //! This implementation is not thread-safe. External synchronization is required //! for concurrent access. //! //! ## Iterator Invalidation -//! WARNING: Modifying the skip list (via put/delete/clear) while iterating will +//! WARNING: Modifying the skip list (via put/remove/clear) while iterating will //! cause undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); +/// Creates a skip list type with specified key, value, comparison function, and max level. +/// +/// ## Parameters +/// - `K`: The key type +/// - `V`: The value type +/// - `compare`: Comparison function for keys +/// - `MAX_LEVEL`: Maximum height of the skip list (1-32). Higher values allow more +/// elements but use more memory. Typical value: 16. pub fn SkipList( comptime K: type, comptime V: type, @@ -50,10 +70,16 @@ pub fn SkipList( rng: std.Random.DefaultPrng, /// Returns the number of elements in the skip list. + /// + /// Time complexity: O(1) pub fn count(self: *const Self) usize { return self.len; } + /// Creates a new empty skip list. + /// + /// ## Errors + /// Returns `error.OutOfMemory` if allocation fails. pub fn init(allocator: std.mem.Allocator) !Self { const header = try allocator.create(Node); header.key = undefined; diff --git a/src/ordered/trie.zig b/src/ordered/trie.zig index 53cdd2a..5d6c56e 100644 --- a/src/ordered/trie.zig +++ b/src/ordered/trie.zig @@ -1,17 +1,43 @@ //! A Trie (prefix tree) data structure for efficient string storage and retrieval. -//! Tries excel at prefix-based operations like autocomplete, word validation, -//! and prefix matching. They provide O(m) complexity where m is the key length. +//! +//! Tries are tree structures where each node represents a character in a string. They +//! excel at prefix-based operations like autocomplete, spell checking, and IP routing. +//! Unlike hash tables, tries support ordered iteration and prefix queries. +//! +//! ## Complexity +//! - Insert: O(m) where m is key length +//! - Remove: O(m) where m is key length +//! - Search: O(m) where m is key length +//! - Prefix search: O(m + k) where k is number of results +//! - Space: O(ALPHABET_SIZE * m * n) worst case, much better in practice with shared prefixes +//! +//! ## Use Cases +//! - Autocomplete and typeahead search +//! - Spell checkers and dictionaries +//! - IP routing tables (prefix matching) +//! - String matching and pattern search +//! - When you need both exact and prefix matching //! //! ## Thread Safety //! This data structure is not thread-safe. External synchronization is required //! for concurrent access. //! //! ## Iterator Invalidation -//! WARNING: Modifying the trie (via put/delete/clear) while iterating will cause +//! WARNING: Modifying the trie (via put/remove/clear) while iterating will cause //! undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); +/// Creates a Trie type that maps string keys to values of type V. +/// +/// ## Example +/// ```zig +/// var trie = try Trie([]const u8).init(allocator); +/// try trie.put("hello", "world"); +/// if (trie.get("hello")) |value| { +/// std.debug.print("{s}\n", .{value.*}); +/// } +/// ``` pub fn Trie(comptime V: type) type { return struct { const Self = @This(); @@ -44,10 +70,16 @@ pub fn Trie(comptime V: type) type { allocator: std.mem.Allocator, /// Returns the number of elements in the trie. + /// + /// Time complexity: O(1) pub fn count(self: *const Self) usize { return self.len; } + /// Creates a new empty trie. + /// + /// ## Errors + /// Returns `error.OutOfMemory` if allocation fails. pub fn init(allocator: std.mem.Allocator) !Self { const root = try TrieNode.init(allocator); return Self{ @@ -57,18 +89,35 @@ pub fn Trie(comptime V: type) type { }; } + /// Frees all memory used by the trie. + /// + /// After calling this, the trie is no longer usable. pub fn deinit(self: *Self) void { self.root.deinit(self.allocator); self.* = undefined; } - /// Removes all elements from the trie. + /// Removes all elements from the trie while keeping the root allocated. + /// + /// Time complexity: O(n) where n is total number of nodes + /// + /// ## Errors + /// Returns `error.OutOfMemory` if root node reallocation fails. pub fn clear(self: *Self) !void { self.root.deinit(self.allocator); self.root = try TrieNode.init(self.allocator); self.len = 0; } + /// Inserts a key-value pair into the trie. + /// + /// If the key already exists, updates its value. Creates nodes as needed + /// for each character in the key. + /// + /// Time complexity: O(m) where m is the key length + /// + /// ## Errors + /// Returns `error.OutOfMemory` if node allocation fails. pub fn put(self: *Self, key: []const u8, value: V) !void { var current = self.root; @@ -87,28 +136,50 @@ pub fn Trie(comptime V: type) type { current.is_end = true; } + /// Retrieves an immutable pointer to the value associated with the given key. + /// + /// Returns `null` if the key doesn't exist. + /// + /// Time complexity: O(m) where m is the key length pub fn get(self: *const Self, key: []const u8) ?*const V { const node = self.findNode(key) orelse return null; if (!node.is_end) return null; return &node.value.?; } + /// Retrieves a mutable pointer to the value associated with the given key. + /// + /// Returns `null` if the key doesn't exist. Allows in-place modification. + /// + /// Time complexity: O(m) where m is the key length pub fn getPtr(self: *Self, key: []const u8) ?*V { const node = self.findNodeMut(key) orelse return null; if (!node.is_end) return null; return &node.value.?; } + /// Checks whether the trie contains an exact match for the given key. + /// + /// Time complexity: O(m) where m is the key length pub fn contains(self: *const Self, key: []const u8) bool { const node = self.findNode(key) orelse return false; return node.is_end; } + /// Checks whether any keys in the trie start with the given prefix. + /// + /// Returns true even if the prefix itself is not a complete key. + /// + /// Time complexity: O(m) where m is the prefix length pub fn hasPrefix(self: *const Self, prefix: []const u8) bool { return self.findNode(prefix) != null; } /// Removes a key and returns its value if it existed. + /// + /// Returns `null` if the key doesn't exist. Prunes nodes that become unnecessary. + /// + /// Time complexity: O(m) where m is the key length pub fn remove(self: *Self, key: []const u8) ?V { const result = self.deleteRecursive(self.root, key, 0); if (result.deleted) { From c698dd76358ff4b85ba850acd1897d675ea6c6ba Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Mon, 27 Oct 2025 11:00:27 +0100 Subject: [PATCH 6/6] Add issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 25 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++ .github/workflows/benches.yml | 2 -- .github/workflows/tests.yml | 2 -- README.md | 15 +++++++++----- src/ordered/red_black_tree.zig | 1 - 7 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..db16dc5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +If applicable, add logs to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..bbdefd2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Discussions + url: https://github.com/CogitatorTech/ordered/discussions + about: Please ask and answer general questions here diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..11fc491 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 617e672..b45c3bd 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -3,8 +3,6 @@ name: Run Benchmarks on: workflow_dispatch: push: - branches: - - main tags: - 'v*' pull_request: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 369a255..5ee09eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,8 +3,6 @@ name: Run Tests on: workflow_dispatch: push: - branches: - - main tags: - 'v*' pull_request: diff --git a/README.md b/README.md index e6696ab..08ce628 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,27 @@

Ordered

[![Tests](https://img.shields.io/github/actions/workflow/status/CogitatorTech/ordered/tests.yml?label=tests&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/actions/workflows/tests.yml) -[![Benchmarks](https://img.shields.io/github/actions/workflow/status/CogitatorTech/ordered/benches.yml?label=benches&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/actions/workflows/benches.yml) +[![Benchmarks](https://img.shields.io/github/actions/workflow/status/CogitatorTech/ordered/benches.yml?label=benchmarks&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/actions/workflows/benches.yml) [![CodeFactor](https://img.shields.io/codefactor/grade/github/CogitatorTech/ordered?label=quality&style=flat&labelColor=282c34&logo=codefactor)](https://www.codefactor.io/repository/github/CogitatorTech/ordered) +[![Zig Version](https://img.shields.io/badge/Zig-0.15.2-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) +
[![Docs](https://img.shields.io/badge/docs-view-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/ordered/) [![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/ordered/tree/main/examples) -[![Zig Version](https://img.shields.io/badge/Zig-0.15.2-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) [![Release](https://img.shields.io/github/release/CogitatorTech/ordered.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/releases/latest) [![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/CogitatorTech/ordered/blob/main/LICENSE) -A collection of data structures that keep data in order in pure Zig +Pure Zig implementations of high-performance, memory-safe ordered data structures --- -Ordered is a Zig library that provides fast and efficient implementations of various popular data structures including +Ordered is a Zig library that provides efficient implementations of various popular data structures including B-tree, skip list, trie, and red-black tree for Zig programming language. ### Features -- Fast and efficient implementations +To be added. ### Data Structures @@ -67,6 +68,10 @@ and view in a web browser. Check out the [examples](examples) directory for example usages of Ordered. +### Benchmarks + +To be added. + --- ### Contributing diff --git a/src/ordered/red_black_tree.zig b/src/ordered/red_black_tree.zig index 55b3ef4..4087889 100644 --- a/src/ordered/red_black_tree.zig +++ b/src/ordered/red_black_tree.zig @@ -35,7 +35,6 @@ const Allocator = std.mem.Allocator; const testing = std.testing; const assert = std.debug.assert; - /// Creates a Red-Black Tree type for the given data type and comparison context. /// /// ## Parameters