Recommendation: YES, with caveats - A double-array trie backend would be beneficial for specific use cases, but should be added as an additional option rather than replacing existing backends.
Best fit for: Static, large dictionaries where memory efficiency and cache locality are critical (e.g., spell checkers, NLP applications with 100k+ terms).
A double-array trie (DAT) is a space-efficient trie implementation using two parallel arrays:
BASE[]: Stores base indices for transitionsCHECK[]: Validates transitions belong to correct parentKey Properties:
| Backend | Construction | Memory | Query Speed | Dynamic | Best Use Case |
|---|---|---|---|---|---|
| PathMap | Fast (1.43ms) | High | Very Fast (683ns) | No | In-memory, speed-critical |
| DAWG | Medium | Low | Fast | No | Static dictionaries |
| DynamicDAWG | Medium | Low | Fast | Yes | Evolving dictionaries |
| SuffixAutomaton | Slow (6.18ms) | Medium | Medium (27µs @ d=1) | Yes | Substring matching |
| Metric | Expected Performance | Notes |
|---|---|---|
| Construction | Slow (5-15ms for 5k words) | Requires solving placement problem |
| Memory | Very Low (4-8 bytes/char) | Most space-efficient |
| Query Speed | Very Fast (400-600ns @ d=0) | Excellent cache locality |
| Dynamic | No (or very slow) | Static structure |
| Best Use | Large static dictionaries | 100k+ terms, memory-constrained |
| Backend | Estimated Memory | Bytes/Word |
|---|---|---|
| PathMap | ~640 KB | 128 |
| DAWG | ~160 KB | 32 |
| DynamicDAWG | ~200 KB | 40 |
| DoubleArray | ~160 KB | 32 |
| SuffixAutomaton | ~320 KB | 64 |
Analysis: DAT would match DAWG in space efficiency while potentially offering better query performance.
Based on literature and similar implementations:
| Distance | PathMap | DAWG (est) | DoubleArray (proj) | Improvement |
|---|---|---|---|---|
| 0 | 712 ns | ~800 ns | 500-600 ns | 15-30% faster |
| 1 | 8.7 µs | ~9.5 µs | 8.0-8.5 µs | 5-15% faster |
| 2 | 83.9 µs | ~90 µs | 80-85 µs | 5-10% faster |
Key advantage: Cache-friendly memory layout benefits Levenshtein automaton traversal.
Challenge: Finding optimal BASE values to minimize collisions
Options:
a) Simple Incremental (easiest, slower):
fn find_base(node: &Node, check: &[i32]) -> usize {
let mut base = 1;
loop {
if all_children_fit(node, base, check) {
return base;
}
base += 1;
}
}
b) Dynamic Programming (optimal, complex):
c) Heuristic-based (good balance):
fn find_base_heuristic(node: &Node, check: &[i32]) -> usize {
// Try common patterns first: ASCII range, vowels/consonants clusters
// Fall back to incremental search
}
Recommendation: Start with heuristic-based approach.
pub struct DoubleArrayTrie {
/// BASE array: stores transition base indices
/// BASE[s] + c gives the index for transition on character c
base: Vec<i32>,
/// CHECK array: validates transitions
/// CHECK[BASE[s] + c] must equal s for valid transition
check: Vec<i32>,
/// Final states: bitmap or separate array
/// Marks dictionary word boundaries
is_final: BitVec,
/// Metadata
num_terms: usize,
}
impl Dictionary for DoubleArrayTrie {
type Node = usize; // Just an index into the arrays
fn root(&self) -> Self::Node {
0 // Root is always at index 0
}
fn is_final(&self, node: &Self::Node) -> bool {
self.is_final[*node]
}
fn edges<'a>(&'a self, node: &Self::Node) -> Box<dyn Iterator<Item = (u8, Self::Node)> + 'a> {
// Iterate through possible transitions from this node
Box::new((0..=255u8).filter_map(move |c| {
let next = self.base[*node] + c as i32;
if next >= 0 && (next as usize) < self.check.len() && self.check[next as usize] == *node as i32 {
Some((c, next as usize))
} else {
None
}
}))
}
}
Issue: The edges() implementation iterates all 256 possible bytes, which is inefficient.
Solution: Add auxiliary structure to track which edges actually exist:
pub struct DoubleArrayTrie {
base: Vec<i32>,
check: Vec<i32>,
is_final: BitVec,
// NEW: Store actual edges for efficient iteration
edge_lists: Vec<SmallVec<[u8; 4]>>, // Most nodes have few edges
}
impl DoubleArrayTrie {
pub fn from_terms<I, S>(terms: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
// 1. Build sorted term list
let mut sorted_terms: Vec<String> = terms
.into_iter()
.map(|s| s.as_ref().to_string())
.collect();
sorted_terms.sort();
sorted_terms.dedup();
// 2. Build trie structure first (temporary)
let trie = build_trie(&sorted_terms);
// 3. Convert to double-array representation
let (base, check, is_final, edge_lists) = convert_to_double_array(&trie);
Self {
base,
check,
is_final,
edge_lists,
num_terms: sorted_terms.len(),
}
}
}
| Backend | Current | DAT (Projected) |
|---|---|---|
| PathMap | 1.43 ms | - |
| DAWG | ~2.0 ms | - |
| DoubleArray | - | 3-8 ms |
Analysis: DAT construction would be slower than PathMap but comparable to DAWG.
| Backend | Current | DAT (Projected) |
|---|---|---|
| PathMap | 30.0 µs | - |
| DoubleArray | - | 27-29 µs |
Analysis: DAT should be competitive with or slightly faster than PathMap due to cache locality.
DoubleArrayTrie structDictionary traitDictionaryBackend enumDictionaryFactorysrc/dictionary/double_array_trie.rs: ~800-1000 lines
- Struct definition: ~50 lines
- Construction algorithm: ~300 lines
- Dictionary trait impl: ~200 lines
- Helper functions: ~200 lines
- Tests: ~200 lines
- Documentation: ~50 lines
Total new code: ~800-1000 lines
Impact on compile time: +5-10 seconds
Binary size increase: ~50-80 KB
yada cratedarts crateRecommendation: Build from scratch, using yada as reference for algorithms.
High Priority IF:
Low Priority IF:
Instead of adding DAT, consider:
This might achieve 80% of the benefit with 20% of the effort.
A double-array trie backend would be beneficial for liblevenshtein-rust, particularly for:
However, the benefit is incremental (10-30% improvement) rather than transformational. The decision should be based on:
Recommended approach: Implement as an experimental backend, gather user feedback, and promote to stable if widely adopted.
yada crate: https://docs.rs/yada/src/dictionary/double_array_trie.rs skeletonCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |