The DynamicDawg provides a mutable DAWG (Directed Acyclic Word Graph) that supports online insertions, deletions, and batch operations while maintaining near-minimal structure.
\mathcal{O}(m)$ per term\mathcal{O}(m)$ per termextend() and remove_many() with automatic compaction\mathcal{O}(n)$ total sizeneeds_compaction() flag after deletionsArc<...> internally (the lock-free LockFreeDawg core) for concurrent accesscompare_exchange (CAS)PathMapDictionaryuse liblevenshtein::prelude::*;
// Empty DAWG
let dawg = DynamicDawg::new();
// From iterator
let dawg = DynamicDawg::from_iter(vec!["test", "testing"]);
// Insert (returns true if new)
dawg.insert("apple");
// Remove (returns true if existed)
dawg.remove("banana");
// Check status
println!("Terms: {}", dawg.term_count());
println!("Nodes: {}", dawg.node_count());
println!("Needs compaction: {}", dawg.needs_compaction());
// Manual batch with explicit compaction
dawg.insert("term1");
dawg.insert("term2");
dawg.remove("term3");
// ... many more operations ...
let nodes_removed = dawg.compact(); // Restore minimality
// Automatic batch methods
let added = dawg.extend(vec!["term1", "term2"]);
let removed = dawg.remove_many(vec!["old1", "old2"]);
DynamicDawg provides two methods for restoring minimality:
compact() - Full Rebuild// Explicit compaction (extracts, sorts, rebuilds, minimizes)
let nodes_removed = dawg.compact();
// Check if needed
if dawg.needs_compaction() {
dawg.compact();
}
When to use:
minimize() - Incremental Minimization// Minimize without full rebuild
let nodes_merged = dawg.minimize();
// Can be called anytime
dawg.minimize();
When to use:
Key Differences:
compact(): Extracts all terms, sorts them, rebuilds from scratch, then minimizesminimize(): Computes node signatures, merges equivalent nodes in-placeminimize() is generally more efficient for incremental updates| Operation | Time Complexity | Notes |
|---|---|---|
insert(term) | $\mathcal{O}(m)$ | $m$ = term length |
remove(term) | $\mathcal{O}(m)$ | May leave orphaned nodes |
compact() | $\mathcal{O}(n \log n + n \cdot s)$ | $n$ = terms, $s$ = signature size |
minimize() | $\mathcal{O}(n \cdot s)$ | $n$ = nodes, $s$ = signature size |
extend(terms) | $\mathcal{O}(n \log n + n \cdot s)$ | Includes compaction |
remove_many(terms) | $\mathcal{O}(n \log n + n \cdot s)$ | Includes compaction |
// ❌ Bad: Compact after every change
dawg.insert("term1");
dawg.compact(); // Expensive!
dawg.insert("term2");
dawg.compact(); // Expensive!
// ✅ Good: Batch then compact once
dawg.insert("term1");
dawg.insert("term2");
// ... more operations ...
dawg.compact();
// ✅ Best: Use batch methods
dawg.extend(vec!["term1", "term2", ...]);
// Strategy 1: Use minimize() for batch insertions
fn batch_insert(dawg: &DynamicDawg, terms: Vec<String>) {
for term in terms {
dawg.insert(&term);
}
dawg.minimize(); // Incremental minimization
}
// Strategy 2: Use compact() after deletions
fn batch_update(dawg: &DynamicDawg, updates: Vec<Update>) {
for update in updates {
match update {
Update::Add(term) => dawg.insert(&term),
Update::Remove(term) => dawg.remove(&term),
};
}
if dawg.needs_compaction() {
dawg.compact(); // Full rebuild after deletions
} else {
dawg.minimize(); // Incremental for insertions
}
}
// Strategy 3: Periodic minimization
let mut ops_since_minimize = 0;
for term in terms {
dawg.insert(term);
ops_since_minimize += 1;
if ops_since_minimize >= 1000 {
dawg.minimize(); // Or compact() if deletions occurred
ops_since_minimize = 0;
}
}
// Strategy 4: Let the flag guide you
fn maybe_optimize(dawg: &DynamicDawg) {
if dawg.needs_compaction() {
dawg.compact(); // Use full rebuild
} else {
dawg.minimize(); // Use incremental
}
}
// DynamicDawg works seamlessly with fuzzy search
let dawg = DynamicDawg::from_iter(vec!["test", "testing"]);
let transducer = Transducer::new(dawg.clone(), Algorithm::Standard);
// Query works immediately after updates
dawg.insert("tested");
let results: Vec<_> = transducer.query("test", 1).collect();
The compaction process:
This guarantees perfect minimality after compaction.
Insertions: Maintain minimality through suffix sharing
Deletions: May create orphans
Solution: Periodic compaction rebuilds the entire structure.
| Feature | DynamicDawg | Static DAWG | PathMap |
|---|---|---|---|
| Insertions | ✅ $\mathcal{O}(m)$ | ❌ No | ✅ $\mathcal{O}(m)$ |
| Deletions | ✅ $\mathcal{O}(m)$ | ❌ No | ✅ $\mathcal{O}(m)$ |
| Minimality | 🟡 Near-minimal | ✅ Perfect | ❌ Not minimal |
| Compaction | ✅ Yes | N/A | N/A |
| Thread-safe | ✅ Lock-free | ✅ Immutable | ✅ Lock-free |
| Space | 🟡 Good | ✅ Excellent | 🟡 Good |
See:
examples/dynamic_dawg_demo.rs - Basic usage and comparisonsexamples/batch_operations.rs - Batch operation patternsA DAWG is minimal when:
Our compaction achieves this by:
\mathcal{O}(n^2)$ worst case per operation\mathcal{O}(m)$ per operation + $\mathcal{O}(n)$ periodic compaction\mathcal{O}(m)$ if compaction frequency is boundedThis trade-off makes dynamic operations practical.
Potential optimizations:
See Future Enhancements for roadmap.
Can 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 |