How to get the largest element from a collection
Short answer: for a one-off, iter().max() — it returns Option<&T>, so an empty
collection gives None instead of a panic. When the largest has to come out again and
again, use BinaryHeap: push adds to it, pop takes the largest back out, and peek
looks without taking.
From a fresh project — cargo new max-demo — replace the contents of src/main.rs with
each block below and run it with cargo run. The project name shows up only in cargo’s own
output; nothing else here depends on it. Everything here is Rust 1.99.0, edition 2024.
max() for a one-off
Section titled “max() for a one-off”fn main() { let nums = vec![3, 7, 2, 9, 4]; println!("{:?}", nums.iter().max()); println!("{:?}", nums.iter().copied().max());
let empty: Vec<i32> = Vec::new(); println!("{:?}", empty.iter().max());}Some(9)Some(9)Nonemax lives on Iterator, so a Vec or a slice reaches it through iter(). The answer is
an Option because the collection can be empty, and the standard library’s own words are
that “if several elements are equally maximum, the last element is returned”. One pass, no
allocation, nothing that can panic.
Both lines print the same — references print transparently in Debug — but the types
differ. iter().max() borrows and gives Option<&T>, leaving the collection usable;
copied().max() gives an owned Option<T> for Copy types; into_iter().max() consumes
the collection to get there.
Ties, and your own idea of “largest”
Section titled “Ties, and your own idea of “largest””fn main() { let scores = vec![("Ada", 3), ("Grace", 5), ("Lin", 5)]; let best = scores.iter().max_by_key(|(_, score)| *score); println!("{best:?}");
let names = vec!["Ada".to_string(), "Grace".to_string(), "Lin".to_string()]; let longest = names.iter().max_by_key(|s| s.len()); println!("{longest:?}"); println!("names still usable: {names:?}");}Some(("Lin", 5))Some("Grace")names still usable: ["Ada", "Grace", "Lin"]max_by_key compares whatever the closure returns, which is how a Vec of structs is
searched by a field. Both 5s tied and the later one won, exactly as the documentation says.
The second half shows the borrow from the outside: longest references names, and
names still prints afterwards — the same rule
find lives under.
BinaryHeap when you keep taking the largest
Section titled “BinaryHeap when you keep taking the largest”use std::collections::BinaryHeap;
fn main() { let mut heap = BinaryHeap::from([4, 1, 7, 3, 9, 2]); println!("peek: {:?}", heap.peek()); heap.push(11); println!("after push(11), peek: {:?}", heap.peek()); println!("pop: {:?}", heap.pop()); println!("iter: {:?}", heap.iter().collect::<Vec<_>>()); while let Some(n) = heap.pop() { println!("drain: {n}"); } println!("pop on empty: {:?}", heap.pop());}peek: Some(9)after push(11), peek: Some(11)pop: Some(11)iter: [9, 4, 7, 3, 1, 2]drain: 9drain: 7drain: 4drain: 3drain: 2drain: 1pop on empty: NoneA BinaryHeap is a priority queue. peek costs O(1) and returns Option<&T>; pop
costs O(log n) in the worst case and returns Option<T>; converting a Vec into one
happens in place in O(n). The drain loop is the whole pattern — while let Some(n) = heap.pop() runs until the heap is empty, always taking the largest first.
Two things surprise people. iter() walks the underlying array “in arbitrary order”, not
sorted — the [9, 4, 7, 3, 1, 2] above — so reach for pop when order matters, or
into_sorted_vec() to consume the heap into a sorted Vec. And the heap allocates, which
is the wrong price for a single maximum.
When max() is enough
Section titled “When max() is enough”One maximum, asked once: iter().max() — one linear pass, no allocation, an Option back.
The heap earns its keep when the question repeats: each push and pop is logarithmic
where a fresh scan per answer is linear. For the top few rather than the top one,
select_nth_unstable partitions around the nth element in O(n) without allocating;
sorting everything is
its own page.
The float trap
Section titled “The float trap”max needs Ord, and f64 only has PartialOrd, because NaN is incomparable:
fn main() { let temps: Vec<f64> = vec![21.5, 19.0, 23.8]; let hottest = temps.iter().max(); println!("{hottest:?}");}error[E0277]: the trait bound `f64: Ord` is not satisfied --> src/main.rs:3:32 | 3 | let hottest = temps.iter().max(); | ^^^ the trait `Ord` is not implemented for `f64` | = help: the following other types implement trait `Ord`: i128 i16 i32 i64 i8 isize u128 u16 and 4 others = note: required for `&f64` to implement `Ord`note: required by a bound in `std::iter::Iterator::max` --> /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/iter/traits/iterator.rs:3249:21 |3246 | fn max(self) -> Option<Self::Item> | --- required by a bound in this associated function...3249 | Self::Item: Ord, | ^^^ required by this bound in `Iterator::max`
For more information about this error, try `rustc --explain E0277`.The fix is max_by with total_cmp, which orders floats by IEEE 754’s totalOrder:
fn main() { let temps: Vec<f64> = vec![21.5, 19.0, 23.8]; let hottest = temps.iter().copied().max_by(f64::total_cmp); println!("{hottest:?}");}Some(23.8)total_cmp gives every float a place in that order, so the usual f64::NAN counts as the
largest value instead of quietly poisoning the comparison.
Related
Section titled “Related”- Which collection should you use? — the decision table for this chapter
- How to get an element from a Vec without panicking —
get, and the sameOptionlesson - What is the difference between HashMap and BTreeMap? — the other collection with a sorted answer
Next in this chapter: why indexing an array out of bounds panics.