What a hash set is
A hash set stores unique items: no value can appear twice. It is built for two jobs: adding a value, and asking "is this value in the collection?". Both take O(1) time on average, unlike a Vec, where checking membership means scanning every element.
Rust's hash set is HashSet<T> from std::collections. It is essentially a HashMap that keeps only keys. The element type must be hashable and comparable for equality, which holds for integers, strings, char, tuples of those, and so on. Floating-point numbers do not qualify.
Creating a set
Import the type, then create an empty set with HashSet::new(). Declare it mut if you will change it.
use std::collections::HashSet;
let mut my_set: HashSet<String> = HashSet::new();
To build a set from existing data, turn the data into an iterator and call collect(). The type annotation matters here, because collect can build many kinds of collections and needs to know which one you want. Duplicates are dropped as the set is filled.
let elements = vec!["b", "c", "c", "d"];
let my_set: HashSet<&str> = elements.into_iter().collect();
This set holds "b", "c" and "d", three elements. into_iter() consumes the vector, so elements can't be used afterwards. Building the set takes O(n) time for n items.
Adding, checking and removing
insert(value)adds a value. It returnstruewhen the value was new andfalsewhen the set already had it. Inserting a duplicate is not an error and changes nothing.contains(&value)returnstrueorfalse. For a set of&stryou can pass a literal such as"apples"directly, and a set ofStringcan be queried with a&str.remove(&value)deletes the value and returnstrueif it was present. Removing an absent value is safe and returnsfalse.len()gives the number of elements.
The boolean results are useful. Code that must act only the first time a value appears can branch on the result of insert, with no separate contains call.
Iterating
Loop over a set with for value in &my_set. The & borrows the set, so it is still usable afterwards; without it the loop would consume the set. A pass takes O(n) time.
The iteration order is arbitrary. It may differ from the insertion order, and from one run of the program to the next. If the order matters, collect the elements into a Vec and sort it.
A set uses O(n) space for n elements.
The exercise, step by step
- Start with data that has a duplicate.
fruits_arrayisvec!["apples", "bananas", "apples"], aVec<&str>. - Build the set.
fruitsis amut HashSet<&str>created withfruits_array.into_iter().collect(). It ismutbecause later steps insert and remove. It holds"apples"and"bananas", since the second apple is dropped. - Insert
"oranges". The set now has three elements. - Test membership.
fruits.contains("apples")is true, so the program printsWe have apples. - Remove
"bananas", leaving apples and oranges. - Print each fruit with
for fruit in &fruits.
let fruits_array = vec!["apples", "bananas", "apples"];
let mut fruits: HashSet<&str> = fruits_array.into_iter().collect();
fruits.insert("oranges");
if fruits.contains("apples") {
println!("We have apples");
}
fruits.remove("bananas");
for fruit in &fruits {
println!("{}", fruit);
}
The output is We have apples, followed by apples and oranges in either order.
Common mistakes
- Leaving out the type on
collect(). The compiler cannot guess which collection to build. Annotate the variable, as inlet s: HashSet<i32> = ..., or writecollect::<HashSet<_>>(). - Forgetting
muton a set you insert into or remove from. - Expecting order. A
HashSethas no position for elements and no indexing likemy_set[0]. - Expecting
insertto replace. If an equal value is already there, the set keeps it and returnsfalse. - Using the vector after
into_iter(). It was moved. Useiter().cloned()to keep the original.
Key takeaways
HashSet<T>holds unique values;insert,containsandremoveareO(1)on average.insertandremovereturn abooltelling you whether the set changed.- Collecting into a set removes duplicates, and a type annotation tells
collectwhat to build. - Iteration order is arbitrary, and a pass costs
O(n).
