Problem overview
The function remove_duplicates receives a vector of integers, nums, and returns a vector in which each distinct value appears exactly once. The order of the result is free. The statement asks for O(n log n) time and O(1) extra space, with no second vector for the unique values.
For nums = [2, 3, 1, 1, 4, 3, -2, 1] the distinct values are -2, 1, 2, 3 and 4. This solution returns them in ascending order, [-2, 1, 2, 3, 4], which is one of the allowed orders.
Approach: sort, then overwrite from the left
An earlier lesson on this problem collects unique values into a new vector and checks membership by scanning it, which takes O(n^2) time and O(n) extra space. Here sorting does both jobs better.
After sorting, equal values are neighbours, so a value is new exactly when it differs from the last value kept. The solution walks through the sorted vector and keeps a write index, p, which marks the position of the last unique value stored at the front of the vector. When the scan finds a value different from nums[p], it moves p one step forward and copies the value there. Duplicates are skipped. The front part nums[0..=p] then always holds the unique values found so far, in order, with no repeats.
Writing to the front never destroys a value that is still needed, because the write position p is never ahead of the read position i. At the end, everything after index p is leftover, and the vector is cut to p + 1 elements.
Here is the vector as the loop runs on the sorted example [-2, 1, 1, 1, 2, 3, 3, 4]:
i=1 p=1 [-2, 1, 1, 1, 2, 3, 3, 4]
i=2 p=1 [-2, 1, 1, 1, 2, 3, 3, 4]
i=3 p=1 [-2, 1, 1, 1, 2, 3, 3, 4]
i=4 p=2 [-2, 1, 2, 1, 2, 3, 3, 4]
i=5 p=3 [-2, 1, 2, 3, 2, 3, 3, 4]
i=6 p=3 [-2, 1, 2, 3, 2, 3, 3, 4]
i=7 p=4 [-2, 1, 2, 3, 4, 3, 3, 4]
After the loop, p is 4, and cutting to 5 elements leaves [-2, 1, 2, 3, 4].
Step by step
- Make the vector mutable and sort it. Shadow the parameter with
let mut nums = nums;, which moves the vector into a new mutable binding without copying it. Then callsort(), so equal values end up together. - Guard the empty vector. The first element is always kept, so the code after the guard assumes at least one element. For an empty vector, return it as it is.
- Create the write index. Declare
pas a mutableusizestarting at 0: the position of the last distinct value kept. The first element is already in place. - Loop over the rest with
iin1..nums.len(). - Compare with the last kept value. If
nums[i]differs fromnums[p], it is met for the first time. Comparing withnums[i - 1]would give the same answer here, because in a sorted vector both hold the latest value. - Store it. Increment
pfirst withp += 1, then copynums[i]intonums[p]. - Cut the leftovers. After the loop, call
truncate(p + 1)to keep only the firstp + 1elements, and returnnums.
The solution in Rust
impl Solution {
fn remove_duplicates(nums: Vec<i32>) -> Vec<i32> {
let mut nums = nums;
nums.sort();
if nums.is_empty() {
return nums;
}
let mut p = 0usize;
for i in 1..nums.len() {
if nums[i] != nums[p] {
p += 1;
nums[p] = nums[i];
}
}
nums.truncate(p + 1);
nums
}
}
The Rust details worth a closer look:
- Shadowing.
let mut nums = nums;declares a new variable with the same name. The old, immutable binding is moved into it. This is a common way to make a parameter mutable without changing the function signature, and it copies nothing. - Copying an element.
nums[p] = nums[i];reads one element and writes another in the same vector. This is allowed becausei32isCopy: the right side is evaluated to a plain number before the write begins, so no two borrows overlap. truncate.nums.truncate(n)keeps the firstnelements and drops the rest. Ifnis larger than the length, it does nothing. It changes the length but keeps the allocation, so no memory moves.- The empty check. For an empty vector,
1..nums.len()is the empty range1..0andtruncate(1)does nothing, so the function would return[]even without the guard. The guard makes the assumption explicit: from there on,nums[0]exists. - The type of
p. Vector indices areusize, and writing0usizefixes the type ofpexplicitly.
Complexity
Time: O(n log n). Sorting takes O(n log n). The loop does one comparison and at most one copy per element, which is O(n). truncate on integers is constant time.
Space. The compaction writes into the same vector and keeps only a few integers. Rust's stable sort() may use a temporary buffer, so strictly its extra space can be O(n). Calling sort_unstable() instead sorts without a buffer, and gives the same result for integers.
Edge cases and common mistakes
- Empty vector. The guard returns it unchanged,
[]. - One element. The loop doesn't run,
pstays 0, andtruncate(1)leaves[9]as it was. - All values equal. For
[5, 5, 5]the condition never fires,pstays 0, and the result is[5]. - Forgetting the
+ 1.pis an index, not a count.truncate(p)would drop the last unique value. - Copying before moving
p. Writingnums[p] = nums[i]beforep += 1overwrites the last unique value instead of the slot after it. - Not sorting first. Without sorting, the same value can appear in separate places, and comparing with the last kept value no longer finds every duplicate. For
[1, 2, 1]the result would be[1, 2, 1].
Testing
nums = [2, 3, 1, 1, 4, 3, -2, 1]gives[-2, 1, 2, 3, 4].nums = []gives[].nums = [9]gives[9].nums = [5, 5, 5]gives[5].
