The Rust Collections Guide
Sets in Rust: HashSet and BTreeSet
Last Updated: 2026-04-05
Why sets matter
A set is a collection of unique values. Unlike a vector, it does not primarily answer "what is at position i?" Unlike a map, it does not primarily answer "what value belongs to this key?" Instead, a set answers a different question: "is this value present?"
That makes sets useful for membership tests, deduplication, overlap checks, blocklists, feature flags, visited-node tracking, and many other tasks where uniqueness matters more than order or duplication.
Rust's standard library provides two major set types: HashSet<T> and BTreeSet<T>. The choice between them mirrors the choice between HashMap<K, V> and BTreeMap<K, V>. HashSet<T> is typically used when fast average-case membership checks matter and order does not. BTreeSet<T> is used when sorted order, deterministic iteration, or range-style traversal matters.
What a set gives you
A set models uniqueness directly. If you insert the same value more than once, the set still contains only one copy. That makes a set a stronger abstraction than a vector whenever duplicates are not meaningful and membership is the main concern.
use std::collections::HashSet;
fn main() {
let mut tags = HashSet::new();
tags.insert("rust");
tags.insert("collections");
tags.insert("rust");
println!("tags = {:?}", tags);
println!("len = {}", tags.len());
}Even though "rust" was inserted twice, the set still contains only one copy. That is the core set idea.
Getting started with `HashSet<T>`
HashSet<T> is the standard unordered set type in Rust. It stores unique values using hashing, which makes average-case membership checks, insertions, and removals efficient when ordering is not important.
use std::collections::HashSet;
fn main() {
let mut seen = HashSet::new();
seen.insert("alice");
seen.insert("bob");
seen.insert("carol");
println!("seen bob = {}", seen.contains("bob"));
println!("seen dave = {}", seen.contains("dave"));
}The main value of HashSet<T> is not that it stores items in a specific order. It is that it can efficiently answer membership questions.
Getting started with `BTreeSet<T>`
BTreeSet<T> is the standard ordered set type in Rust. It keeps elements sorted according to their Ord implementation, which makes iteration deterministic and ordered.
use std::collections::BTreeSet;
fn main() {
let mut words = BTreeSet::new();
words.insert("pear");
words.insert("apple");
words.insert("banana");
for word in &words {
println!("{word}");
}
}This differs from HashSet<T>, whose iteration order is not something you should depend on. If sorted output matters, BTreeSet<T> often fits naturally.
Insertion, existence, and removal
The most common set operations are insertion, membership tests, and removal.
use std::collections::HashSet;
fn main() {
let mut users = HashSet::new();
let a = users.insert("alice");
let b = users.insert("alice");
println!("first insert changed set = {}", a);
println!("second insert changed set = {}", b);
println!("contains alice = {}", users.contains("alice"));
let removed = users.remove("alice");
println!("removed alice = {}", removed);
println!("contains alice = {}", users.contains("alice"));
}Notice that insert returns a bool. It tells you whether the set changed, which is a direct way to detect whether the value was new.
Sets as a natural deduplication tool
One of the most common uses of sets is deduplication. If you have input with repeated values and only care about the unique values, collecting into a set is often the simplest expression of intent.
use std::collections::HashSet;
fn main() {
let items = vec!["red", "blue", "red", "green", "blue"];
let unique: HashSet<_> = items.into_iter().collect();
println!("unique = {:?}", unique);
}This is often better than writing manual membership checks against a vector because the abstraction itself says what the code means: uniqueness matters.
When a vector is still enough
Sets are useful, but they are not always necessary. If the data is very small, order matters, or you only need occasional duplicate removal, a vector may still be the simpler choice.
fn dedup_preserving_first_order(input: &[&str]) -> Vec<&str> {
let mut out = Vec::new();
for item in input {
if !out.contains(item) {
out.push(*item);
}
}
out
}
fn main() {
let values = ["a", "b", "a", "c", "b"];
println!("{:?}", dedup_preserving_first_order(&values));
}This works, and for small inputs it may be perfectly acceptable. The set abstraction becomes more compelling when membership testing and uniqueness are not incidental but central to the workload.
Ordered versus unordered sets
The central tradeoff between HashSet<T> and BTreeSet<T> is the same broad tradeoff seen with maps. HashSet<T> emphasizes general membership performance when order does not matter. BTreeSet<T> emphasizes ordered behavior and sorted iteration.
use std::collections::{BTreeSet, HashSet};
fn main() {
let mut fast = HashSet::new();
fast.insert(3);
fast.insert(1);
fast.insert(2);
let mut ordered = BTreeSet::new();
ordered.insert(3);
ordered.insert(1);
ordered.insert(2);
println!("hash set iteration:");
for value in &fast {
println!(" {value}");
}
println!("btree set iteration:");
for value in &ordered {
println!(" {value}");
}
}If output order has meaning, testing expectations, or user-facing importance, BTreeSet<T> is often the better choice.
Set algebra: union, intersection, difference, symmetric difference
Sets are especially valuable because they support set algebra directly. These operations let you express overlap and combination clearly.
use std::collections::HashSet;
fn main() {
let a: HashSet<_> = [1, 2, 3, 4].into_iter().collect();
let b: HashSet<_> = [3, 4, 5, 6].into_iter().collect();
let union: Vec<_> = a.union(&b).copied().collect();
let intersection: Vec<_> = a.intersection(&b).copied().collect();
let difference: Vec<_> = a.difference(&b).copied().collect();
let symmetric_difference: Vec<_> = a.symmetric_difference(&b).copied().collect();
println!("union = {:?}", union);
println!("intersection = {:?}", intersection);
println!("difference = {:?}", difference);
println!("symmetric_difference = {:?}", symmetric_difference);
}These operations are useful whenever you need to compare permissions, features, tags, categories, or visited states across multiple groups.
Subset, superset, and disjointness checks
Sets also support relationship checks between groups of values. These are often clearer than writing custom loops.
use std::collections::HashSet;
fn main() {
let small: HashSet<_> = [1, 2].into_iter().collect();
let large: HashSet<_> = [1, 2, 3, 4].into_iter().collect();
let other: HashSet<_> = [8, 9].into_iter().collect();
println!("small subset of large = {}", small.is_subset(&large));
println!("large superset of small = {}", large.is_superset(&small));
println!("small disjoint with other = {}", small.is_disjoint(&other));
}These methods are especially useful in validation logic and rules systems where set relationships are part of the domain.
Common real uses of sets
Sets are often the right abstraction when the question is fundamentally about presence or uniqueness. A few common examples are tracking visited nodes in graph traversal, keeping a blocklist of IDs, determining whether a user has a required permission, filtering duplicates from a stream, and checking whether two groups overlap.
use std::collections::HashSet;
fn main() {
let mut visited = HashSet::new();
for node in [0, 1, 2, 1, 3, 2] {
if visited.insert(node) {
println!("first time seeing node {node}");
} else {
println!("already saw node {node}");
}
}
}This pattern is a good example of how set semantics directly match the problem.
Range-oriented behavior with `BTreeSet<T>`
One reason to choose BTreeSet<T> is that ordered sets can support range-style traversal. Because elements are stored in sorted order, you can iterate over a subset of values bounded by a range.
use std::collections::BTreeSet;
fn main() {
let values: BTreeSet<_> = [10, 20, 30, 40, 50, 60].into_iter().collect();
for value in values.range(20..=50) {
println!("{value}");
}
}This is one of the clearest capabilities that separates BTreeSet<T> from HashSet<T>. If interval-like traversal matters, the ordered tree structure is often the right fit.
Trait requirements for set elements
The two set types differ in what they require from their element types. HashSet<T> requires hashing and equality behavior, typically through Hash and Eq. BTreeSet<T> requires ordering through Ord.
use std::collections::{BTreeSet, HashSet};
#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
struct Tag(&'static str);
fn main() {
let mut hs = HashSet::new();
hs.insert(Tag("alpha"));
let mut bs = BTreeSet::new();
bs.insert(Tag("beta"));
println!("hs = {:?}", hs);
println!("bs = {:?}", bs);
}This matters when designing domain types. The collection you choose influences which traits your types need to implement.
Borrowed lookup and owned values
As with maps, set membership checks often work well with borrowed forms. A common example is storing owned String values in a set while checking membership with &str.
use std::collections::HashSet;
fn main() {
let mut names: HashSet<String> = HashSet::new();
names.insert("alice".to_string());
names.insert("bob".to_string());
println!("contains alice = {}", names.contains("alice"));
}This is useful because it avoids unnecessary allocation at lookup time.
Performance intuition without overcomplicating it
A good rough intuition is that HashSet<T> is usually chosen when fast average-case membership testing matters and ordering does not. BTreeSet<T> is chosen when ordered traversal, deterministic iteration, or range-based access matters.
But as with maps, real performance depends on more than abstract complexity. Memory layout, output requirements, and workload shape all matter. A set is not automatically better than a vector just because membership exists as an operation. It becomes the right tool when membership and uniqueness define the actual problem.
When a set is the right abstraction
A set is the right abstraction when duplicates are not meaningful and the main questions are about presence, overlap, uniqueness, or membership relationships.
A set is often a stronger model than a vector when you find yourself repeatedly writing code like if !items.contains(&x) { items.push(x); } or when you want to compare groups through union and intersection rather than manual loops.
On the other hand, if order matters, duplicates are meaningful, or the data is tiny and only scanned occasionally, a vector may still be simpler.
API design with sets
When returning or accepting sets in APIs, think about whether uniqueness itself is part of the contract. If it is, a set can communicate intent more clearly than a vector.
For example, returning a HashSet<String> from a function can signal that the caller should think in terms of membership rather than ordering or duplication. Returning a BTreeSet<String> can go further and signal that sorted iteration is part of the result's meaning.
As elsewhere in Rust collection design, pick the concrete type when its semantics matter, not just because it happens to work.
A small decision guide
Choose HashSet<T> when uniqueness and membership matter and order does not.
Choose BTreeSet<T> when uniqueness matters and you also want sorted iteration, deterministic order, or range-style access.
Use set algebra when the problem is about overlap, difference, or combination between groups.
Do not reach for a set automatically if a short ordered vector is simpler and already matches the workload.
A small sandbox project
A tiny Cargo project is enough to explore HashSet<T> and BTreeSet<T> side by side.
[package]
name = "sets-guide"
version = "0.1.0"
edition = "2024"Create and run it like this.
cargo new sets-guide
cd sets-guide
cargo runA minimal src/main.rs could look like this.
use std::collections::{BTreeSet, HashSet};
fn main() {
let input = ["pear", "apple", "pear", "banana", "apple"];
let fast: HashSet<_> = input.into_iter().collect();
println!("hash set unique = {:?}", fast);
let ordered: BTreeSet<_> = input.into_iter().collect();
println!("btree set ordered unique:");
for item in &ordered {
println!(" {item}");
}
let a: HashSet<_> = ["read", "write"].into_iter().collect();
let b: HashSet<_> = ["write", "execute"].into_iter().collect();
let overlap: Vec<_> = a.intersection(&b).copied().collect();
println!("overlap = {:?}", overlap);
}This one small program demonstrates the core roles of sets: uniqueness, membership-oriented thinking, ordered versus unordered behavior, and direct overlap computation through set algebra.
