Rust: Binary Search Tree
In this article we’ll see how to implement a Binary Search Tree in Rust. You can also get the final source code.
Node
A tree is composed of nodes. Each node has a key, a value and two sub-trees: a left one and a right one. We want to be able to store integers, strings, structs or any other data structure in our BST, so we use generics both for the key and the value. A node doesn’t always have sub-trees, and we use Option to represent the possibility of presence or absence. Here is the Node struct:
struct Node<K,V> {
: Option<Box<Node<K,V>>>,
left: Option<Box<Node<K,V>>>,
right: K,
key: V,
value}
Rust has many types of pointers, the most important ones are: references
(&T
) and Box
es.
- References (
&T
) borrow a resource for a limited time. They don’t own the resource and are valid as long as the resource they point to is valid. Box
es instead own the resource they point to. When theBox
goes out of scope the resource is deallocated.
Here’s an example:
struct Node {
: u32,
field}
fn main() {
let x;
{
let y = Node{field: 3};
= &y;
x }
// error: here x holds a reference to y
// but y goes out of scope
}
As you can see we can only reference a Node while we are inside its scope. What we need instead is to own the Node we refer to…
struct Node {
: u32,
field}
fn main() {
let x;
{
let y = Node{field: 3};
= Box::new(y);
x }
// ok: x now owns y
}
Methods
We need a method to create a new
node given a key and a
value:
impl<K, V> Node<K, V> {
fn new(k: K, v: V) -> Node<K, V> {
{left: None,
Node : None,
right: k,
key: v,
value}
}
}
BSTs are sorted which means that nodes on the left are less than the current node and those on the right are greater. How we decide if your generic type K is lesser or greater? We use a trait constrain which says that our key can be of any type as long as said type implements the Ord trait.
impl<K: Ord, V> Node<K, V> {
The insert
method is defined recursively. If the Node to
be inserted is less follow the left sub-tree, if it’s greater follow the
right one, otherwise do nothing.
fn insert(&mut self, n: Node<K, V>) {
match n.key.cmp(&self.key) {
=> {
Less match self.left {
None => self.left = Some(Box::new(n)),
Some(ref mut l) => l.insert(n),
}
},
=> {
Greater match self.right {
None => self.right = Some(Box::new(n)),
Some(ref mut r) => r.insert(n),
}
},
=> {},
_ }
}
We do a similar thing with get
.
fn get(&self, key: &K) -> Option<&V> {
match key.cmp(&self.key) {
=> {
Equal Some(&self.value)
},
=> {
Less if let Some(ref l) = self.left {
.get(key)
l} else {
None
}
},
=> {
Greater if let Some(ref r) = self.right {
.get(key)
r} else {
None
}
}
}
}
Instead of letting the user manipulate Nodes, we use another struct called Bst which holds the root of our tree and optionally other fields (like the length of the tree).
struct Bst<K,V> {
: Option<Box<Node<K,V>>>,
root}
impl<K, V> Bst<K, V> {
fn new() -> Bst<K, V> {
{root: None,
Bst }
}
}
impl<K: Ord, V> Bst<K, V> {
fn insert(&mut self, k: K, v: V) {
match self.root {
None => self.root = Some(Box::new(Node::new(k, v))),
Some(ref mut r) => r.insert(Node::new(k, v)),
}
}
fn get(&self, k: &K) -> Option<&V> {
match self.root {
None => None,
Some(ref r) => r.get(k),
}
}
}
Here is an example
fn main() {
let mut bst = Bst::new();
.insert(2, "2");
bst.insert(1, "1");
bst.insert(3, "3");
bstprintln!("{}", bst.get(2));
}
Traits
To make our BST more ergonomic to use, we need to implement some traits from the standard library.
Index
With this trait we can do bst[2]
instead of
bst.get(2)
.
impl<K: Ord, V> Index<K> for Bst<K, V>{
type Output = V;
fn index(&self, index: K) -> &V {
self.get(&index).expect("no entry found for key")
}
}
Iter
TODO
derive
Rust (like Haskell) can automaticaly implement some traits, when asked to do so, using the derive attribute. Add…
#[derive(Debug)]
struct Node<K,V> {
…and…
#[derive(Debug, Default)]
struct Bst<K, V> {
this will implement the Debug and Default traits.
Grapvhiz
Graphviz is a graph visualization software. We can generate a DOT file and pass
the output to the dot
program which will generate the graph
of our tree.
impl<K: std::fmt::Display, V> Node<K, V> {
fn dot_leaf(&self, leaf: &Option<Box<Node<K, V>>>, c: &mut u32) {
match *leaf {
None => {
println!("null{} [shape=point];", c);
println!("{} -> null{};", &self.key, c);
*c += 1;
},
Some(ref l) => {
println!("{} -> {}", &self.key, &l.key);
.dot(c);
l}
}
}
fn dot(&self, c: &mut u32) {
self.dot_leaf(&self.left, c);
self.dot_leaf(&self.right, c);
}
}
impl<K: std::fmt::Display, V> Bst<K, V> {
fn dot(&self) {
println!("digraph BST {{");
let mut c = 0u32;
match self.root {
None => {},
Some(ref r) => { r.dot(&mut c); },
}
println!("}}");
}
}
Download the example and run it:
$ rustc dot.rs -o /tmp/dot && /tmp/dot | dot -Tx11
Source code
Exercises for the reader
- Implement a remove method.
- Implement a length method on
Bst
. One way is to recursively calculate each time the length of the tree, the other is to have alength
field instruct Bst
and makeinsert
return atrue
if a new node was inserted,false
otherwise (the same for remove). - Implement a self-balancing tree (for example a Splay Tree).
- Implement the IndexMut trait.