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 6f21c91..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:
@@ -25,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/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..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:
@@ -25,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/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 a55c66f..08ce628 100644
--- a/README.md
+++ b/README.md
@@ -7,42 +7,38 @@
Ordered
[](https://github.com/CogitatorTech/ordered/actions/workflows/tests.yml)
-[](https://github.com/CogitatorTech/ordered/actions/workflows/benches.yml)
+[](https://github.com/CogitatorTech/ordered/actions/workflows/benches.yml)
[](https://www.codefactor.io/repository/github/CogitatorTech/ordered)
+[](https://ziglang.org/download/)
+
[](https://CogitatorTech.github.io/ordered/)
[](https://github.com/CogitatorTech/ordered/tree/main/examples)
-[](https://ziglang.org/download/)
[](https://github.com/CogitatorTech/ordered/releases/latest)
[](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.
-### Supported Data Structures
+### Features
-Currently supported data structures include:
+To be added.
-- [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 Structures
-| # | 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)$ |
+| 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
@@ -72,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/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/build.zig.zon b/build.zig.zon
index 015df48..77e0f54 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,8 +1,8 @@
.{
.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",
+ .minimum_zig_version = "0.15.2",
.paths = .{
"build.zig",
"build.zig.zon",
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 463cf7d..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();
@@ -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..9bd786f 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 a 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..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 {
@@ -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/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)"
]
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 58a4634..d37e784 100644
--- a/src/ordered/btree_map.zig
+++ b/src/ordered/btree_map.zig
@@ -1,10 +1,47 @@
-//! 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
+//! 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");
+/// 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,
@@ -28,15 +65,38 @@ 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 {
- if (self.root) |r| self.deinitNode(r);
+ self.clear();
self.* = undefined;
}
+ /// 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;
+ self.len = 0;
+ }
+
fn deinitNode(self: *Self, node: *Node) void {
if (!node.is_leaf) {
for (node.children[0 .. node.len + 1]) |child| {
@@ -62,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| {
@@ -77,14 +148,53 @@ 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 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| {
+ 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;
+ }
+
+ /// 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 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();
new_node.keys[0] = key;
@@ -104,8 +214,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 +265,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 +286,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,10 +305,22 @@ pub fn BTreeMap(
i += 1;
}
}
- self.insertNonFull(node.children[i].?, key, value);
+ return self.insertNonFull(node.children[i].?, key, value);
}
}
+ /// 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;
@@ -227,18 +373,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..fcf59a4 100644
--- a/src/ordered/cartesian_tree.zig
+++ b/src/ordered/cartesian_tree.zig
@@ -1,11 +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.
pub fn CartesianTree(comptime K: type, comptime V: type) type {
return struct {
const Self = @This();
@@ -30,17 +60,33 @@ 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.destroySubtree(self.root);
+ 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;
+ self.len = 0;
+ }
+
fn destroySubtree(self: *Self, node: ?*Node) void {
if (node) |n| {
self.destroySubtree(n.left);
@@ -49,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);
@@ -128,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);
}
@@ -146,41 +215,64 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type {
};
}
- /// Remove key from tree
- pub fn remove(self: *Self, key: K) bool {
+ /// 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);
+ }
+
+ 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 };
}
}
@@ -220,12 +312,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 +325,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 +351,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);
}
};
@@ -288,7 +380,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 +394,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 +406,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 +475,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 +487,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);
@@ -424,14 +517,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/red_black_tree.zig b/src/ordered/red_black_tree.zig
index 77e9605..4087889 100644
--- a/src/ordered/red_black_tree.zig
+++ b/src/ordered/red_black_tree.zig
@@ -1,10 +1,55 @@
+//! 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.
+///
+/// ## Parameters
+/// - `T`: The data type to store in the tree
+/// - `Context`: A type providing a `lessThan(ctx, a, b) bool` method for comparison
+///
+/// ## 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();
@@ -32,6 +77,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,
@@ -41,10 +91,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;
@@ -59,13 +115,25 @@ 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;
}
- pub fn insert(self: *Self, data: T) !void {
+ /// 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.find(data)) |existing| {
+ if (self.get(data)) |existing| {
existing.data = data;
return;
}
@@ -167,11 +235,17 @@ 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.
+ ///
+ /// 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;
self.removeNode(node);
self.size -= 1;
- return true;
+ return value;
}
fn removeNode(self: *Self, node: *Node) void {
@@ -348,7 +422,13 @@ 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.
+ ///
+ /// 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;
while (current) |node| {
@@ -364,8 +444,11 @@ 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.find(data) != null;
+ return self.get(data) != null;
}
fn findMinimum(self: Self, node: *Node) *Node {
@@ -459,9 +542,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 +559,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 +567,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 +583,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 +598,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 +617,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 +629,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 +648,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 +663,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 +695,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 +709,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..f5362b3 100644
--- a/src/ordered/skip_list.zig
+++ b/src/ordered/skip_list.zig
@@ -1,10 +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/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,
@@ -41,6 +69,17 @@ pub fn SkipList(
allocator: std.mem.Allocator,
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;
@@ -60,15 +99,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 +220,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 +316,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 +374,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 +387,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 +439,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 +453,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 4435ed7..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,9 +23,14 @@ 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 = .{},
+ .items = std.ArrayList(T){},
.allocator = allocator,
};
}
@@ -25,14 +39,25 @@ 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 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 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) {
+ return false;
+ }
try self.items.insert(self.allocator, index, value);
+ return true;
}
/// Removes an element at a given index.
@@ -61,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));
@@ -89,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);
@@ -98,17 +123,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.put(10);
+ const added2 = try vec.put(10);
+ const added3 = try vec.put(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 +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);
}
@@ -132,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
@@ -147,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..5d6c56e 100644
--- a/src/ordered/trie.zig
+++ b/src/ordered/trie.zig
@@ -1,9 +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/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();
@@ -35,6 +69,17 @@ pub fn Trie(comptime V: type) type {
len: usize,
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{
@@ -44,11 +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 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;
@@ -67,28 +136,51 @@ 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;
}
- pub fn delete(self: *Self, key: []const u8) ?V {
+ /// 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) {
self.len -= 1;
@@ -153,34 +245,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 +442,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 +473,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 +488,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 +500,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 +576,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"));