Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 119 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

</div>

---

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.
Expand All @@ -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/<branch_or_tag>.tar.gz"
```

Replace `<branch_or_tag>` 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()});
}
```

---

Expand All @@ -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.

---

Expand Down
23 changes: 23 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -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
104 changes: 28 additions & 76 deletions benches/README.md
Original file line number Diff line number Diff line change
@@ -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-<benchmark_name>
```
### 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)).
2 changes: 1 addition & 1 deletion benches/b1_btree_map.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion benches/b2_sorted_set.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Loading
Loading