Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
77: Implementing a Binary Search Tree in Rust
We've spent a lot of time with Vec and HashMap, but there are times when you need your data to stay sorted while allowing for fast insertions and lookups. That's where a Binary Search Tree (BST) comes in. To keep this concrete, let's build a UserRegistry. Imagine we're building a system that maps usernames (strings) to user IDs (integers), and we want to be able to iterate through them alphabetically without sorting the whole list every time.
Defining the Node Structure
In Rust, recursive data structures are a bit of a puzzle because the compiler needs to know exactly how much space a type takes up at compile time. If a Node contains another Node, it's infinitely large.
I'll be honest: every time I start a new project in a different language, I occasionally forget that Rust handles this differently than Java or Python. I initially tried to define my node like this:
// This will NOT compile
struct Node {
username: String,
id: u32,
left: Option<Node>,
right: Option<Node>,
}
The compiler immediately yelled at me with a "recursive type has infinite size" error. To fix this, we need indirection. By wrapping the child nodes in a Box, we're telling Rust to put the child node on the heap. The Box itself has a fixed size (it's just a pointer), which makes the compiler happy.
struct Node {
username: String,
id: u32,
left: Option<Box<Node>>,
right: Option<Box<Node>>,
}
pub struct UserRegistry {
root: Option<Box<Node>>,
}
Wiring up the Insertion Logic
Now we need a way to put users into the tree. The rule for a BST is simple: smaller values (alphabetically, in our case) go left, larger values go right. Since we're dealing with Option<Box<Node>>, the cleanest way to handle this is by using a mutable reference to the current node we're inspecting.
Check out how I'm using a helper function here. I prefer this approach because it keeps the public API clean while allowing the recursion to handle the heavy lifting.
impl UserRegistry {
pub fn new() -> Self {
UserRegistry { root: None }
}
pub fn insert(&mut self, username: String, id: u32) {
let mut current = &mut self.root;
while let Some(ref mut node) = current {
if username < node.username {
current = &mut node.left;
} else if username > node.username {
current = &mut node.right;
} else {
// User already exists, just update the ID
node.id = id;
return;
}
}
// We've reached an empty spot, insert the new node here
*current = Some(Box::new(Node {
username,
id,
left: None,
right: None,
}));
}
}
Finding a User in the Tree
Searching is where the BST really shines. Instead of checking every single user, we can discard half the remaining tree with every step. I'll implement this using a recursive approach this time, just to show you how it differs from the iterative insertion we just did.
impl UserRegistry {
pub fn get(&self, username: &str) -> Option<u32> {
fn search(node: &Option<Box<Node>>, target: &str) -> Option<u32> {
match node {
None => None,
Some(n) if n.username == target => Some(n.id),
Some(n) if target < n.username.as_str() => search(&n.left, target),
Some(n) => search(&n.right, target),
}
}
search(&self.root, username)
}
}
Notice how we're passing &Option<Box<Node>> into the helper. We aren't taking ownership of the nodes; we're just borrowing them as we dive deeper into the tree. If we tried to move the node out of the Option, the compiler would stop us because we don't own the tree—we're just searching it.
📋 Practical Task
Exercise: Implement an "In-Order Traversal" for UserRegistry
A major advantage of a BST is that if you visit the nodes in a specific order (Left, Root, Right), you get all the elements sorted alphabetically.
Your task is to add a method to the UserRegistry called list_users. This method should:
- Traverse the tree in-order.
- Collect the usernames and IDs into a
Vec<(String, u32)>. - Return that vector to the caller.
You will likely need a private recursive helper function to handle the traversal, as you did with the search logic in the lesson. Ensure that you are borrowing the nodes, not moving them!
There are no comments for now.