How to find min max in Vec

You can find the minimum and maximum values in a `Vec` using the built-in `min()` and `max()` methods, which return `Option<&T>` for safe handling of empty collections, or by using the `minmax()` method available in Rust 1.57+ to compute both in a single pass.

The dashboard needs bounds

Picture a weather station dashboard. The sensor dumps a Vec<f64> of temperatures every hour. You need to draw a chart. The chart needs the y-axis bounds. You grab the min and max.

In Python, you'd call min(data) and max(data). If the list is empty, you get an exception. In JavaScript, Math.min(...data) returns Infinity for an empty array, which is a silent logic trap. Rust takes a different path. The methods return Option<&T>. If the vector is empty, you get None. The compiler forces you to handle the empty case before your code can run. This design prevents crashes and makes the data shape explicit.

Why Option and references?

Rust's min() and max() methods return Option<&T>. The Option wrapper handles the empty case. The reference &T avoids copying the value. Iterators yield references by default. iter() borrows each element. min() compares the borrowed elements and returns a reference to the winner.

This matters for large types. If your vector holds structs with heavy fields, returning a reference avoids an expensive copy. For small types like integers or floats, you usually want the value, not the reference. That's where copied() comes in.

fn main() {
    let readings = vec![12.5, 45.0, 3.2, 99.1];

    // iter() yields &f64. min() returns Option<&f64>.
    // copied() converts Option<&f64> to Option<f64> because f64 is Copy.
    let min_val = readings.iter().min().copied();
    let max_val = readings.iter().max().copied();

    match (min_val, max_val) {
        (Some(min), Some(max)) => println!("Range: {} to {}", min, max),
        _ => println!("No data to analyze"),
    }
}

The copied() method works because f64 implements the Copy trait. It dereferences the reference and duplicates the bits. The result is an owned f64 inside the Option.

Convention aside: the community prefers copied() for Copy types like integers and floats. It signals that no heap allocation happens. cloned() suggests a deep copy might occur. Using copied() makes the performance characteristic visible in the code. Use copied() for primitives. It tells the reader you know the value is cheap to duplicate.

The single-pass workhorse

Calling min() and max() separately iterates the vector twice. For small vectors, this doesn't matter. For large datasets, a second pass wastes CPU cycles and hurts cache locality. Rust 1.57 introduced minmax() to solve this.

minmax() computes both values in a single pass. It also reduces the number of comparisons. A naive approach compares each element against the current min and the current max, resulting in two comparisons per element. minmax() uses a tournament-style algorithm. It compares pairs of elements first, then compares the smaller against the min and the larger against the max. This cuts the total comparisons to roughly 1.5N instead of 2N.

Ah-ha: minmax() is faster not just because it iterates once, but because it does fewer comparisons. The algorithmic improvement saves 25% of the work.

fn main() {
    let readings = vec![12.5, 45.0, 3.2, 99.1];

    // minmax() does one pass. Returns Option<(&T, &T)>.
    // The tuple contains references to the min and max elements.
    if let Some((min, max)) = readings.iter().minmax() {
        println!("Min: {}, Max: {}", min, max);
    } else {
        println!("Empty vector");
    }
}

The result is Option<(&T, &T)>. The if let pattern extracts the tuple. If the vector is empty, the else branch runs. This pattern is idiomatic for Option handling. It avoids nested matches and keeps the logic flat.

If you need both values, ask for both. The compiler won't merge two loops, and minmax() saves comparisons too.

Floating-point pitfalls

Floating-point numbers have a special value called NaN (Not a Number). NaN breaks ordering rules. Any comparison involving NaN returns false. min() and max() return None if the iterator contains a NaN. This prevents silent propagation of bad data.

This behavior catches bugs early. If a sensor fails and returns NaN, the result becomes None. Your code can detect the failure and alert the user. If min() returned NaN, the error might hide until the chart renders garbage.

fn main() {
    let data = vec![1.0, f64::NAN, 3.0];

    // min() returns None because NaN is present.
    let result = data.iter().min();
    assert!(result.is_none());

    // Filter NaNs to get a valid result.
    let clean_min = data.iter()
        .filter(|x| !x.is_nan())
        .min();
    
    println!("Clean min: {:?}", clean_min); // Some(1.0)
}

The filter() method removes NaNs before min() runs. The is_nan() check is explicit. This pattern is common in data processing pipelines. NaN is a black hole. Filter it out before you search.

Custom comparisons

Vectors often hold structs, not primitives. You might need the min or max based on a specific field. min_by_key() and max_by_key() handle this. You pass a closure that extracts the comparison key.

#[derive(Debug)]
struct Reading {
    id: u32,
    value: f64,
}

fn main() {
    let readings = vec![
        Reading { id: 1, value: 45.0 },
        Reading { id: 2, value: 12.5 },
        Reading { id: 3, value: 99.1 },
    ];

    // min_by_key extracts the value field for comparison.
    // Returns Option<&Reading>.
    if let Some(min_reading) = readings.iter().min_by_key(|r| r.value as i64) {
        println!("Lowest reading: {:?}", min_reading);
    }
}

The closure |r| r.value as i64 extracts the key. Casting to i64 avoids floating-point comparison issues in this example, though f64 works if you handle NaNs. The method returns Option<&Reading>, a reference to the struct.

Convention aside: min_by_key() calls the key function multiple times. If the key computation is expensive, this hurts performance. The convention is to use min_by_key() for cheap keys. For expensive keys, map the iterator first or use min_by() with pre-computed values. min_by_key() is convenient, not magical. Watch the key cost.

Error handling and types

The compiler enforces type safety. If you try to use min() on a vector of types that don't implement PartialOrd, you get a trait bound error. The error code is E0277. The message tells you which trait is missing.

If you forget copied() and try to store the result in a local variable, you might hit lifetime issues. The reference points into the vector. If the vector goes out of scope, the reference dangles. The compiler rejects this with a lifetime error. Using copied() or cloned() extracts an owned value, breaking the lifetime dependency.

fn get_min(data: Vec<f64>) -> f64 {
    // This fails. The reference points into data.
    // data is dropped at the end of the function.
    // The reference would be dangling.
    // let min_ref = data.iter().min();
    // *min_ref.unwrap() // ERROR: use of moved value or lifetime issue.

    // Correct: copied() extracts the value.
    data.iter().min().copied().unwrap_or(0.0)
}

The unwrap_or(0.0) provides a default value if the vector is empty. This is safe because 0.0 is a valid fallback. The compiler accepts this because the result is an owned f64, not a reference. Trust the borrow checker. It usually has a point.

Decision matrix

Use min() when you need only the smallest value and the collection is small enough that a second pass for max() doesn't matter.

Use max() when you need only the largest value.

Use minmax() when you need both values; it runs in a single pass and is the idiomatic choice for paired extrema.

Use min_by_key() when you need to compare structs based on a field and the key extraction is cheap.

Use fold() or a manual loop when you need to track indices, filter values on the fly, or implement custom comparison logic that PartialOrd doesn't cover.

Pick the tool that matches your data shape. minmax is the workhorse for pairs.

Where to go next