Problem overview
The method singleNumber receives a non-empty vector of integers, nums, in which every value appears exactly twice except one value, which appears once. It returns that single value. For [2, 2, 1] the answer is 1. For [4, 1, 2, 1, 2] it is 4, and for [1] it is 1.
The statement asks for O(n log n) time and O(1) extra space. The lesson says that faster solutions exist and come later, so the goal here is a clean approach built on sorting.
Approach: sort, then look for the number without a twin
Without any structure, finding the odd one out means comparing each value against all the others, which costs O(n²). Sorting removes that work. After sorting, equal values sit next to each other, so a value's twin can only be its left or right neighbour.
Take [4, 1, 2, 1, 2]. Sorted, it is [1, 1, 2, 2, 4]. The 1s are side by side, so are the 2s, and the 4 has a different value on its left and nothing on its right. The single number is the one element that matches neither neighbour.
That gives a test for position i: it holds the single number if
iis 0 ornums[i - 1]differs fromnums[i], andiis the last position ornums[i + 1]differs fromnums[i].
Sorting costs O(n log n), and the scan afterwards is O(n), so the sort is the expensive step.
Step by step
- Write a helper
bool isSingleNumber(vector<int>& nums, int index)that reports whether the value atindexhas no equal neighbour. - Check the left neighbour. The condition is
index > 0 && nums[index - 1] == nums[index]. Theindex > 0part comes first so the code never reads before the start of the vector. In&&, the right side runs only if the left side is true. - Return
falsein that case: an equal left neighbour means the value has its twin. - Check the right neighbour with
index < (int)nums.size() - 1 && nums[index] == nums[index + 1]. The cast matters, as explained below. - Return
falseagain for an equal right neighbour. - Return
trueat the end of the helper: neither neighbour matched. - Handle a one-element vector in
singleNumberwith an early check onnums.size() == 1. The helper would also give the right answer here, but the shortcut skips the sort and the loop. - Return
nums[0]in that case. - Sort the vector with
sort(nums.begin(), nums.end()), so twins become adjacent. - Loop over every index
i. - Ask the helper about each position:
if (isSingleNumber(nums, i)). - Return
nums[i]when it says yes. - Return -1 after the loop. The lesson guarantees a single number exists, so this line is a fallback that never runs on valid input.
The solution in C++
class Solution {
public:
bool isSingleNumber(vector<int>& nums, int index) {
if (index > 0 && nums[index - 1] == nums[index]) {
return false;
}
if (index < (int)nums.size() - 1 && nums[index] == nums[index + 1]) {
return false;
}
return true;
}
int singleNumber(vector<int>& nums) {
if (nums.size() == 1) {
return nums[0];
}
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
if (isSingleNumber(nums, i)) {
return nums[i];
}
}
return -1;
}
};
C++ details that matter here:
- Unsigned
size().nums.size()returns an unsigned type. Computingnums.size() - 1on an empty vector wraps around to a huge number instead of -1. Casting first,(int)nums.size() - 1, keeps the arithmetic signed, so the comparison with the signedindexbehaves as it reads. Without the cast,indexwould also be converted to unsigned for the comparison. A helper can't be sure its caller never passes an empty vector, so the cast is a good habit. - Out-of-range reads.
nums[-1]ornums[nums.size()]on avectoris undefined behaviour. The compiler does not stop you, and the program may seem to work or may crash. The bounds tests that come before the comparisons are what keep the reads valid. - Short-circuit evaluation.
a && bevaluatesbonly whenais true. That is whyindex > 0 && nums[index - 1] == nums[index]is safe at index 0. - Sorting in place.
sortcomes from<algorithm>, takes a pair of iterators, and reorders the caller's vector, becausenumsis passed by reference. If the caller needs the original order, sort a copy instead. The statement'sO(1)extra space refers to the algorithm's own extra memory, and in-place sorting keeps to it. - Signed and unsigned loop bound. In
i < nums.size(), theintiis converted to unsigned for the comparison. That's fine here sinceiis never negative, though some compilers warn about it.
Complexity
Time: O(n log n). Sorting dominates. The scan calls the helper for each index, and each call does constant work.
Space: O(1) extra, apart from the small working memory of the sort. No additional array or map is built.
Edge cases and common mistakes
- One element.
[1]returns 1, through the early return. - The single number at either end. In
[4, 1, 1], sorted to[1, 1, 4], the 4 is last, and its right-neighbour check is skipped because there is no right neighbour. In[1, 2, 2]the 1 is first, and the left check is skipped. - Negative values. They sort like any other integer. For
[-3, 5, -3]the answer is 5. - Checking only one side. Looking only at the left neighbour would report the first element of every pair, since the first of a pair has a different left neighbour. Both sides are required.
- Skipping the sort. On an unsorted vector, neighbours say nothing about twins, so the test gives wrong answers.
- Reading past the end. Removing the bounds tests is undefined behaviour in C++, even if it seems to work on your machine.
Testing
[2, 2, 1]gives 1.[4, 1, 2, 1, 2]gives 4.[1]gives 1.[-3, 5, -3]gives 5.