The Rust Collections Guide

Maps in Rust: HashMap and BTreeMap

Last Updated: 2026-04-05

Why maps matter

Maps are the standard way to store data by key rather than by position. Instead of asking for the item at index 3, you ask for the value associated with a key such as a username, a file path, an ID, or a timestamp bucket. This makes maps one of the most important collection families in Rust.

The standard library provides two major map types: HashMap<K, V> and BTreeMap<K, V>. Both store key-value pairs, but they organize and access data differently. HashMap<K, V> is optimized for fast average-case lookup by key when ordering does not matter. BTreeMap<K, V> keeps keys in sorted order and supports ordered iteration and range queries.

The core skill is not memorizing method names. It is learning to match the map type to the workload. Do you need fast key lookup without caring about order, or do you need sorted keys and predictable traversal?

What a map gives you

A map answers a particular shape-of-data question: given a key, what value is associated with it? That makes maps useful for counting, indexing, grouping, memoization, lookup tables, configuration overlays, caches, and many other tasks.

Unlike a vector, where the main access path is position, a map's main access path is the key. This changes how you think about insertion, lookup, update, and iteration.

use std::collections::HashMap;
 
fn main() {
    let mut capitals = HashMap::new();
    capitals.insert("Canada", "Ottawa");
    capitals.insert("France", "Paris");
 
    println!("Canada => {:?}", capitals.get("Canada"));
}

This is the essential map idea in its simplest form. A key identifies a value, and the collection manages that association.

Getting started with `HashMap<K, V>`

HashMap<K, V> is the standard unordered map type in Rust. It stores key-value pairs using hashing, which gives fast average-case insertion, lookup, and removal when keys can be hashed and compared for equality.

use std::collections::HashMap;
 
fn main() {
    let mut scores = HashMap::new();
    scores.insert("alice", 10);
    scores.insert("bob", 20);
    scores.insert("carol", 15);
 
    println!("scores = {:?}", scores);
    println!("bob => {:?}", scores.get("bob"));
}

The important idea is that HashMap<K, V> does not preserve sorted key order. Its iteration order is not something you should treat as stable or meaningful.

Getting started with `BTreeMap<K, V>`

BTreeMap<K, V> is the standard ordered map type in Rust. It keeps keys sorted according to their Ord implementation, which makes iteration deterministic and ordered.

use std::collections::BTreeMap;
 
fn main() {
    let mut scores = BTreeMap::new();
    scores.insert("carol", 15);
    scores.insert("alice", 10);
    scores.insert("bob", 20);
 
    println!("scores in key order:");
    for (name, score) in &scores {
        println!("  {name} => {score}");
    }
}

This is the first major contrast with HashMap<K, V>. A B-tree map gives you sorted order by key as part of the collection's behavior.

Insertion, replacement, and map semantics

Inserting into a map associates a value with a key. If the key is new, the pair is added. If the key already exists, the old value is replaced.

use std::collections::HashMap;
 
fn main() {
    let mut stock = HashMap::new();
    stock.insert("apples", 10);
    stock.insert("bananas", 5);
 
    let old = stock.insert("apples", 12);
 
    println!("old apples value = {:?}", old);
    println!("stock = {:?}", stock);
}

This replacement behavior is important. A map has at most one value for any given key. Updating by reinserting is often fine, but more structured update patterns become easier with the entry API.

Lookup patterns

The most common map operation is lookup by key. Both HashMap<K, V> and BTreeMap<K, V> support get, which returns Option<&V>.

use std::collections::HashMap;
 
fn main() {
    let mut users = HashMap::new();
    users.insert(101, "Alice");
    users.insert(102, "Bob");
 
    println!("101 => {:?}", users.get(&101));
    println!("999 => {:?}", users.get(&999));
}

Because lookup returns an Option, missing keys are handled explicitly. This is a recurring Rust pattern: absence is part of the type system rather than something hidden.

Checking existence and removing entries

Maps also support presence checks and removal. These are useful when the program needs to branch on whether a key exists or consume an entry entirely.

use std::collections::HashMap;
 
fn main() {
    let mut sessions = HashMap::new();
    sessions.insert("token-1", "alice");
    sessions.insert("token-2", "bob");
 
    println!("has token-1 = {}", sessions.contains_key("token-1"));
 
    let removed = sessions.remove("token-2");
    println!("removed = {:?}", removed);
    println!("sessions = {:?}", sessions);
}

Together, contains_key, get, insert, and remove form the basic vocabulary of map use.

Updating values with mutable access

Sometimes you do not want to replace a value wholesale. You want to mutate the value already stored under a key. Both map types support this through methods like get_mut.

use std::collections::HashMap;
 
fn main() {
    let mut counts = HashMap::new();
    counts.insert("red", 2);
    counts.insert("blue", 1);
 
    if let Some(value) = counts.get_mut("red") {
        *value += 3;
    }
 
    println!("counts = {:?}", counts);
}

This is useful when the value is something large or structured and you want to update it in place rather than replace it.

The entry API: why it exists

The entry API is one of the most important map patterns in Rust. It handles the common case where you want to update a value if the key exists, or insert a default if it does not. Without it, code often has to do redundant lookups or awkward branching.

use std::collections::HashMap;
 
fn main() {
    let mut counts = HashMap::new();
 
    for word in ["apple", "banana", "apple", "pear", "banana", "apple"] {
        *counts.entry(word).or_insert(0) += 1;
    }
 
    println!("counts = {:?}", counts);
}

This pattern is central because maps are often used for counting and grouping. The entry API lets you express that logic directly and efficiently.

Common entry API patterns

The two entry methods learners meet first are or_insert and or_default. or_insert(value) inserts a provided value when the key is absent. or_default() inserts the type's default value.

use std::collections::HashMap;
 
fn main() {
    let mut settings: HashMap<&str, String> = HashMap::new();
    settings.entry("theme").or_insert("dark".to_string());
    settings.entry("theme").or_insert("light".to_string());
 
    let mut groups: HashMap<&str, Vec<i32>> = HashMap::new();
    groups.entry("even").or_default().push(2);
    groups.entry("even").or_default().push(4);
    groups.entry("odd").or_default().push(1);
 
    println!("settings = {:?}", settings);
    println!("groups = {:?}", groups);
}

This style becomes especially powerful when the value type is itself a collection such as Vec<T> or HashSet<T>.

Grouping data with maps

A map is often the natural structure for grouping items by category. The key represents the group, and the value holds the accumulated items for that group.

use std::collections::BTreeMap;
 
fn main() {
    let mut by_length: BTreeMap<usize, Vec<&str>> = BTreeMap::new();
 
    for word in ["cat", "pear", "dog", "plum", "sun"] {
        by_length.entry(word.len()).or_default().push(word);
    }
 
    for (len, words) in &by_length {
        println!("{len} => {:?}", words);
    }
}

This example uses BTreeMap<K, V> so that the groups print in ascending key order. The same grouping idea works with HashMap<K, V> when order is unimportant.

Iteration: ordered versus unordered

Both map types support iteration, but the meaning of the order differs. A HashMap<K, V> does not promise a useful or stable key order. A BTreeMap<K, V> iterates in sorted key order.

use std::collections::{BTreeMap, HashMap};
 
fn main() {
    let mut hm = HashMap::new();
    hm.insert(3, "c");
    hm.insert(1, "a");
    hm.insert(2, "b");
 
    let mut bt = BTreeMap::new();
    bt.insert(3, "c");
    bt.insert(1, "a");
    bt.insert(2, "b");
 
    println!("hash map iteration:");
    for (k, v) in &hm {
        println!("  {k} => {v}");
    }
 
    println!("btree map iteration:");
    for (k, v) in &bt {
        println!("  {k} => {v}");
    }
}

This difference matters any time output order is user-visible, test-sensitive, or part of the intended semantics.

Range queries with `BTreeMap<K, V>`

One of the strongest reasons to choose BTreeMap<K, V> is support for range-based access. Because the keys are ordered, you can iterate over a key interval directly.

use std::collections::BTreeMap;
 
fn main() {
    let mut readings = BTreeMap::new();
    readings.insert(10, "low");
    readings.insert(20, "normal");
    readings.insert(30, "high");
    readings.insert(40, "critical");
 
    for (k, v) in readings.range(15..=35) {
        println!("{k} => {v}");
    }
}

This is a capability that does not naturally belong to HashMap<K, V>. If range traversal is part of the workload, BTreeMap<K, V> is often the obvious fit.

Choosing between `HashMap<K, V>` and `BTreeMap<K, V>`

A good first-pass rule is simple. Choose HashMap<K, V> when you want fast average-case lookup and ordering does not matter. Choose BTreeMap<K, V> when you need sorted keys, deterministic iteration order, or range queries.

Neither type is universally better. They solve overlapping but distinct problems. HashMap<K, V> is often the default for counting, indexing by ID, caches, and general lookup tables. BTreeMap<K, V> is often the better fit for ordered reports, interval-oriented logic, and any situation where key order has meaning.

Trait requirements for keys

The two map types differ in what they require from key types. HashMap<K, V> requires keys that implement hashing and equality in the right way, typically through Eq and Hash. BTreeMap<K, V> requires keys that implement total ordering through Ord.

use std::collections::{BTreeMap, HashMap};
 
#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
struct UserId(u32);
 
fn main() {
    let mut hm = HashMap::new();
    hm.insert(UserId(1), "alice");
 
    let mut bt = BTreeMap::new();
    bt.insert(UserId(2), "bob");
 
    println!("hm = {:?}", hm);
    println!("bt = {:?}", bt);
}

This is a practical reason to think carefully about your key type. The collection's requirements shape which traits your domain types need.

Maps with borrowed and owned keys

Map APIs often work well with borrowed lookup forms. A common example is storing owned String keys while looking them up using &str.

use std::collections::HashMap;
 
fn main() {
    let mut users: HashMap<String, i32> = HashMap::new();
    users.insert("alice".to_string(), 10);
    users.insert("bob".to_string(), 20);
 
    println!("alice => {:?}", users.get("alice"));
}

This is important because it avoids unnecessary allocation during lookup. You do not have to build a new String just to search for an existing string key.

When a vector is still enough

Maps are powerful, but they are not automatically the right answer. If the data set is small, the key space is simple, or lookups are infrequent, a vector of pairs may be entirely adequate.

fn find_price(items: &[(&str, i32)], name: &str) -> Option<i32> {
    items.iter().find(|(k, _)| *k == name).map(|(_, v)| *v)
}
 
fn main() {
    let prices = vec![("apple", 3), ("banana", 2), ("pear", 4)];
    println!("pear => {:?}", find_price(&prices, "pear"));
}

This is not an argument against maps. It is a reminder that collection choice should follow workload, not habit. Maps earn their place when key-based access is central enough to justify them.

Performance intuition without overcommitting to Big-O

It is useful to know the broad intuition: HashMap<K, V> is usually chosen for fast average-case key lookup, while BTreeMap<K, V> trades some of that lookup shape for ordered behavior and range-friendly traversal. But real performance depends on more than asymptotic complexity. Memory layout, key size, iteration needs, and output requirements all matter.

A HashMap<K, V> can be ideal for a frequency table where order is irrelevant. A BTreeMap<K, V> can be ideal for producing stable, sorted reports without a separate sorting step. The right choice depends on what the program actually does most often.

API design with maps

When designing functions around maps, think carefully about whether callers truly need to know the concrete map type. Sometimes returning a specific map type is the right choice because its properties matter, such as sorted output from a BTreeMap<K, V>. In other cases, the map is just an implementation detail.

A function that builds a word-frequency table can reasonably return HashMap<String, usize> if fast key lookup is the primary goal. A function that prepares a stable ordered summary might intentionally return BTreeMap<String, usize> because sorted iteration is part of the result's meaning.

The main idea is the same as elsewhere in Rust collection design: choose concrete types when their semantics matter, and avoid unnecessary commitment when they do not.

A small decision guide

Choose HashMap<K, V> when key-based lookup is the core need and key order does not matter.

Choose BTreeMap<K, V> when sorted keys, deterministic iteration, or range queries are part of the problem.

Use the entry API when the logic is naturally "update if present, insert if absent." This is especially common in counting and grouping.

Do not reach for a map automatically if a short vector scan would be simpler and sufficient for the workload.

A small sandbox project

A tiny Cargo project is enough to compare HashMap<K, V> and BTreeMap<K, V> side by side.

[package]
name = "maps-guide"
version = "0.1.0"
edition = "2024"

Create and run it like this.

cargo new maps-guide
cd maps-guide
cargo run

A minimal src/main.rs could look like this.

use std::collections::{BTreeMap, HashMap};
 
fn word_counts(text: &str) -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word.to_string()).or_insert(0) += 1;
    }
    counts
}
 
fn ordered_counts(text: &str) -> BTreeMap<String, usize> {
    let mut counts = BTreeMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word.to_string()).or_insert(0) += 1;
    }
    counts
}
 
fn main() {
    let text = "pear apple pear banana apple pear";
 
    let fast = word_counts(text);
    println!("hash map counts = {:?}", fast);
 
    let ordered = ordered_counts(text);
    println!("btree map counts:");
    for (word, count) in &ordered {
        println!("  {word} => {count}");
    }
 
    println!("range over ordered keys:");
    for (word, count) in ordered.range("banana".to_string()..="pear".to_string()) {
        println!("  {word} => {count}");
    }
}

This one program shows the core contrasts clearly: both map types can count words, but HashMap<K, V> emphasizes general key lookup while BTreeMap<K, V> adds ordered traversal and range-oriented behavior.