Problem overview
The method singleNumber receives a non-empty int[] named nums in which every value appears exactly twice, except for one value that appears once. It returns that single value. In [4, 1, 2, 1, 2] the 1s and the 2s come in pairs, so the answer is 4. In [2, 2, 1] the answer is 1, and in [1] it is 1.
The note in the statement sets the target for this lesson: O(n log n) time and O(1) extra space. It also says that faster solutions exist and will come later.
Approach: sort, then find the value without a twin
The obvious way to count how often each value appears is a hash map, but that uses O(n) extra space, which the note rules out. Comparing every value with every other one avoids the space but takes O(n²) time.
Sorting fixes both problems. After sorting, equal values sit next to each other, so each pair becomes two neighbors. The single value is the only element that equals neither its left neighbor nor its right neighbor. Sorting in place costs O(n log n) time and needs no second array, and the final scan is one pass.
Take [4, 1, 2, 1, 2]. Sorted, it is [1, 1, 2, 2, 4]. Index 0 equals its right neighbor, index 1 equals its left neighbor, and so do indexes 2 and 3 with their partners. Index 4, holding 4, has a different left neighbor and no right neighbor, so 4 is the answer.
Step by step
- Write a helper
isSingleNumber(nums, index)that says whether the value atindexhas no twin next to it. It is private because onlysingleNumberuses it. - Check the left neighbor. If
index > 0andnums[index - 1] == nums[index], the value has a partner, so returnfalse. Theindex > 0test comes first so the code never readsnums[-1]. - Check the right neighbor the same way:
index < nums.length - 1andnums[index] == nums[index + 1]mean a partner, so returnfalse. - Return
truewhen neither neighbor matched. - Handle a single element in
singleNumber: ifnums.length == 1, returnnums[0]. The helper would also work for this case, but this check skips the sort and the loop. - Sort with
Arrays.sort(nums), so duplicates become adjacent. - Scan every index with a
forloop and returnnums[i]as soon asisSingleNumber(nums, i)is true. - Return
-1after the loop. The input guarantees a single number, so this line is only there because Java requires every path through the method to return something.
The solution in Java
class Solution {
private boolean isSingleNumber(int[] nums, int index) {
if (index > 0 && nums[index - 1] == nums[index]) {
return false;
}
if (index < nums.length - 1 && nums[index] == nums[index + 1]) {
return false;
}
return true;
}
public int singleNumber(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
if (isSingleNumber(nums, i)) {
return nums[i];
}
}
return -1;
}
}
Java details worth a closer look:
- Short-circuit
&&. Inindex > 0 && nums[index - 1] == nums[index], the right side runs only when the left side is true. That is what keepsnums[index - 1]from being evaluated with a negative index. Java throws anArrayIndexOutOfBoundsExceptionfor any index outside the array, instead of reading nearby memory. Arrays.sortchanges the array. It sorts in place, so the caller'snumscomes back in ascending order. For primitive arrays likeint[], it uses a dual-pivot quicksort. If the caller's order matters, sort a copy withnums.clone(), which costsO(n)extra space.- A boolean method as a condition.
if (isSingleNumber(nums, i))needs no== true, since the method already returns aboolean. - "O(1) extra space." The sort works within the array and creates no second copy. Like most sorting routines, it still uses a small amount of stack space for its own bookkeeping, so the note's "constant extra space" means that nothing grows with a copy of the data.
Complexity
Time: O(n log n). The sort dominates. The scan afterwards looks at each index once, and the helper does a constant number of comparisons, so it adds O(n).
Space: O(1) extra, apart from the small working space of the sort. No extra array or map is created.
A later idea does better: combine all the values with the XOR operator, result ^= num. Equal values cancel each other out, so only the single value is left. That runs in O(n) time with O(1) space and no sorting, but the sorting approach is the one this lesson practises.
Edge cases and common mistakes
- One element.
[1]returns 1 straight away. - The single value at either end. In
[1, 2, 2]the 1 is at index 0, with no left neighbor. In[2, 2, 1]the 1 is at the last index, with no right neighbor. The index guards in the helper handle both. - The single value in the middle. In
[1, 1, 2, 3, 3]the 2 differs from both neighbors. - Negative numbers.
[-3, 5, -3]sorts to[-3, -3, 5]and returns 5. The method only compares for equality, so signs don't matter. - Only checking one neighbor. Testing just the left neighbor would wrongly accept the first element of every pair, since its left neighbor is a different value.
- Reading past the ends. Without the
index > 0andindex < nums.length - 1guards, the first or last element would throw anArrayIndexOutOfBoundsException. - Expecting the original order. After the call,
numsis sorted.
Testing
[2, 2, 1]gives1.[4, 1, 2, 1, 2]gives4.[1]gives1.[1, 1, 2, 3, 3]gives2.[-3, 5, -3]gives5.