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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ keywords = ["radix-tree", "trie", "data-structure", "prefix-search", "utf8"]
categories = ["data-structures", "algorithms"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
criterion = "0.5"
Expand All @@ -20,3 +21,6 @@ rand = "0.8"
[[bench]]
name = "benchmarks"
harness = false

[lints.clippy]
needless_doctest_main = "allow"
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,46 @@ fn main() {
tree.insert("approach", "method");
tree.insert("appropriate", "suitable");

// Search by prefix - returns all keys starting with "app"
println!("Search 'app' by prefix");
let iter = tree.search_iter("app", SearchMode::Prefix);
for (key, value) in iter {
println!(" {} -> {}", String::from_utf8_lossy(&key), value);
}
// Output:
// apple: fruit
// application: main app
// apply: verb
// Search 'app' by prefix
// app -> short form
// apple -> fruit
// application -> main app
// apply -> verb
// approach -> method
// appropriate -> suitable

// Update existing value or create a new one
tree.mut_value("app", |value| {
*value = Some("short form");
});

println!("Search exactly 'app'");
let iter = tree.search_iter("app", SearchMode::Exact);
for (key, value) in iter {
println!(" {} -> {}", String::from_utf8_lossy(&key), value);
}
// Output:
// app -> short form

println!("Search 'app' with tolerance");
let iter = tree.search_with_tolerance("apl", 1);
for (key, value, distance) in iter {
let key = String::from_utf8_lossy(&key).to_string();
println!(" {} -> {} (distance={})", key, value, distance);
}
// Output:
// Search 'app' with tolerance
// app -> short form (distance=1)
// apple -> fruit (distance=1)
// application -> main app (distance=1)
// apply -> verb (distance=1)
// approach -> method (distance=1)
// appropriate -> suitable (distance=1)
}
```
67 changes: 67 additions & 0 deletions src/iter/leaves.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use crate::tree::RadixNode;

/// Iterator for traversing only the leaf nodes of a radix tree.
///
/// A leaf node is defined as a node that has a value but no children,
/// representing the end of a key path with no further extensions.
/// This iterator performs lazy traversal using a stack-based approach.
/// Memory usage is O(tree depth) storing only node references and child indices.
pub struct LeavesIterator<'a, T> {
// Stack of (node, child_index, current_key) pairs for traversal state
stack: Vec<(&'a RadixNode<T>, usize, Vec<u8>)>,
}

impl<'a, T> LeavesIterator<'a, T> {
pub(crate) fn new(root: &'a RadixNode<T>) -> Self {
let mut iterator = Self { stack: Vec::new() };

// Start traversal from the root with empty key
iterator.stack.push((root, 0, Vec::new()));
iterator
}
}

impl<'a, T> Iterator for LeavesIterator<'a, T> {
type Item = (Vec<u8>, &'a T);

fn next(&mut self) -> Option<Self::Item> {
loop {
let (node, child_index, current_key) = self.stack.pop()?;

// If this is the first visit to this node (child_index == 0), check if it's a leaf
if child_index == 0 {
// A leaf node has a value and no children
if let Some(ref value) = node.value {
if node.children.is_empty() {
// This is a leaf node - return it
return Some((current_key.clone(), value));
}
}

// Not a leaf or no value, start processing children
if !node.children.is_empty() {
self.stack.push((node, 1, current_key));
}
continue;
}

// Processing children: child_index - 1 is the current child being processed
let current_child_idx = child_index - 1;

if current_child_idx < node.children.len() {
// Push the next child index for this node
if current_child_idx + 1 < node.children.len() {
self.stack
.push((node, child_index + 1, current_key.clone()));
}

// Push the current child to be processed
let (_, child) = &node.children[current_child_idx];
let mut child_key = current_key;
child_key.extend_from_slice(&child.key);

self.stack.push((child, 0, child_key));
}
}
}
}
7 changes: 7 additions & 0 deletions src/iter/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod leaves;
mod search;
mod tolerance;

pub use leaves::*;
pub use search::*;
pub use tolerance::*;
151 changes: 151 additions & 0 deletions src/iter/search.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use crate::SearchMode;
use crate::tree::{RadixNode, common_prefix_length};

/// Iterator for traversing search results from a radix tree search.
///
/// This iterator performs truly lazy traversal using a stack-based approach.
/// Memory usage is O(tree depth) storing only node references and child indices.
pub struct SearchIterator<'a, T> {
// Stack of (node, child_index, current_key) pairs for traversal state
stack: Vec<(&'a RadixNode<T>, usize, Vec<u8>)>,
// Search mode to determine matching behavior
mode: SearchMode,
// Original search key for exact matching validation
search_key: Vec<u8>,
}

impl<'a, T> SearchIterator<'a, T> {
pub(crate) fn new(root: &'a RadixNode<T>, prefix: &[u8], mode: SearchMode) -> Self {
let mut iterator = Self {
stack: Vec::new(),
mode,
search_key: prefix.to_vec(),
};

// Find the starting node that matches the prefix
if let Some((start_node, key_prefix)) = Self::find_starting_node(root, prefix, Vec::new()) {
iterator.stack.push((start_node, 0, key_prefix));
}

iterator
}

fn find_starting_node(
node: &'a RadixNode<T>,
prefix: &[u8],
current_key: Vec<u8>,
) -> Option<(&'a RadixNode<T>, Vec<u8>)> {
if prefix.is_empty() {
// Empty prefix - start from this node
return Some((node, current_key));
}

let first_byte = prefix[0];
if let Ok(index) = node
.children
.binary_search_by_key(&first_byte, |(byte, _)| *byte)
{
let (_, child) = &node.children[index];
let common_len = common_prefix_length(&child.key, prefix);

if common_len == child.key.len() {
// Child's key is fully consumed, continue with remaining prefix
let mut new_current_key = current_key;
new_current_key.extend_from_slice(&child.key);
let remaining_prefix = &prefix[common_len..];
return Self::find_starting_node(child, remaining_prefix, new_current_key);
} else if common_len == prefix.len() && child.key.starts_with(prefix) {
// Prefix is fully consumed and matches - start from this child
let mut new_current_key = current_key;
new_current_key.extend_from_slice(&child.key);
return Some((child, new_current_key));
}
}

None
}
}

impl<'a, T> Iterator for SearchIterator<'a, T> {
type Item = (Vec<u8>, &'a T);

fn next(&mut self) -> Option<Self::Item> {
loop {
let (node, child_index, current_key) = self.stack.pop()?;

// If this is the first visit to this node (child_index == 0), check for value
if child_index == 0 {
if let Some(ref value) = node.value {
// Check if this value matches based on search mode
let matches = match self.mode {
SearchMode::Exact => current_key == self.search_key,
SearchMode::Prefix => true, // All values found are valid for prefix search
};

if matches {
// Push back with child_index = 1 to continue with children next time
if !node.children.is_empty() {
self.stack.push((node, 1, current_key.clone()));
}
return Some((current_key, value));
}
}

// No value or no match, start processing children
if !node.children.is_empty() {
let should_continue = match self.mode {
SearchMode::Prefix => true, // Always continue for prefix search
SearchMode::Exact => current_key.len() < self.search_key.len(), // Only continue if we haven't exceeded search key length
};

if should_continue {
self.stack.push((node, 1, current_key));
}
}
continue;
}

// Processing children: child_index - 1 is the current child being processed
let current_child_idx = child_index - 1;

if current_child_idx < node.children.len() {
// Push the next child index for this node
if current_child_idx + 1 < node.children.len() {
let should_continue_siblings = match self.mode {
SearchMode::Prefix => true,
SearchMode::Exact => current_key.len() <= self.search_key.len(),
};

if should_continue_siblings {
self.stack
.push((node, child_index + 1, current_key.clone()));
}
}

// Push the current child to be processed
let (_, child) = &node.children[current_child_idx];
let mut child_key = current_key;
child_key.extend_from_slice(&child.key);

// Determine if we should process this child
let should_process_child = match self.mode {
SearchMode::Prefix => true, // Always process for prefix search
SearchMode::Exact => {
// Only process child if it could lead to the exact key
child_key.len() <= self.search_key.len()
&& self.search_key.starts_with(&child_key)
}
};

if should_process_child {
self.stack.push((child, 0, child_key));
}
}
}
}

fn size_hint(&self) -> (usize, Option<usize>) {
// Cannot determine size without traversing, provide conservative estimate
(0, None)
}
}
Loading