Skip to content

← Ground Rules: Values, Types, Control Flow step 20 of 24

Easy Primitives

Remove duplicates while preserving order

Return each distinct input value once, in the order of its first appearance. Leave the input unchanged.

pub fn dedup_preserve_order(values: &[i32]) -> Vec<i32>

[1, 2, 1, 3, 2] produces [1, 2, 3]. An empty slice produces an empty vector, and [7, 7, 7] produces [7]. Negative values and the full i32 range are valid. Do not sort the result: [5, 2, 5, 1] must produce [5, 2, 1], not [1, 2, 5].

The starter computes the right values, but its parameter and loop trigger Clippy warnings. Fix those warnings while preserving the behavior. The &[i32] signature also lets callers pass arrays and slices directly.

A vector stores a sequence that can grow

Vec<i32> owns contiguous storage for its elements. Its length counts the initialized elements, and its capacity is how many elements it can hold before it needs to grow. Pushing an element increases the length by one.

A vector keeps a pointer, length and capacity separately from its element storage. An empty Vec::new() need not allocate. Growth can reallocate its buffer, possibly moving the elements; do not assume the address stays fixed. Rust does not specify a fixed growth factor or an exact allocation count. Vec::with_capacity(n) reserves room for at least n elements, but here the number of distinct values may be much smaller than the input length.

Accept a slice when you only need the elements

This function only reads its input. A slice provides the operations it needs without requiring the caller to supply a vector. For example, a function taking a slice can read all or part of an array:

fn first(values: &[i32]) -> Option<i32> {
    values.first().copied()
}

fn main() {
    let values = [8, 3, 8];
    assert_eq!(first(&values), Some(8));
    assert_eq!(first(&values[1..]), Some(3));
    assert_eq!(first(&[]), None);
}

A vector also works through a slice borrow, such as &values or &values[1..]. Clippy's ptr_arg warning recommends changing this starter's &Vec<i32> parameter to &[i32]. This is appropriate because the function does not need vector-specific information such as capacity. It is not a rule that every function taking a vector reference can always use a slice.

Simplify the loop and condition

The starter indexes values using every integer in 0..values.len(). Replace that with for &value in values: it borrows each element and copies its i32 value into the loop variable. This addresses needless_range_loop and avoids managing an index that the algorithm does not otherwise need. It does not imply that every indexed loop is wrong or that each indexed access necessarily retains a bounds check after optimization.

Clippy's len_zero warning recommends is_empty() over len() == 0. In this particular algorithm you can remove the emptiness check entirely: out.contains(&value) is already false when out is empty. Push the value only when it is absent from out.

Borrowing and consuming a vector are different

The element type being Copy does not make the vector itself Copy. This complete example shows three forms of iteration:

fn main() {
    let mut values = vec![1, 2, 3];
    for value in &values {
        assert!(*value > 0); // value is &i32; values stays usable.
    }
    for value in &mut values {
        *value += 10; // value is &mut i32.
    }
    assert_eq!(values, [11, 12, 13]);
    let mut sum = 0;
    for value in values {
        sum += value; // value is i32; this loop consumes the vector.
    }
    assert_eq!(sum, 36);
}

After the final loop, using values again would produce a moved-value error. The visualization illustrates the same ownership rule with an assignment.

Loading visualization…

Why this algorithm preserves order

Scan left to right and append a value only on its first occurrence. The output doubles as the record of values already seen. Later duplicates leave it alone. Vec::dedup() alone is different: it removes only consecutive duplicates.

Searching the output with contains takes linear time in its current length, giving this approach O(n²) worst-case time and O(k) output space for k distinct values. It is a simple approach for this exercise's small inputs. For larger inputs, a separate HashSet can track membership while a vector retains the order, giving expected O(n) total time with extra storage. Returning the set's iteration order would not meet this task's contract.