Problem overview
The method removeDuplicates(vector<int>& nums) takes an unsorted vector and must leave each distinct value in it exactly once. The result may be in any order. The statement asks for the work to be done in place, with O(1) extra memory and O(n log n) time.
For [2, 3, 1, 1, 4, 3, -2, 1] the distinct values are 2, 3, 1, 4 and -2, so any ordering of those five is accepted. The solution below returns them sorted: [-2, 1, 2, 3, 4].
Approach: sort, then keep the first copy of each value
With unsorted data, finding out whether a value was seen before means remembering all earlier values, which costs extra memory (a hash set), or scanning everything before it, which makes the whole job O(n²). Sorting removes the problem. After sorting, equal values sit next to each other, so a value is new exactly when it differs from its left neighbour.
The in-place part uses two positions. The reading index i walks over the sorted vector. The writing index p marks where the last distinct value was stored. Each time nums[i] differs from nums[i - 1], the loop moves p one step right and copies nums[i] there. The distinct values gather at the front of the vector, in order, and p + 1 is how many there are. Since p never gets ahead of i, nothing is overwritten before it has been read.
Take the sorted vector [-2, 1, 1, 1, 2, 3, 3, 4], with p = 0 at the start:
i = 1: 1 differs from -2, sopbecomes 1 andnums[1] = 1.i = 2andi = 3: the value is 1 again, so nothing happens.i = 4: 2 is new, sop = 2andnums[2] = 2.i = 5: 3 is new, sop = 3andnums[3] = 3.i = 6: 3 repeats; nothing happens.i = 7: 4 is new, sop = 4andnums[4] = 4.
The front of the vector reads -2, 1, 2, 3, 4. With p = 4, the vector is cut to p + 1 = 5 elements.
Step by step
- Handle an empty vector first. The rest of the function treats
nums[0]as a kept value. On an empty vector, the finalresize(p + 1)would grow it to one element and return{0}, so return early instead. - Sort with
sort(nums.begin(), nums.end()). It takes the range as a pair of iterators. - Set
p = 0. The first element is always kept, so it is already in place. - Loop from
i = 1to the end of the vector. - Compare with the previous element:
nums[i] != nums[i - 1]means a new value. - Store it with
nums[++p] = nums[i]. The pre-increment raisespfirst and then uses the new value as the index. - Shrink and return.
nums.resize(p + 1)drops the leftover tail, and the function returnsnums.
The solution in C++
class Solution {
public:
vector<int> removeDuplicates(vector<int>& nums) {
if (nums.empty()) {
return nums;
}
sort(nums.begin(), nums.end());
int p = 0;
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] != nums[i - 1]) {
nums[++p] = nums[i];
}
}
nums.resize(p + 1);
return nums;
}
};
Some C++ details:
- The parameter is a reference.
vector<int>& numsmeans the caller's vector is sorted and shrunk in place. The function also returnsnumsby value, which makes a copy for the return. That copy is a separate vector the size of the result, so strictly the extra memory is notO(1)for the returned value. The work onnumsitself uses onlypandi. resizeto a smaller size erases elements from the end. It is the C++ way to cut a vector, since there is no way to "return a prefix" of an array.nums.size()is unsigned. Comparing it with theintiis fine for non-negative values and the loop is safe, though compilers may warn about the mixed types.- The standard library has this idiom.
nums.erase(unique(nums.begin(), nums.end()), nums.end())aftersortdoes the same job:uniquemoves the distinct values to the front and returns an iterator to the end of them, anderasecuts the tail. It is worth knowing, and it is what to write outside this lesson.
Complexity
Time: O(n log n). std::sort takes O(n log n) comparisons. The compaction loop is a single O(n) pass, and the final resize only removes elements. The sort dominates.
Space: O(1) extra beyond the input vector, apart from the small stack space the sort uses internally. Everything else is two integers.
If the order does not matter and extra memory is allowed, a set or unordered_set removes duplicates without sorting. An unordered_set takes O(n) time on average and O(n) space, and it can keep the original order if you only append values that insert reports as new. This lesson trades that memory for a sort.
Edge cases and common mistakes
- Empty vector. The early return handles it. Without it,
resize(1)would produce{0}, a value that was never in the input. - One element. The loop does not run,
pstays 0, and the vector is unchanged. - All values equal.
[7, 7, 7]becomes[7]. - Forgetting to sort. Comparing with the previous element only catches adjacent repeats. On
[1, 2, 1]without sorting, nothing would be removed. - Forgetting
resize. The distinct values are at the front, but stale values remain after positionp. The vector would still have its old length.
Testing
[2, 3, 1, 1, 4, 3, -2, 1]gives[-2, 1, 2, 3, 4].[]gives[].[5]gives[5].[7, 7, 7]gives[7].
