Start learning free

LESSON 266 OF 414 · HASH TABLE

Hash Sets in Rust: HashSet Basics

Lesson Read the full explanation

Hash Sets are collections of unique items i.e no element can be repeated.

Hash Sets were designed to give us a way of adding and looking for unique values in a collection in a quick manner.

The values in a Hash Set can be either simple primitives like strings or integers as well as more complex types like structs or tuples.

In Rust, the Hash Set is implemented as the HashSet type from the Standard Library.


Creation:

To create a new empty HashSet that holds strings, you use the following syntax:

use std::collections::HashSet;
let mut my_set: HashSet<String> = HashSet::new();

Creating an empty Set takes O(1) time.


Constructing from iterable

You can also build a set from any iterator with collect(). All the unique elements of the iterator will be added to the new set:

let elements = vec!["b", "c", "c", "d"];
let my_set: HashSet<&str> = elements.into_iter().collect();

// my_set contains {"b", "c", "d"}

This takes O(n) time, where n is the number of elements in the iterator.


Adding elements:

To add an element to the set, you use the insert() method:

let mut my_set: HashSet<&str> = ["b", "c", "c", "d"].iter().cloned().collect();

// Adding new elements
my_set.insert("x");

// Adding an existing element
my_set.insert("d"); // nothing happens

// my_set contains {"b", "c", "d", "x"}

The Set first checks if that element already exists and if so, it does nothing and also doesn't raise an error.

Adding an element to a Set takes O(1) time.


Checking if a value exists:

To check if a Set has a specific element, you use the contains() method which returns true if the set contains the element, otherwise, it returns false.

let my_set: HashSet<&str> = ["b", "c", "c", "d"].iter().cloned().collect();

my_set.contains("c"); // returns true since 'c' exists in the set

my_set.contains("x"); // returns false since it does not exist in the set

Checking if a value exists in a Set takes O(1) time.


Removing elements:

To delete a specified element from a set, you use the remove() method:

let mut my_set: HashSet<&str> = ["b", "c", "c", "d"].iter().cloned().collect();

// Deleting elements
my_set.remove("c");

// Deleting a non-existent element
my_set.remove("z"); // does nothing

// my_set contains {"b", "d"}

The Set first checks if that key exists and if not, it does nothing and also doesn't raise an error, so it's safe to delete non-existing elements.

Removing an element from a Set takes O(1) time.


Iterating over the Set values:

If we want to iterate over values of the HashSet, we can use the for loop along with the & operator:

let my_set: HashSet<&str> = ["b", "c", "c", "d"].iter().cloned().collect();

for val in &my_set {
    println!("{}", val);
}

// This will print the following:
// d
// c
// b

Iterating over a HashSet takes O(n) time.

Notice the order is not the same as we initiated. HashSet keeps the data in random order


Space Complexity

A Set uses O(n) space, where n is the number of elements existing in the Set.


Assignment
Follow the Coding Tutorial and let's play with some Hash Sets.


Hint
Look at the examples above if you get stuck.


STEP 1 OF 6
    Ctrl + Enter to check

    RUST · HASH TABLE

    Hash Set in Rust, explained

    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 returns true when the value was new and false when the set already had it. Inserting a duplicate is not an error and changes nothing.
    • contains(&value) returns true or false. For a set of &str you can pass a literal such as "apples" directly, and a set of String can be queried with a &str.
    • remove(&value) deletes the value and returns true if it was present. Removing an absent value is safe and returns false.
    • 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

    1. Start with data that has a duplicate. fruits_array is vec!["apples", "bananas", "apples"], a Vec<&str>.
    2. Build the set. fruits is a mut HashSet<&str> created with fruits_array.into_iter().collect(). It is mut because later steps insert and remove. It holds "apples" and "bananas", since the second apple is dropped.
    3. Insert "oranges". The set now has three elements.
    4. Test membership. fruits.contains("apples") is true, so the program prints We have apples.
    5. Remove "bananas", leaving apples and oranges.
    6. 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 in let s: HashSet<i32> = ..., or write collect::<HashSet<_>>().
    • Forgetting mut on a set you insert into or remove from.
    • Expecting order. A HashSet has no position for elements and no indexing like my_set[0].
    • Expecting insert to replace. If an equal value is already there, the set keeps it and returns false.
    • Using the vector after into_iter(). It was moved. Use iter().cloned() to keep the original.

    Key takeaways

    • HashSet<T> holds unique values; insert, contains and remove are O(1) on average.
    • insert and remove return a bool telling you whether the set changed.
    • Collecting into a set removes duplicates, and a type annotation tells collect what to build.
    • Iteration order is arbitrary, and a pass costs O(n).
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free