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
95: VecDeque for Double-Ended Queues
I was recently sketching out a simple "Recent Files" tracker for a text editor I'm building. The logic is straightforward: whenever a user opens a file, we add it to the list. If the list grows larger than, say, ten items, we drop the oldest one to make room for the new one. It's a classic sliding window.
The Shifting Problem
My first instinct was to use a Vec. It's the tool we use for almost everything in Rust, right? Here is how I started implementing it:
let mut recent_files = vec!["main.rs", "lib.rs", "cargo.toml"];
// User opens a new file
recent_files.push("mod.rs");
if recent_files.len() > 3 {
// Remove the oldest file (at the front)
recent_files.remove(0);
}
It works. The code is clean. But as I started thinking about scaling this—maybe a logs viewer that keeps the last 10,000 lines—I hit a wall. Vec::remove(0) is a performance nightmare. Why? Because a Vec is a contiguous block of memory. When you yank the element at index 0, every single other element in the vector has to shift one slot to the left to fill the gap. That's an $O(n)$ operation.
If I'm doing this thousands of times a second, I'm spending more time moving memory around than actually processing data.
Switching to a Ring Buffer
I needed something that could push to the back and pop from the front without moving the rest of the collection. I dug into std::collections and found VecDeque. The "Deque" stands for "Double-Ended Queue."
Let's try the same logic, but swap the Vec for a VecDeque:
use std::collections::VecDeque;
let mut recent_files = VecDeque::from([
"main.rs",
"lib.rs",
"cargo.toml"
]);
recent_files.push_back("mod.rs");
if recent_files.len() > 3 {
// This is the magic part
recent_files.pop_front();
}
Now, instead of shifting everything, VecDeque uses a ring buffer. Imagine the memory as a circle. It just moves a "head" pointer forward when we pop from the front. No shifting required. This turns our $O(n)$ removal into a constant $O(1)$ operation.
Playing with both ends
One thing I noticed while playing with the API is that VecDeque is incredibly flexible. Since it's double-ended, I can treat it like a stack, a queue, or both. I tried adding a feature where a "pinned" file gets jumped to the front of the recent list immediately:
recent_files.push_front("important_config.toml");
I can now push and pop from either side with equal efficiency. If I need to see what's at the ends without removing them, I've got front() and back(), which return options.
You might be wondering: "Why not just use a LinkedList?" I've tried that in other languages, but in Rust, VecDeque almost always wins. LinkedList scatters elements all over the heap, which is terrible for your CPU's cache. VecDeque keeps its data in a contiguous (though wrapped) block, meaning the CPU can fetch the next few elements much faster.
The only real trade-off is that VecDeque doesn't allow for the same kind of direct slicing that Vec does. If you need to pass a &[T] slice to a function, VecDeque can be a bit annoying because its elements might wrap around the end of the internal buffer, meaning they aren't always in one continuous slice. But for a queue? It's the gold standard.
📋 Practical Task
Implementing a Fixed-Size Undo Buffer
Build a small UndoBuffer struct that tracks the last 5 actions performed by a user. The buffer should maintain a maximum capacity of 5; when the 6th action is added, the oldest action should be automatically discarded.
Requirements:
- Use a
VecDequeto store the actions (strings). - Implement a method
add_action(&mut self, action: String)that pushes an action to the back and maintains the size limit. - Implement a method
undo(&mut self) -> Option<String>that removes and returns the most recent action (the back of the queue). - Implement a method
clear_oldest(&mut self) -> Option<String>that removes and returns the oldest action (the front of the queue).
There are no comments for now.