diff --git a/README.md b/README.md index 08ce628..6a66df2 100644 --- a/README.md +++ b/README.md @@ -16,33 +16,51 @@ [![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) -Pure Zig implementations of high-performance, memory-safe ordered data structures +A sorted collection library for Zig --- -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. +Ordered is a Zig library that provides fast and efficient implementations of various data structures that keep elements +sorted (AKA sorted collections). +It is written in pure Zig and has no external dependencies. +Ordered is inspired by [Java collections](https://en.wikipedia.org/wiki/Java_collections_framework) and sorted +containers in the [C++ standard library](https://en.cppreference.com/w/cpp/container), and aims to provide a similar +experience in Zig. ### Features -To be added. +- Simple and uniform API for all data structures +- Pure Zig implementations with no external dependencies +- Fast and memory-efficient implementations (see [benches](benches)) ### 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)$ | +Ordered provides two main interfaces for working with sorted collections: sorted maps and sorted sets. +At the moment, Ordered supports the following implementations of these interfaces: -- $n$: number of stored elements -- $m$: maximum length of a key -- \*: average case complexity +#### Maps (Key-value) + +| Type | Data Structure | Insert | Search | Delete | Space | +|--------------------|------------------------------------------------------|--------------|--------------|--------------|----------------| +| `BTreeMap` | [B-tree](https://en.wikipedia.org/wiki/B-tree) | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | $O(n)$ | +| `SkipListMap` | [Skip list](https://en.wikipedia.org/wiki/Skip_list) | $O(\log n)$† | $O(\log n)$† | $O(\log n)$† | $O(n)$ | +| `TrieMap` | [Trie](https://en.wikipedia.org/wiki/Trie) | $O(m)$ | $O(m)$ | $O(m)$ | $O(n \cdot m)$ | +| `CartesianTreeMap` | [Treap](https://en.wikipedia.org/wiki/Treap) | $O(\log n)$† | $O(\log n)$† | $O(\log n)$† | $O(n)$ | + +#### Sets (Value-only) + +| Type | Data Structure | Insert | Search | Delete | Space | +|-------------------|----------------------------------------------------------------|-------------|-------------|-------------|--------| +| `SortedSet` | [Sorted array](https://en.wikipedia.org/wiki/Sorted_array) | $O(n)$ | $O(\log n)$ | $O(n)$ | $O(n)$ | +| `RedBlackTreeSet` | [Red-black tree](https://en.wikipedia.org/wiki/Red-black_tree) | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | $O(n)$ | + +- $n$ = number of elements stored +- $m$ = length of the key (for string-based keys) +- † = average case complexity (the worst case is $O(n)$) + +See the [ROADMAP.md](ROADMAP.md) for the list of implemented and planned features. > [!IMPORTANT] > Ordered is in early development, so bugs and breaking API changes are expected. @@ -52,7 +70,91 @@ To be added. ### Getting Started -To be added. +You can add Ordered to your project and start using it by following the steps below. + +#### Installation + +Run the following command in the root directory of your project to download Ordered: + +```sh +zig fetch --save=ordered "https://github.com/CogitatorTech/ordered/archive/.tar.gz" +``` + +Replace `` with the desired branch or release tag, like `main` (for the development version) or `v0.1.0`. +This command will download Ordered and add it to Zig's global cache and update your project's `build.zig.zon` file. + +#### Adding to Build Script + +Next, modify your `build.zig` file to make Ordered available to your build target as a module. + +```zig +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // 1. Get the dependency object from the builder + const ordered_dep = b.dependency("ordered", .{}); + + // 2. Create a module for the dependency + const ordered_module = ordered_dep.module("ordered"); + + // 3. Create your executable module and add ordered as import + const exe_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + exe_module.addImport("ordered", ordered_module); + + // 4. Create executable with the module + const exe = b.addExecutable(.{ + .name = "your-application", + .root_module = exe_module, + }); + + b.installArtifact(exe); +} +``` + +#### Using Ordered in Your Project + +Finally, you can `@import("ordered")` and start using it in your Zig code. + +```zig +const std = @import("std"); +const ordered = @import("ordered"); + +// Define a comparison function for the keys. +// The function must return a `std.math.Order` value based on the comparison of the two keys +fn strCompare(lhs: []const u8, rhs: []const u8) std.math.Order { + return std.mem.order(u8, lhs, rhs); +} + +pub fn main() !void { + const allocator = std.heap.page_allocator; + + std.debug.print("## BTreeMap Example ##\n", .{}); + const B = 4; // Branching Factor for B-tree + var map = ordered.BTreeMap([]const u8, u32, strCompare, B).init(allocator); + defer map.deinit(); + + try map.put("banana", 150); + try map.put("apple", 100); + try map.put("cherry", 200); + + const key_to_find = "apple"; + if (map.get(key_to_find)) |value_ptr| { + std.debug.print("Found key '{s}': value is {d}\n", .{ key_to_find, value_ptr.* }); + } + + 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()}); +} +``` --- @@ -70,7 +172,7 @@ Check out the [examples](examples) directory for example usages of Ordered. ### Benchmarks -To be added. +Check out the [benchmarks](benches) directory for local benchmarks. --- diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..a6fece5 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,23 @@ +## Feature Roadmap + +This document includes the roadmap for the Ordered library. +It outlines features to be implemented and their current status. + +> [!IMPORTANT] +> This roadmap is a work in progress and is subject to change. + + +### 1. Core Features + +* **Collections (implemented)** + * [x] B-tree map (`BTreeMap`) + * [x] Skip list map (`SkipListMap`) + * [x] Trie map (`TrieMap`) + * [x] Cartesian tree (`CartesianTreeMap`) + * [x] Array-based sorted set (`SortedSet`) + * [x] Red-black tree set (`RedBlackTreeSet`) +* **Common API** + * [x] `init` and `deinit` lifecycle + * [x] `put`, `get`, `remove`, and `contains` + * [x] Iterators (in-order traversal) + * [x] Size and emptiness checks diff --git a/benches/README.md b/benches/README.md index ebbb48f..616cf5f 100644 --- a/benches/README.md +++ b/benches/README.md @@ -1,87 +1,39 @@ -## Benchmarks - -This directory contains benchmarks for the data structures in the `ordered` library. - -### Running Benchmarks - -Each benchmark can be run using the following command pattern: - -```bash -zig build bench- -``` +### Ordered Benchmarks #### Available Benchmarks -- **BTreeMap**: `zig build bench-btree_map_bench` -- **SortedSet**: `zig build bench-sorted_set_bench` -- **RedBlackTree**: `zig build bench-red_black_tree_bench` -- **SkipList**: `zig build bench-skip_list_bench` -- **Trie**: `zig build bench-trie_bench` -- **CartesianTree**: `zig build bench-cartesian_tree_bench` - -### What Each Benchmark Tests - -#### BTreeMap Benchmark -- **Insert**: Sequential insertion of integers -- **Lookup**: Finding all inserted keys -- **Delete**: Removing all keys - -#### SortedSet Benchmark -- **Add**: Adding elements while maintaining a sorted order -- **Contains**: Checking if elements exist -- **Remove**: Removing elements from the set +| # | File | Description | +|---|--------------------------------------------------------|--------------------------------------------------| +| 1 | [b1_btree_map.zig](b1_btree_map.zig) | Benchmarks for B-tree map implementation | +| 2 | [b2_sorted_set.zig](b2_sorted_set.zig) | Benchmarks for Sorted set implementation | +| 3 | [b3_red_black_tree_set.zig](b3_red_black_tree_set.zig) | Benchmarks for Red-black tree set implementation | +| 4 | [b4_skip_list_map.zig](b4_skip_list_map.zig) | Benchmarks for Skip list map implementation | +| 5 | [b5_trie_map.zig](b5_trie_map.zig) | Benchmarks for Trie map implementation | +| 6 | [b6_cartesian_tree_map.zig](b6_cartesian_tree_map.zig) | Benchmarks for Cartesian tree map implementation | -#### RedBlackTree Benchmark -- **Insert**: Inserting nodes with self-balancing -- **Find**: Searching for nodes -- **Remove**: Deleting nodes while maintaining balance -- **Iterator**: In-order traversal performance +#### Running Benchmarks -#### SkipList Benchmark -- **Put**: Inserting key-value pairs with probabilistic levels -- **Get**: Retrieving values by key -- **Delete**: Removing key-value pairs - -#### Trie Benchmark -- **Put**: Inserting strings with associated values -- **Get**: Retrieving values by string key -- **Contains**: Checking if strings exist -- **Prefix Search**: Finding all keys with a common prefix - -#### CartesianTree Benchmark -- **Put**: Inserting key-value pairs with random priorities -- **Get**: Retrieving values by key -- **Remove**: Deleting nodes -- **Iterator**: In-order traversal performance - -### Benchmark Sizes - -Each benchmark tests with multiple dataset sizes: -- Small: 1,000 items -- Medium: 10,000 items -- Large: 50,000 - 100,000 items (varies by data structure) - -### Build Configuration - -Benchmarks are compiled with `ReleaseFast` optimization mode for accurate performance measurements. - -### Example Output +To execute a specific benchmark, run: +```sh +zig build bench-{FILE_NAME_WITHOUT_EXTENSION} ``` -=== BTreeMap Benchmark === -Insert 1000 items: 0.42 ms (420 ns/op) -Lookup 1000 items: 0.18 ms (180 ns/op, found: 1000) -Delete 1000 items: 0.35 ms (350 ns/op) +For example: -Insert 10000 items: 5.23 ms (523 ns/op) -Lookup 10000 items: 2.10 ms (210 ns/op, found: 10000) -Delete 10000 items: 4.15 ms (415 ns/op) +```sh +zig build bench-b1_btree_map ``` -### Notes - -- All benchmarks use a simple integer or string key type for consistency -- Times are reported in both total milliseconds and nanoseconds per operation -- Memory allocations use `GeneralPurposeAllocator` for simulating a more realistic memory usage -- Results may vary based on a hardware and system load +> [!NOTE] +> Each benchmark measures three core operations across multiple data sizes: +> 1. **Insert and Put**: measures the time to insert elements sequentially into an empty data structure +> 2. **Lookup**: measures the time to search for all elements in a pre-populated structure +> 3. **Delete**: measures the time to remove all elements from a pre-populated structure +> +> **Test Sizes**: benchmarks run with 1,000, 10,000, 100,000, and 1,000,000 elements to show performance scaling. +> +> **Timing Method**: uses `std.time.Timer` for high-precision nanosecond-level timing. Each operation is timed in bulk, +> then divided by the number of operations to get per-operation timing. +> +> **Compilation**: benchmarks are compiled with `ReleaseFast` optimization (see [build.zig](../build.zig)). diff --git a/benches/b1_btree_map.zig b/benches/b1_btree_map.zig index c7757dd..2ab171a 100644 --- a/benches/b1_btree_map.zig +++ b/benches/b1_btree_map.zig @@ -9,7 +9,7 @@ pub fn main() !void { std.debug.print("=== BTreeMap Benchmark ===\n\n", .{}); - const sizes = [_]usize{ 1000, 10_000, 100_000 }; + const sizes = [_]usize{ 1000, 10_000, 100_000, 1_000_000 }; inline for (sizes) |size| { try benchmarkInsert(allocator, size); diff --git a/benches/b2_sorted_set.zig b/benches/b2_sorted_set.zig index 282dde7..8adabdd 100644 --- a/benches/b2_sorted_set.zig +++ b/benches/b2_sorted_set.zig @@ -9,7 +9,7 @@ pub fn main() !void { std.debug.print("=== SortedSet Benchmark ===\n\n", .{}); - const sizes = [_]usize{ 1000, 10_000, 50_000 }; + const sizes = [_]usize{ 1000, 10_000, 100_000, 1_000_000 }; inline for (sizes) |size| { try benchmarkAdd(allocator, size); diff --git a/benches/b3_red_black_tree.zig b/benches/b3_red_black_tree_set.zig similarity index 88% rename from benches/b3_red_black_tree.zig rename to benches/b3_red_black_tree_set.zig index 1666c9a..22a6419 100644 --- a/benches/b3_red_black_tree.zig +++ b/benches/b3_red_black_tree_set.zig @@ -9,7 +9,7 @@ pub fn main() !void { std.debug.print("=== RedBlackTree Benchmark ===\n\n", .{}); - const sizes = [_]usize{ 1000, 10_000, 100_000 }; + const sizes = [_]usize{ 1000, 10_000, 100_000, 1_000_000 }; inline for (sizes) |size| { try benchmarkInsert(allocator, size); @@ -28,7 +28,7 @@ const Context = struct { }; fn benchmarkInsert(allocator: std.mem.Allocator, size: usize) !void { - var tree = ordered.RedBlackTree(i32, Context).init(allocator, Context{}); + var tree = ordered.RedBlackTreeSet(i32, Context).init(allocator, Context{}); defer tree.deinit(); var timer = try Timer.start(); @@ -50,7 +50,7 @@ fn benchmarkInsert(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkFind(allocator: std.mem.Allocator, size: usize) !void { - var tree = ordered.RedBlackTree(i32, Context).init(allocator, Context{}); + var tree = ordered.RedBlackTreeSet(i32, Context).init(allocator, Context{}); defer tree.deinit(); var i: i32 = 0; @@ -79,7 +79,7 @@ fn benchmarkFind(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { - var tree = ordered.RedBlackTree(i32, Context).init(allocator, Context{}); + var tree = ordered.RedBlackTreeSet(i32, Context).init(allocator, Context{}); defer tree.deinit(); var i: i32 = 0; @@ -106,7 +106,7 @@ fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkIterator(allocator: std.mem.Allocator, size: usize) !void { - var tree = ordered.RedBlackTree(i32, Context).init(allocator, Context{}); + var tree = ordered.RedBlackTreeSet(i32, Context).init(allocator, Context{}); defer tree.deinit(); var i: i32 = 0; diff --git a/benches/b4_skip_list.zig b/benches/b4_skip_list_map.zig similarity index 88% rename from benches/b4_skip_list.zig rename to benches/b4_skip_list_map.zig index 3b3c424..098f06b 100644 --- a/benches/b4_skip_list.zig +++ b/benches/b4_skip_list_map.zig @@ -9,7 +9,7 @@ pub fn main() !void { std.debug.print("=== SkipList Benchmark ===\n\n", .{}); - const sizes = [_]usize{ 1000, 10_000, 100_000 }; + const sizes = [_]usize{ 1000, 10_000, 100_000, 1_000_000 }; inline for (sizes) |size| { try benchmarkPut(allocator, size); @@ -24,7 +24,7 @@ fn i32Compare(lhs: i32, rhs: i32) std.math.Order { } fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { - var list = try ordered.SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try ordered.SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); var timer = try Timer.start(); @@ -46,7 +46,7 @@ fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { - var list = try ordered.SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try ordered.SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); var i: i32 = 0; @@ -75,7 +75,7 @@ fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkDelete(allocator: std.mem.Allocator, size: usize) !void { - var list = try ordered.SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try ordered.SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); var i: i32 = 0; diff --git a/benches/b5_trie.zig b/benches/b5_trie_map.zig similarity index 93% rename from benches/b5_trie.zig rename to benches/b5_trie_map.zig index 8a54774..f63b490 100644 --- a/benches/b5_trie.zig +++ b/benches/b5_trie_map.zig @@ -9,7 +9,7 @@ pub fn main() !void { std.debug.print("=== Trie Benchmark ===\n\n", .{}); - const sizes = [_]usize{ 1000, 10_000, 50_000 }; + const sizes = [_]usize{ 1000, 10_000, 100_000, 1_000_000 }; inline for (sizes) |size| { try benchmarkPut(allocator, size); @@ -25,7 +25,7 @@ fn generateKey(allocator: std.mem.Allocator, i: usize) ![]u8 { } fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { - var trie = try ordered.Trie(i32).init(allocator); + var trie = try ordered.TrieMap(i32).init(allocator); defer trie.deinit(); var arena = std.heap.ArenaAllocator.init(allocator); @@ -52,7 +52,7 @@ fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { - var trie = try ordered.Trie(i32).init(allocator); + var trie = try ordered.TrieMap(i32).init(allocator); defer trie.deinit(); var arena = std.heap.ArenaAllocator.init(allocator); @@ -87,7 +87,7 @@ fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkContains(allocator: std.mem.Allocator, size: usize) !void { - var trie = try ordered.Trie(i32).init(allocator); + var trie = try ordered.TrieMap(i32).init(allocator); defer trie.deinit(); var arena = std.heap.ArenaAllocator.init(allocator); @@ -122,7 +122,7 @@ fn benchmarkContains(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkPrefixSearch(allocator: std.mem.Allocator, size: usize) !void { - var trie = try ordered.Trie(i32).init(allocator); + var trie = try ordered.TrieMap(i32).init(allocator); defer trie.deinit(); var arena = std.heap.ArenaAllocator.init(allocator); diff --git a/benches/b6_cartesian_tree.zig b/benches/b6_cartesian_tree_map.zig similarity index 89% rename from benches/b6_cartesian_tree.zig rename to benches/b6_cartesian_tree_map.zig index 26ea7cd..0cd77a1 100644 --- a/benches/b6_cartesian_tree.zig +++ b/benches/b6_cartesian_tree_map.zig @@ -9,7 +9,7 @@ pub fn main() !void { std.debug.print("=== CartesianTree Benchmark ===\n\n", .{}); - const sizes = [_]usize{ 1000, 10_000, 100_000 }; + const sizes = [_]usize{ 1000, 10_000, 100_000, 1_000_000 }; inline for (sizes) |size| { try benchmarkPut(allocator, size); @@ -21,7 +21,7 @@ pub fn main() !void { } fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { - var tree = ordered.CartesianTree(i32, i32).init(allocator); + var tree = ordered.CartesianTreeMap(i32, i32).init(allocator); defer tree.deinit(); var timer = try Timer.start(); @@ -43,7 +43,7 @@ fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { - var tree = ordered.CartesianTree(i32, i32).init(allocator); + var tree = ordered.CartesianTreeMap(i32, i32).init(allocator); defer tree.deinit(); var i: i32 = 0; @@ -72,7 +72,7 @@ fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { - var tree = ordered.CartesianTree(i32, i32).init(allocator); + var tree = ordered.CartesianTreeMap(i32, i32).init(allocator); defer tree.deinit(); var i: i32 = 0; @@ -99,7 +99,7 @@ fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { } fn benchmarkIterator(allocator: std.mem.Allocator, size: usize) !void { - var tree = ordered.CartesianTree(i32, i32).init(allocator); + var tree = ordered.CartesianTreeMap(i32, i32).init(allocator); defer tree.deinit(); var i: i32 = 0; diff --git a/build.zig.zon b/build.zig.zon index 77e0f54..8b170bc 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ordered, - .version = "0.1.0-alpha.3", + .version = "0.1.0", .fingerprint = 0xc3121f99b0352e1b, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .paths = .{ diff --git a/examples/README.md b/examples/README.md index 1fbf780..2a315c4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,24 +1,26 @@ -### Usage Examples +### Ordered Examples -| # | File | Description | -|---|------------------------------------------------|-----------------------------------------------------| -| 1 | [e1_btree_map.zig](e1_btree_map.zig) | An example using the `BTreeMap` data structure | -| 2 | [e2_sorted_set.zig](e2_sorted_set.zig) | An example using the `SortedSet` data structure | -| 3 | [e3_red_black_tree.zig](e3_red_black_tree.zig) | An example using the `RedBlackTree` data structure | -| 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 | +#### List of Examples -### Running Examples +| # | File | Description | +|---|--------------------------------------------------------|-------------------------------------| +| 1 | [e1_btree_map.zig](e1_btree_map.zig) | Example of using B-tree map | +| 2 | [e2_sorted_set.zig](e2_sorted_set.zig) | Example of using Sorted set | +| 3 | [e3_red_black_tree_set.zig](e3_red_black_tree_set.zig) | Example of using Red-black tree set | +| 4 | [e4_skip_list_map.zig](e4_skip_list_map.zig) | Example of using Skip list map | +| 5 | [e5_trie_map.zig](e5_trie_map.zig) | Example of using Trie map | +| 6 | [e6_cartesian_tree_map.zig](e6_cartesian_tree_map.zig) | Example of using Cartesian tree map | -To run an example, run the following command from the root of the repository: +#### Running Examples -```zig +To execute an example, run the following command from the root of the repository: + +```sh zig build run-{FILE_NAME_WITHOUT_EXTENSION} ``` For example: -```zig +```sh zig build run-e1_btree_map ``` diff --git a/examples/e3_red_black_tree.zig b/examples/e3_red_black_tree_set.zig similarity index 86% rename from examples/e3_red_black_tree.zig rename to examples/e3_red_black_tree_set.zig index 6e30370..6fa68ee 100644 --- a/examples/e3_red_black_tree.zig +++ b/examples/e3_red_black_tree_set.zig @@ -1,7 +1,7 @@ const std = @import("std"); const ordered = @import("ordered"); -// Context object for comparison. This is needed by RedBlackTree +// Context object for comparison. This is needed by RedBlackTreeSet const I32Context = struct { // This function must be public to be visible from the library code. pub fn lessThan(_: @This(), a: i32, b: i32) bool { @@ -12,8 +12,8 @@ const I32Context = struct { pub fn main() !void { const allocator = std.heap.page_allocator; - std.debug.print("## RedBlackTree Example (as a Set) ##\n", .{}); - var rbt = ordered.RedBlackTree(i32, I32Context).init(allocator, .{}); + std.debug.print("## RedBlackTreeSet Example (as a Set) ##\n", .{}); + var rbt = ordered.RedBlackTreeSet(i32, I32Context).init(allocator, .{}); defer rbt.deinit(); try rbt.put(40); diff --git a/examples/e4_skip_list.zig b/examples/e4_skip_list_map.zig similarity index 81% rename from examples/e4_skip_list.zig rename to examples/e4_skip_list_map.zig index 04b349f..b6bee96 100644 --- a/examples/e4_skip_list.zig +++ b/examples/e4_skip_list_map.zig @@ -8,8 +8,8 @@ fn strCompare(lhs: []const u8, rhs: []const u8) std.math.Order { pub fn main() !void { const allocator = std.heap.page_allocator; - std.debug.print("## SkipList Example ##\n", .{}); - var skip_list = try ordered.SkipList([]const u8, u32, strCompare, 16).init(allocator); + std.debug.print("## SkipListMap Example ##\n", .{}); + var skip_list = try ordered.SkipListMap([]const u8, u32, strCompare, 16).init(allocator); defer skip_list.deinit(); try skip_list.put("zebra", 300); @@ -17,7 +17,7 @@ pub fn main() !void { try skip_list.put("mango", 250); try skip_list.put("banana", 150); - std.debug.print("SkipList count: {d}\n", .{skip_list.count()}); + std.debug.print("SkipListMap count: {d}\n", .{skip_list.count()}); if (skip_list.get("mango")) |value_ptr| { std.debug.print("Found 'mango': value is {d}\n", .{value_ptr.*}); diff --git a/examples/e5_trie.zig b/examples/e5_trie_map.zig similarity index 86% rename from examples/e5_trie.zig rename to examples/e5_trie_map.zig index f3711f4..1c62876 100644 --- a/examples/e5_trie.zig +++ b/examples/e5_trie_map.zig @@ -4,8 +4,8 @@ const ordered = @import("ordered"); pub fn main() !void { const allocator = std.heap.page_allocator; - std.debug.print("## Trie Example ##\n", .{}); - var trie = try ordered.Trie([]const u8).init(allocator); + std.debug.print("## TrieMap Example ##\n", .{}); + var trie = try ordered.TrieMap([]const u8).init(allocator); defer trie.deinit(); try trie.put("cat", "feline"); @@ -14,7 +14,7 @@ pub fn main() !void { try trie.put("care", "to look after"); try trie.put("careful", "cautious"); - std.debug.print("Trie count: {d}\n", .{trie.count()}); + std.debug.print("TrieMap count: {d}\n", .{trie.count()}); if (trie.get("car")) |value_ptr| { std.debug.print("Found 'car': {s}\n", .{value_ptr.*}); diff --git a/examples/e6_cartesian_tree.zig b/examples/e6_cartesian_tree_map.zig similarity index 78% rename from examples/e6_cartesian_tree.zig rename to examples/e6_cartesian_tree_map.zig index bad0cda..5cb971e 100644 --- a/examples/e6_cartesian_tree.zig +++ b/examples/e6_cartesian_tree_map.zig @@ -4,15 +4,15 @@ const ordered = @import("ordered"); pub fn main() !void { const allocator = std.heap.page_allocator; - std.debug.print("## Cartesian Tree Example ##\n", .{}); - var cartesian_tree = ordered.CartesianTree(i32, []const u8).init(allocator); + std.debug.print("## CartesianTreeMap Example ##\n", .{}); + var cartesian_tree = ordered.CartesianTreeMap(i32, []const u8).init(allocator); defer cartesian_tree.deinit(); try cartesian_tree.put(50, "fifty"); try cartesian_tree.put(30, "thirty"); try cartesian_tree.put(70, "seventy"); - std.debug.print("Cartesian tree size: {d}\n", .{cartesian_tree.count()}); + std.debug.print("CartesianTreeMap size: {d}\n", .{cartesian_tree.count()}); const search_key = 30; if (cartesian_tree.get(search_key)) |value| { diff --git a/src/lib.zig b/src/lib.zig index 8c82455..7dfc54f 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,33 +1,41 @@ //! The `ordered` library provides a collection of data structures that maintain //! their elements in sorted order. //! -//! Available Structures: +//! ## Sets (store values only) //! - `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. -//! - `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. +//! - `RedBlackTreeSet`: A self-balancing binary search tree for ordered sets. //! -//! 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 +//! ## Maps (store key-value pairs) +//! - `BTreeMap`: A cache-efficient B-tree for mapping sorted keys to values. +//! - `SkipListMap`: A probabilistic data structure for ordered key-value storage. +//! - `TrieMap`: A prefix tree for efficient string key operations and prefix matching. +//! - `CartesianTreeMap`: A randomized treap combining BST and heap properties. +//! +//! ## Common API +//! All map structures provide similar methods: +//! - `init(allocator)` - Create new instance +//! - `deinit()` - Free all memory +//! - `count()` - Get number of elements +//! - `contains(key)` - Check if key exists +//! - `get(key)` - Get immutable value +//! - `getPtr(key)` - Get mutable value pointer +//! - `put(key, value)` - Insert or update +//! - `remove(key)` - Remove and return value +//! - `iterator()` - Iterate in order +//! +//! Set structures use similar API but store only values (no separate key): +//! - `put(value)` - Insert value (returns bool for duplicate detection) +//! - `contains(value)` - Check if value exists +//! - `remove(index)` or `removeValue(value)` - Remove value +//! - `iterator()` - Iterate in sorted order pub const SortedSet = @import("ordered/sorted_set.zig").SortedSet; +pub const RedBlackTreeSet = @import("ordered/red_black_tree_set.zig").RedBlackTreeSet; + pub const BTreeMap = @import("ordered/btree_map.zig").BTreeMap; -pub const SkipList = @import("ordered/skip_list.zig").SkipList; -pub const Trie = @import("ordered/trie.zig").Trie; -pub const RedBlackTree = @import("ordered/red_black_tree.zig").RedBlackTree; -pub const CartesianTree = @import("ordered/cartesian_tree.zig").CartesianTree; +pub const SkipListMap = @import("ordered/skip_list_map.zig").SkipListMap; +pub const TrieMap = @import("ordered/trie_map.zig").TrieMap; +pub const CartesianTreeMap = @import("ordered/cartesian_tree_map.zig").CartesianTreeMap; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/ordered/btree_map.zig b/src/ordered/btree_map.zig index d37e784..69d7389 100644 --- a/src/ordered/btree_map.zig +++ b/src/ordered/btree_map.zig @@ -20,7 +20,7 @@ //! for concurrent access. //! //! ## Iterator Invalidation -//! WARNING: Modifying the map (via put/remove/clear) while iterating will cause +//! WARNING: Modifying the map (via put, remove, or clear) while iterating will cause //! undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); @@ -210,19 +210,19 @@ pub fn BTreeMap( new_root.is_leaf = false; new_root.children[0] = root_node; self.root = new_root; - self.splitChild(new_root, 0); + try self.splitChild(new_root, 0); root_node = new_root; } - const is_new = self.insertNonFull(root_node, key, value); + const is_new = try self.insertNonFull(root_node, key, value); if (is_new) { self.len += 1; } } - fn splitChild(self: *Self, parent: *Node, index: u16) void { + fn splitChild(self: *Self, parent: *Node, index: u16) !void { const child = parent.children[index].?; - const new_sibling = self.createNode() catch @panic("OOM"); + const new_sibling = try self.createNode(); new_sibling.is_leaf = child.is_leaf; const t = MIN_KEYS; @@ -265,7 +265,7 @@ pub fn BTreeMap( parent.len += 1; } - fn insertNonFull(self: *Self, node: *Node, key: K, value: V) bool { + fn insertNonFull(self: *Self, node: *Node, key: K, value: V) !bool { var i = node.len; if (node.is_leaf) { // Check if key already exists @@ -300,12 +300,12 @@ pub fn BTreeMap( 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); + try self.splitChild(node, i); if (compare(node.keys[i], key) == .lt) { i += 1; } } - return self.insertNonFull(node.children[i].?, key, value); + return try self.insertNonFull(node.children[i].?, key, value); } } @@ -509,6 +509,106 @@ pub fn BTreeMap( node.len -= 1; self.allocator.destroy(sibling); } + + /// Iterator for in-order traversal of the B-tree. + /// + /// The iterator visits keys in sorted order. + pub const Iterator = struct { + stack: std.ArrayList(StackFrame), + allocator: std.mem.Allocator, + + const StackFrame = struct { + node: *Node, + index: u16, + }; + + pub const Entry = struct { + key: K, + value: V, + }; + + fn init(allocator: std.mem.Allocator, root: ?*Node) !Iterator { + var stack = std.ArrayList(StackFrame){}; + + if (root) |r| { + try stack.append(allocator, StackFrame{ .node = r, .index = 0 }); + // Descend to leftmost leaf + var current = r; + while (!current.is_leaf) { + if (current.children[0]) |child| { + try stack.append(allocator, StackFrame{ .node = child, .index = 0 }); + current = child; + } else break; + } + } + + return Iterator{ + .stack = stack, + .allocator = allocator, + }; + } + + pub fn deinit(self: *Iterator) void { + self.stack.deinit(self.allocator); + } + + pub fn next(self: *Iterator) !?Entry { + while (self.stack.items.len > 0) { + var frame = &self.stack.items[self.stack.items.len - 1]; + + if (frame.index < frame.node.len) { + const result = Entry{ + .key = frame.node.keys[frame.index], + .value = frame.node.values[frame.index], + }; + + // Move to next position + if (!frame.node.is_leaf) { + // Go to right child of current key + if (frame.node.children[frame.index + 1]) |child| { + frame.index += 1; + try self.stack.append(self.allocator, StackFrame{ .node = child, .index = 0 }); + + // Descend to leftmost leaf + var current = child; + while (!current.is_leaf) { + if (current.children[0]) |left_child| { + try self.stack.append(self.allocator, StackFrame{ .node = left_child, .index = 0 }); + current = left_child; + } else break; + } + } else { + frame.index += 1; + } + } else { + frame.index += 1; + } + + return result; + } else { + _ = self.stack.pop(); + } + } + + return null; + } + }; + + /// Returns an iterator over the map in sorted key order. + /// + /// Time complexity: O(1) for initialization + /// + /// ## Example + /// ```zig + /// var iter = try map.iterator(); + /// defer iter.deinit(); + /// while (try iter.next()) |entry| { + /// std.debug.print("{} => {}\n", .{entry.key, entry.value}); + /// } + /// ``` + pub fn iterator(self: *const Self) !Iterator { + return Iterator.init(self.allocator, self.root); + } }; } @@ -626,77 +726,48 @@ test "BTreeMap: reverse insertion" { } try std.testing.expectEqual(@as(usize, 50), map.len); - - i = 1; - while (i <= 50) : (i += 1) { - try std.testing.expectEqual(i * 3, map.get(i).?.*); - } } -test "BTreeMap: random operations" { +test "BTreeMap: iterator in-order traversal" { const allocator = std.testing.allocator; - var map = BTreeMap(i32, i32, i32Compare, 4).init(allocator); + var map = BTreeMap(i32, []const u8, i32Compare, 4).init(allocator); defer map.deinit(); - const values = [_]i32{ 15, 3, 27, 8, 42, 1, 19, 33, 11, 25 }; - for (values) |val| { - try map.put(val, val * 10); - } - - try std.testing.expectEqual(@as(usize, 10), map.len); - - for (values) |val| { - try std.testing.expectEqual(val * 10, map.get(val).?.*); + // Insert in random order + try map.put(30, "thirty"); + try map.put(10, "ten"); + try map.put(20, "twenty"); + try map.put(5, "five"); + try map.put(25, "twenty-five"); + try map.put(15, "fifteen"); + + // Verify iterator returns items in sorted key order + var iter = try map.iterator(); + defer iter.deinit(); + + const expected_keys = [_]i32{ 5, 10, 15, 20, 25, 30 }; + const expected_values = [_][]const u8{ "five", "ten", "fifteen", "twenty", "twenty-five", "thirty" }; + + var count: usize = 0; + while (try iter.next()) |entry| { + try std.testing.expect(count < expected_keys.len); + try std.testing.expectEqual(expected_keys[count], entry.key); + try std.testing.expectEqualStrings(expected_values[count], entry.value); + count += 1; } - // Remove some - _ = map.remove(15); - _ = map.remove(27); - _ = map.remove(1); - - try std.testing.expectEqual(@as(usize, 7), map.len); - try std.testing.expect(map.get(15) == null); - try std.testing.expect(map.get(27) == null); - try std.testing.expect(map.get(1) == null); + try std.testing.expectEqual(@as(usize, 6), count); } -test "BTreeMap: remove all elements" { +test "BTreeMap: iterator on empty map" { const allocator = std.testing.allocator; var map = BTreeMap(i32, i32, i32Compare, 4).init(allocator); defer map.deinit(); - try map.put(1, 1); - try map.put(2, 2); - try map.put(3, 3); - try map.put(4, 4); - try map.put(5, 5); - - _ = map.remove(1); - _ = map.remove(2); - _ = map.remove(3); - _ = map.remove(4); - _ = map.remove(5); - - try std.testing.expectEqual(@as(usize, 0), map.len); - try std.testing.expect(map.get(3) == null); -} - -test "BTreeMap: minimum branching factor" { - const allocator = std.testing.allocator; - var map = BTreeMap(i32, i32, i32Compare, 3).init(allocator); - defer map.deinit(); - - var i: i32 = 0; - while (i < 20) : (i += 1) { - try map.put(i, i); - } - - try std.testing.expectEqual(@as(usize, 20), map.len); + var iter = try map.iterator(); + defer iter.deinit(); - i = 0; - while (i < 20) : (i += 1) { - try std.testing.expectEqual(i, map.get(i).?.*); - } + try std.testing.expect((try iter.next()) == null); } test "BTreeMap: negative keys" { diff --git a/src/ordered/cartesian_tree.zig b/src/ordered/cartesian_tree_map.zig similarity index 87% rename from src/ordered/cartesian_tree.zig rename to src/ordered/cartesian_tree_map.zig index fcf59a4..326d24b 100644 --- a/src/ordered/cartesian_tree.zig +++ b/src/ordered/cartesian_tree_map.zig @@ -1,6 +1,6 @@ -//! A Cartesian Tree (Treap) implementation combining binary search tree and heap properties. +//! A Cartesian tree (Treap) implementation combining binary search tree and heap properties. //! -//! A Cartesian Tree maintains two orderings simultaneously: +//! 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) //! @@ -21,14 +21,14 @@ //! - 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 +//! - Simpler alternative to AVL and 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 +//! WARNING: Modifying the tree (via put, remove, or clear) while iterating will cause //! undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); @@ -36,7 +36,7 @@ const testing = std.testing; const Allocator = std.mem.Allocator; const Order = std.math.Order; -pub fn CartesianTree(comptime K: type, comptime V: type) type { +pub fn CartesianTreeMap(comptime K: type, comptime V: type) type { return struct { const Self = @This(); @@ -60,7 +60,7 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { allocator: Allocator, len: usize = 0, - /// Creates a new empty Cartesian Tree. + /// Creates a new empty Cartesian tree. /// /// ## Parameters /// - `allocator`: Memory allocator for node allocation @@ -217,14 +217,15 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { /// 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. + /// Returns `null` if the key doesn't exist. The returned pointer can be used + /// to modify the value in place without re-inserting. /// /// Time complexity: O(log n) expected pub fn getPtr(self: *Self, key: K) ?*V { - return self.getNodePtr(self.root, key); + return Self.getNodePtr(self.root, key); } - fn getNodePtr(_: *Self, root: ?*Node, key: K) ?*V { + fn getNodePtr(root: ?*Node, key: K) ?*V { if (root == null) return null; const node = root.?; @@ -232,8 +233,8 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { return switch (key_cmp) { .eq => &node.value, - .lt => getNodePtr(undefined, node.left, key), - .gt => getNodePtr(undefined, node.right, key), + .lt => getNodePtr(node.left, key), + .gt => getNodePtr(node.right, key), }; } @@ -357,8 +358,8 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { }; } -test "CartesianTree basic operations" { - var tree = CartesianTree(i32, []const u8).init(testing.allocator); +test "CartesianTreeMap basic operations" { + var tree = CartesianTreeMap(i32, []const u8).init(testing.allocator); defer tree.deinit(); // Test insertion and retrieval @@ -386,8 +387,8 @@ test "CartesianTree basic operations" { try testing.expect(!tree.contains(3)); } -test "CartesianTree: empty tree operations" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: empty tree operations" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); try testing.expect(tree.isEmpty()); @@ -397,8 +398,8 @@ test "CartesianTree: empty tree operations" { try testing.expect(tree.remove(42) == null); } -test "CartesianTree: single element" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: single element" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); try tree.putWithPriority(42, 100, 50); @@ -411,8 +412,8 @@ test "CartesianTree: single element" { try testing.expect(tree.root == null); } -test "CartesianTree: priority ordering" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: priority ordering" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); // Higher priority should be closer to root @@ -424,8 +425,8 @@ test "CartesianTree: priority ordering" { try testing.expectEqual(@as(i32, 10), tree.root.?.key); } -test "CartesianTree: update existing key with different priority" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: update existing key with different priority" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); try tree.putWithPriority(10, 100, 50); @@ -435,8 +436,8 @@ test "CartesianTree: update existing key with different priority" { try testing.expectEqual(@as(i32, 200), tree.get(10).?); } -test "CartesianTree: random priorities with put" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: random priorities with put" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); // Using put which generates random priorities @@ -450,8 +451,8 @@ test "CartesianTree: random priorities with put" { try testing.expectEqual(@as(i32, 3), tree.get(3).?); } -test "CartesianTree: sequential keys" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: sequential keys" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); var i: i32 = 0; @@ -467,8 +468,8 @@ test "CartesianTree: sequential keys" { } } -test "CartesianTree: remove non-existent key" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: remove non-existent key" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); try tree.putWithPriority(10, 10, 10); @@ -479,8 +480,8 @@ test "CartesianTree: remove non-existent key" { try testing.expectEqual(@as(usize, 2), tree.count()); } -test "CartesianTree: remove all elements" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: remove all elements" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); try tree.putWithPriority(1, 1, 1); @@ -495,8 +496,8 @@ test "CartesianTree: remove all elements" { try testing.expect(tree.get(2) == null); } -test "CartesianTree: negative keys" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: negative keys" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); try tree.putWithPriority(-10, 10, 100); @@ -508,8 +509,8 @@ test "CartesianTree: negative keys" { try testing.expectEqual(@as(i32, -5), tree.get(5).?); } -test "CartesianTree: iterator traversal" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: iterator traversal" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); try tree.putWithPriority(30, 30, 30); @@ -530,8 +531,8 @@ test "CartesianTree: iterator traversal" { try testing.expectEqual(@as(usize, 4), idx); } -test "CartesianTree: large dataset" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: large dataset" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); var i: i32 = 0; @@ -547,8 +548,8 @@ test "CartesianTree: large dataset" { } } -test "CartesianTree: same priorities different keys" { - var tree = CartesianTree(i32, i32).init(testing.allocator); +test "CartesianTreeMap: same priorities different keys" { + var tree = CartesianTreeMap(i32, i32).init(testing.allocator); defer tree.deinit(); // When priorities are equal, BST property still maintained by key diff --git a/src/ordered/red_black_tree.zig b/src/ordered/red_black_tree_set.zig similarity index 89% rename from src/ordered/red_black_tree.zig rename to src/ordered/red_black_tree_set.zig index 4087889..6f8a4b6 100644 --- a/src/ordered/red_black_tree.zig +++ b/src/ordered/red_black_tree_set.zig @@ -1,6 +1,6 @@ -//! Red-Black Tree - A self-balancing binary search tree. +//! Red-black tree - A self-balancing binary search tree. //! -//! Red-Black Trees guarantee O(log n) time complexity for insert, delete, and search +//! 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). //! @@ -27,7 +27,7 @@ //! for concurrent access. //! //! ## Iterator Invalidation -//! WARNING: Modifying the tree (via put/remove/clear) while iterating will +//! WARNING: Modifying the tree (via put, remove, or clear) while iterating will //! cause undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); @@ -35,7 +35,7 @@ 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. +/// Creates a Red-black tree type for the given data type and comparison context. /// /// ## Parameters /// - `T`: The data type to store in the tree @@ -48,9 +48,9 @@ const assert = std.debug.assert; /// return a < b; /// } /// }; -/// var tree = RedBlackTree(i32, Context).init(allocator, .{}); +/// var tree = RedBlackTreeSet(i32, Context).init(allocator, .{}); /// ``` -pub fn RedBlackTree(comptime T: type, comptime Context: type) type { +pub fn RedBlackTreeSet(comptime T: type, comptime Context: type) type { return struct { const Self = @This(); @@ -77,7 +77,7 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { context: Context, size: usize, - /// Creates a new empty Red-Black Tree. + /// Creates a new empty Red-black tree. /// /// ## Parameters /// - `allocator`: Memory allocator for node allocation @@ -118,7 +118,7 @@ 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 { + pub fn count(self: *const Self) usize { return self.size; } @@ -428,7 +428,7 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { /// to access or modify the data directly. /// /// Time complexity: O(log n) - pub fn get(self: Self, data: T) ?*Node { + pub fn get(self: *const Self, data: T) ?*Node { var current = self.root; while (current) |node| { @@ -447,11 +447,11 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { /// Checks whether the tree contains the given value. /// /// Time complexity: O(log n) - pub fn contains(self: Self, data: T) bool { + pub fn contains(self: *const Self, data: T) bool { return self.get(data) != null; } - fn findMinimum(self: Self, node: *Node) *Node { + fn findMinimum(self: *const Self, node: *Node) *Node { _ = self; // Mark as intentionally unused var current = node; while (current.left) |left| { @@ -516,7 +516,7 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { } }; - pub fn iterator(self: Self) !Iterator { + pub fn iterator(self: *const Self) !Iterator { return Iterator.init(self.allocator, self.root); } }; @@ -533,13 +533,13 @@ pub fn DefaultContext(comptime T: type) type { } // Convenience type aliases -pub fn RedBlackTreeManaged(comptime T: type) type { - return RedBlackTree(T, DefaultContext(T)); +pub fn RedBlackTreeSetManaged(comptime T: type) type { + return RedBlackTreeSet(T, DefaultContext(T)); } -test "RedBlackTree: basic operations" { +test "RedBlackTreeSet: basic operations" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(10); @@ -552,9 +552,9 @@ test "RedBlackTree: basic operations" { try std.testing.expect(!tree.contains(99)); } -test "RedBlackTree: empty tree operations" { +test "RedBlackTreeSet: empty tree operations" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try std.testing.expect(!tree.contains(42)); @@ -562,9 +562,9 @@ test "RedBlackTree: empty tree operations" { try std.testing.expect(tree.remove(42) == null); } -test "RedBlackTree: single element" { +test "RedBlackTreeSet: single element" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(42); @@ -578,9 +578,9 @@ test "RedBlackTree: single element" { try std.testing.expect(tree.root == null); } -test "RedBlackTree: duplicate insertions" { +test "RedBlackTreeSet: duplicate insertions" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(10); @@ -591,9 +591,9 @@ test "RedBlackTree: duplicate insertions" { try std.testing.expectEqual(@as(usize, 1), tree.count()); } -test "RedBlackTree: sequential insertion" { +test "RedBlackTreeSet: sequential insertion" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); var i: i32 = 0; @@ -610,9 +610,9 @@ test "RedBlackTree: sequential insertion" { } } -test "RedBlackTree: reverse insertion" { +test "RedBlackTreeSet: reverse insertion" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); var i: i32 = 50; @@ -624,9 +624,9 @@ test "RedBlackTree: reverse insertion" { try std.testing.expect(tree.root.?.color == .black); } -test "RedBlackTree: remove from middle" { +test "RedBlackTreeSet: remove from middle" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(10); @@ -643,9 +643,9 @@ test "RedBlackTree: remove from middle" { try std.testing.expect(tree.contains(7)); } -test "RedBlackTree: remove root" { +test "RedBlackTreeSet: remove root" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(10); @@ -658,9 +658,9 @@ test "RedBlackTree: remove root" { try std.testing.expect(tree.root.?.color == .black); } -test "RedBlackTree: minimum and maximum" { +test "RedBlackTreeSet: minimum and maximum" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(10); @@ -678,9 +678,9 @@ test "RedBlackTree: minimum and maximum" { try std.testing.expectEqual(@as(i32, 20), max.?.data); } -test "RedBlackTree: iterator empty tree" { +test "RedBlackTreeSet: iterator empty tree" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); var iter = try tree.iterator(); @@ -690,9 +690,9 @@ test "RedBlackTree: iterator empty tree" { try std.testing.expect(node == null); } -test "RedBlackTree: clear" { +test "RedBlackTreeSet: clear" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(1); @@ -704,9 +704,9 @@ test "RedBlackTree: clear" { try std.testing.expect(tree.root == null); } -test "RedBlackTree: negative numbers" { +test "RedBlackTreeSet: negative numbers" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(-10); @@ -719,9 +719,9 @@ test "RedBlackTree: negative numbers" { try std.testing.expect(tree.contains(0)); } -test "RedBlackTree: get returns correct node" { +test "RedBlackTreeSet: get returns correct node" { const allocator = std.testing.allocator; - var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + var tree = RedBlackTreeSet(i32, DefaultContext(i32)).init(allocator, .{}); defer tree.deinit(); try tree.put(10); diff --git a/src/ordered/skip_list.zig b/src/ordered/skip_list_map.zig similarity index 90% rename from src/ordered/skip_list.zig rename to src/ordered/skip_list_map.zig index f5362b3..efdf8bc 100644 --- a/src/ordered/skip_list.zig +++ b/src/ordered/skip_list_map.zig @@ -8,7 +8,7 @@ //! - 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 +//! - Space: O(n) average (with level distribution, approximately 1.33n pointers) //! //! ## Use Cases //! - Ordered key-value storage with simpler implementation than trees @@ -20,7 +20,7 @@ //! for concurrent access. //! //! ## Iterator Invalidation -//! WARNING: Modifying the skip list (via put/remove/clear) while iterating will +//! WARNING: Modifying the skip list (via put, remove, or clear) while iterating will //! cause undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); @@ -33,7 +33,7 @@ const std = @import("std"); /// - `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( +pub fn SkipListMap( comptime K: type, comptime V: type, comptime compare: fn (lhs: K, rhs: K) std.math.Order, @@ -294,9 +294,9 @@ fn i32Compare(lhs: i32, rhs: i32) std.math.Order { return std.math.order(lhs, rhs); } -test "SkipList: basic operations" { +test "SkipListMap: basic operations" { const allocator = std.testing.allocator; - var list = try SkipList(i32, []const u8, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, []const u8, i32Compare, 16).init(allocator); defer list.deinit(); // Test put and get @@ -326,9 +326,9 @@ test "SkipList: basic operations" { try std.testing.expect(!list.contains(20)); } -test "SkipList: iteration order" { +test "SkipListMap: iteration order" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); // Insert in random order @@ -353,9 +353,9 @@ test "SkipList: iteration order" { try std.testing.expectEqual(expected_keys.len, index); } -test "SkipList: string keys" { +test "SkipListMap: string keys" { const allocator = std.testing.allocator; - var list = try SkipList([]const u8, i32, strCompare, 16).init(allocator); + var list = try SkipListMap([]const u8, i32, strCompare, 16).init(allocator); defer list.deinit(); try list.put("apple", 1); @@ -367,9 +367,9 @@ test "SkipList: string keys" { try std.testing.expect(!list.contains("date")); } -test "SkipList: empty list operations" { +test "SkipListMap: empty list operations" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); try std.testing.expect(list.get(42) == null); @@ -378,9 +378,9 @@ test "SkipList: empty list operations" { try std.testing.expect(!list.contains(42)); } -test "SkipList: single element" { +test "SkipListMap: single element" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); try list.put(42, 100); @@ -392,9 +392,9 @@ test "SkipList: single element" { try std.testing.expectEqual(@as(usize, 0), list.len); } -test "SkipList: large dataset sequential" { +test "SkipListMap: large dataset sequential" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); var i: i32 = 0; @@ -410,9 +410,9 @@ test "SkipList: large dataset sequential" { } } -test "SkipList: reverse insertion" { +test "SkipListMap: reverse insertion" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); var i: i32 = 50; @@ -431,9 +431,9 @@ test "SkipList: reverse insertion" { } } -test "SkipList: delete non-existent" { +test "SkipListMap: delete non-existent" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); try list.put(10, 10); @@ -444,9 +444,9 @@ test "SkipList: delete non-existent" { try std.testing.expectEqual(@as(usize, 2), list.len); } -test "SkipList: delete all elements" { +test "SkipListMap: delete all elements" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); try list.put(1, 1); @@ -461,9 +461,9 @@ test "SkipList: delete all elements" { try std.testing.expect(list.get(2) == null); } -test "SkipList: negative keys" { +test "SkipListMap: negative keys" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); try list.put(-10, 10); @@ -475,9 +475,9 @@ test "SkipList: negative keys" { try std.testing.expectEqual(@as(i32, -5), list.get(5).?.*); } -test "SkipList: getPtr mutation" { +test "SkipListMap: getPtr mutation" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); defer list.deinit(); try list.put(10, 100); @@ -489,9 +489,9 @@ test "SkipList: getPtr mutation" { try std.testing.expectEqual(@as(i32, 999), list.get(10).?.*); } -test "SkipList: minimum max level" { +test "SkipListMap: minimum max level" { const allocator = std.testing.allocator; - var list = try SkipList(i32, i32, i32Compare, 1).init(allocator); + var list = try SkipListMap(i32, i32, i32Compare, 1).init(allocator); defer list.deinit(); try list.put(1, 1); diff --git a/src/ordered/sorted_set.zig b/src/ordered/sorted_set.zig index 6dcf2ac..3022289 100644 --- a/src/ordered/sorted_set.zig +++ b/src/ordered/sorted_set.zig @@ -7,9 +7,8 @@ //! 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. +//! WARNING: Modifying the set (via put, remove, or clear) while iterating will +//! cause undefined behavior. Complete all iterations before modifying the structure. const std = @import("std"); @@ -30,7 +29,7 @@ pub fn SortedSet( pub fn init(allocator: std.mem.Allocator) Self { return .{ - .items = std.ArrayList(T){}, + .items = .{}, .allocator = allocator, }; } @@ -65,7 +64,14 @@ pub fn SortedSet( return self.items.orderedRemove(index); } - /// Returns true if the vector contains the given value. + /// Removes a value from the set and returns it if it existed. + /// Returns null if the value was not found. + pub fn removeValue(self: *Self, value: T) ?T { + const index = self.findIndex(value) orelse return null; + return self.remove(index); + } + + /// Returns true if the set contains the given value. pub fn contains(self: *Self, value: T) bool { return self.findIndex(value) != null; } @@ -74,6 +80,24 @@ pub fn SortedSet( pub fn findIndex(self: *Self, value: T) ?usize { return std.sort.binarySearch(T, self.items.items, value, compareFn); } + + /// Iterator for traversing the set in sorted order. + pub const Iterator = struct { + items: []const T, + index: usize = 0, + + pub fn next(self: *Iterator) ?T { + if (self.index >= self.items.len) return null; + const value = self.items[self.index]; + self.index += 1; + return value; + } + }; + + /// Returns an iterator over the set in sorted order. + pub fn iterator(self: *const Self) Iterator { + return Iterator{ .items = self.items.items }; + } }; } @@ -83,113 +107,135 @@ fn i32Compare(lhs: i32, rhs: i32) std.math.Order { test "SortedSet basic functionality" { const allocator = std.testing.allocator; - var vec = SortedSet(i32, i32Compare).init(allocator); - defer vec.deinit(); + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); - _ = try vec.put(100); - _ = try vec.put(50); - _ = try vec.put(75); + _ = try set.put(100); + _ = try set.put(50); + _ = try set.put(75); - try std.testing.expectEqualSlices(i32, &.{ 50, 75, 100 }, vec.items.items); - try std.testing.expect(vec.contains(75)); - try std.testing.expect(!vec.contains(99)); - try std.testing.expectEqual(@as(?usize, 1), vec.findIndex(75)); + try std.testing.expectEqualSlices(i32, &.{ 50, 75, 100 }, set.items.items); + try std.testing.expect(set.contains(75)); + try std.testing.expect(!set.contains(99)); + try std.testing.expectEqual(@as(?usize, 1), set.findIndex(75)); - _ = vec.remove(1); // Remove 75 - try std.testing.expectEqualSlices(i32, &.{ 50, 100 }, vec.items.items); + _ = set.remove(1); // Remove 75 + try std.testing.expectEqualSlices(i32, &.{ 50, 100 }, set.items.items); } test "SortedSet: empty set operations" { const allocator = std.testing.allocator; - var vec = SortedSet(i32, i32Compare).init(allocator); - defer vec.deinit(); + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); - try std.testing.expect(!vec.contains(42)); - try std.testing.expectEqual(@as(?usize, null), vec.findIndex(42)); - try std.testing.expectEqual(@as(usize, 0), vec.items.items.len); + try std.testing.expect(!set.contains(42)); + try std.testing.expectEqual(@as(?usize, null), set.findIndex(42)); + try std.testing.expectEqual(@as(usize, 0), set.items.items.len); } test "SortedSet: single element" { const allocator = std.testing.allocator; - var vec = SortedSet(i32, i32Compare).init(allocator); - defer vec.deinit(); + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); - _ = try vec.put(42); - try std.testing.expect(vec.contains(42)); - try std.testing.expectEqual(@as(usize, 1), vec.items.items.len); + _ = try set.put(42); + try std.testing.expect(set.contains(42)); + try std.testing.expectEqual(@as(usize, 1), set.items.items.len); - const removed = vec.remove(0); + const removed = set.remove(0); try std.testing.expectEqual(@as(i32, 42), removed); - try std.testing.expectEqual(@as(usize, 0), vec.items.items.len); + try std.testing.expectEqual(@as(usize, 0), set.items.items.len); } test "SortedSet: duplicate values rejected" { const allocator = std.testing.allocator; - var vec = SortedSet(i32, i32Compare).init(allocator); - defer vec.deinit(); + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); - const added1 = try vec.put(10); - const added2 = try vec.put(10); - const added3 = try vec.put(10); + const added1 = try set.put(10); + const added2 = try set.put(10); + const added3 = try set.put(10); // 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); + try std.testing.expectEqual(@as(usize, 1), set.items.items.len); } test "SortedSet: negative numbers" { const allocator = std.testing.allocator; - var vec = SortedSet(i32, i32Compare).init(allocator); - defer vec.deinit(); + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); - _ = try vec.put(-5); - _ = try vec.put(-10); - _ = try vec.put(0); - _ = try vec.put(5); + _ = try set.put(-5); + _ = try set.put(-10); + _ = try set.put(0); + _ = try set.put(5); - try std.testing.expectEqualSlices(i32, &.{ -10, -5, 0, 5 }, vec.items.items); + try std.testing.expectEqualSlices(i32, &.{ -10, -5, 0, 5 }, set.items.items); } test "SortedSet: large dataset" { const allocator = std.testing.allocator; - var vec = SortedSet(i32, i32Compare).init(allocator); - defer vec.deinit(); + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); // Insert in reverse order var i: i32 = 100; while (i >= 0) : (i -= 1) { - _ = try vec.put(i); + _ = try set.put(i); } // Verify sorted - try std.testing.expectEqual(@as(usize, 101), vec.items.items.len); - for (vec.items.items, 0..) |val, idx| { + try std.testing.expectEqual(@as(usize, 101), set.items.items.len); + for (set.items.items, 0..) |val, idx| { try std.testing.expectEqual(@as(i32, @intCast(idx)), val); } } test "SortedSet: remove boundary cases" { const allocator = std.testing.allocator; - var vec = SortedSet(i32, i32Compare).init(allocator); - defer vec.deinit(); + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); - _ = try vec.put(1); - _ = try vec.put(2); - _ = try vec.put(3); - _ = try vec.put(4); - _ = try vec.put(5); + _ = try set.put(1); + _ = try set.put(2); + _ = try set.put(3); + _ = try set.put(4); + _ = try set.put(5); // Remove first - _ = vec.remove(0); - try std.testing.expectEqualSlices(i32, &.{ 2, 3, 4, 5 }, vec.items.items); + _ = set.remove(0); + try std.testing.expectEqualSlices(i32, &.{ 2, 3, 4, 5 }, set.items.items); // Remove last - _ = vec.remove(3); - try std.testing.expectEqualSlices(i32, &.{ 2, 3, 4 }, vec.items.items); + _ = set.remove(3); + try std.testing.expectEqualSlices(i32, &.{ 2, 3, 4 }, set.items.items); // Remove middle - _ = vec.remove(1); - try std.testing.expectEqualSlices(i32, &.{ 2, 4 }, vec.items.items); + _ = set.remove(1); + try std.testing.expectEqualSlices(i32, &.{ 2, 4 }, set.items.items); +} + +test "SortedSet: removeValue method" { + const allocator = std.testing.allocator; + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); + + _ = try set.put(10); + _ = try set.put(20); + _ = try set.put(30); + _ = try set.put(40); + + // Remove existing value + const removed = set.removeValue(20); + try std.testing.expectEqual(@as(i32, 20), removed.?); + try std.testing.expectEqual(@as(usize, 3), set.items.items.len); + try std.testing.expect(!set.contains(20)); + + // Try to remove non-existent value + const not_found = set.removeValue(99); + try std.testing.expect(not_found == null); + try std.testing.expectEqual(@as(usize, 3), set.items.items.len); } diff --git a/src/ordered/trie.zig b/src/ordered/trie_map.zig similarity index 90% rename from src/ordered/trie.zig rename to src/ordered/trie_map.zig index 5d6c56e..e2d8727 100644 --- a/src/ordered/trie.zig +++ b/src/ordered/trie_map.zig @@ -23,22 +23,22 @@ //! for concurrent access. //! //! ## Iterator Invalidation -//! WARNING: Modifying the trie (via put/remove/clear) while iterating will cause +//! WARNING: Modifying the trie (via put, remove, or 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. +/// Creates a TrieMap type that maps string keys to values of type V. /// /// ## Example /// ```zig -/// var trie = try Trie([]const u8).init(allocator); +/// var trie = try TrieMap([]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 { +pub fn TrieMap(comptime V: type) type { return struct { const Self = @This(); @@ -97,15 +97,12 @@ pub fn Trie(comptime V: type) type { self.* = undefined; } - /// Removes all elements from the trie while keeping the root allocated. + /// Removes all elements from the trie. /// /// 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 { + pub fn clear(self: *Self) void { self.root.deinit(self.allocator); - self.root = try TrieNode.init(self.allocator); + self.root = TrieNode.init(self.allocator) catch unreachable; self.len = 0; } @@ -356,7 +353,7 @@ pub fn Trie(comptime V: type) type { self.current_key.deinit(self.allocator); } - pub fn next(self: *Iterator) ?struct { key: []const u8, value: V } { + pub fn next(self: *Iterator) !?struct { key: []const u8, value: V } { while (self.stack.items.len > 0) { var frame = &self.stack.items[self.stack.items.len - 1]; @@ -369,13 +366,13 @@ pub fn Trie(comptime V: type) type { const char = entry.key_ptr.*; const child = entry.value_ptr.*; - self.current_key.append(self.allocator, char) catch return null; + try self.current_key.append(self.allocator, char); - self.stack.append(self.allocator, IteratorFrame{ + try self.stack.append(self.allocator, IteratorFrame{ .node = child, .child_iter = child.children.iterator(), .visited_self = false, - }) catch return null; + }); } else { _ = self.stack.pop(); if (self.current_key.items.len > 0) { @@ -393,9 +390,9 @@ pub fn Trie(comptime V: type) type { }; } -test "Trie: basic operations" { +test "TrieMap: basic operations" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("hello", 1); @@ -408,9 +405,9 @@ test "Trie: basic operations" { try std.testing.expect(trie.get("bye") == null); } -test "Trie: empty trie operations" { +test "TrieMap: empty trie operations" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try std.testing.expect(trie.get("key") == null); @@ -419,9 +416,9 @@ test "Trie: empty trie operations" { try std.testing.expect(!trie.hasPrefix("pre")); } -test "Trie: single character keys" { +test "TrieMap: single character keys" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("a", 1); @@ -433,9 +430,9 @@ test "Trie: single character keys" { try std.testing.expect(!trie.contains("d")); } -test "Trie: empty string key" { +test "TrieMap: empty string key" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("", 42); @@ -447,9 +444,9 @@ test "Trie: empty string key" { try std.testing.expectEqual(@as(usize, 0), trie.len); } -test "Trie: overlapping prefixes" { +test "TrieMap: overlapping prefixes" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("test", 1); @@ -464,9 +461,9 @@ test "Trie: overlapping prefixes" { try std.testing.expect(trie.hasPrefix("test")); } -test "Trie: delete with shared prefixes" { +test "TrieMap: delete with shared prefixes" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("car", 1); @@ -481,9 +478,9 @@ test "Trie: delete with shared prefixes" { try std.testing.expect(trie.contains("care")); } -test "Trie: delete non-existent key" { +test "TrieMap: delete non-existent key" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("hello", 1); @@ -493,9 +490,9 @@ test "Trie: delete non-existent key" { try std.testing.expectEqual(@as(usize, 1), trie.len); } -test "Trie: delete prefix that is not a key" { +test "TrieMap: delete prefix that is not a key" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("testing", 1); @@ -506,9 +503,9 @@ test "Trie: delete prefix that is not a key" { try std.testing.expect(trie.contains("testing")); } -test "Trie: update existing key" { +test "TrieMap: update existing key" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("key", 100); @@ -519,9 +516,9 @@ test "Trie: update existing key" { try std.testing.expectEqual(@as(i32, 200), trie.get("key").?.*); } -test "Trie: hasPrefix with exact match" { +test "TrieMap: hasPrefix with exact match" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("hello", 1); @@ -532,9 +529,9 @@ test "Trie: hasPrefix with exact match" { try std.testing.expect(!trie.hasPrefix("helloo")); } -test "Trie: getPtr mutation" { +test "TrieMap: getPtr mutation" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("key", 100); @@ -546,9 +543,9 @@ test "Trie: getPtr mutation" { try std.testing.expectEqual(@as(i32, 999), trie.get("key").?.*); } -test "Trie: many keys" { +test "TrieMap: many keys" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); const keys = [_][]const u8{ @@ -567,9 +564,9 @@ test "Trie: many keys" { } } -test "Trie: delete all keys" { +test "TrieMap: delete all keys" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("a", 1); @@ -584,9 +581,9 @@ test "Trie: delete all keys" { try std.testing.expect(!trie.hasPrefix("a")); } -test "Trie: special characters" { +test "TrieMap: special characters" { const allocator = std.testing.allocator; - var trie = try Trie(i32).init(allocator); + var trie = try TrieMap(i32).init(allocator); defer trie.deinit(); try trie.put("hello-world", 1);