Problem overview
The method singleNumber receives a non-empty array nums of integers in which every value appears twice except one. It returns that one 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 allows O(n log n) time and O(1) extra space. It also says faster solutions exist and will come in later lessons, so this lesson solves the problem with sorting.
Approach: sort, then look for the value with no equal neighbour
The direct way to count how often a value appears is to compare it with every other element, which costs O(n²). A hash map of counts is faster, but it needs O(n) extra space. Sorting gets between the two: it costs O(n log n) and uses no extra array of our own.
After sorting, equal values sit next to each other. Take [2, 1, 3, 1, 3], which sorts to [1, 1, 2, 3, 3]. Every value that appears twice has an equal neighbour, either on its left or on its right. The single value has no equal neighbour on either side. So the scan checks each position: is the left neighbour different (or missing, at index 0), and is the right neighbour different (or missing, at the last index)? The first position where both answers are yes holds the answer.
Step by step
- Write the helper
isSingleNumber(nums, index), which returns aboolean: does the value atindexlack a matching neighbour? - Check the left side. If
index > 0andnums[index - 1] === nums[index], returnfalse. The value has a pair on its left. The guardindex > 0comes first, so the code never reads before the start of the array. - Check the right side. If
index < nums.length - 1andnums[index] === nums[index + 1], returnfalse. The guard avoids reading past the end. - Return
trueat the end of the helper: neither neighbour matched. - Handle a one-element array in
singleNumber: ifnums.length === 1, returnnums[0]. The helper would also work here, but the early return skips the sort and the loop. - Sort numerically. Call
nums.sort((a, b) => a - b). Pass the comparator so the order is numeric: without it,sortcompares the values as strings. - Scan every index
iwith aforloop. Whenthis.isSingleNumber(nums, i)is true, returnnums[i]. - Return -1 after the loop. The input always contains a single value, so this line is only a fallback.
The solution in TypeScript
class Solution {
isSingleNumber(nums: number[], index: number): boolean {
if (index > 0 && nums[index - 1] === nums[index]) {
return false;
}
if (index < nums.length - 1 && nums[index] === nums[index + 1]) {
return false;
}
return true;
}
singleNumber(nums: number[]): number {
if (nums.length === 1) {
return nums[0];
}
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length; i++) {
if (this.isSingleNumber(nums, i)) {
return nums[i];
}
}
return -1;
}
}
TypeScript details worth knowing:
- The default
sortcompares strings.[10, 9, 1, -2].sort()gives[-2, 1, 10, 9], because"10"comes before"9"as text. With the comparator(a, b) => a - bthe result is[-2, 1, 9, 10]. A negative result from the comparator putsafirst, a positive one putsbfirst. The compiler acceptsnums.sort()without a comparator, so nothing warns you about the mistake. sortchanges the array and returns it. It sortsnumsin place, and the return value is the same array, so the code needs no assignment. The caller's array is left sorted, which matters if the caller needs the original order. Copy first with[...nums].sort(...)in that case, at the price ofO(n)extra space.- Reading outside an array. TypeScript does not throw for it:
nums[-1]andnums[nums.length]are bothundefined. The guards in the helper still matter for clarity, and they keep the logic correct even if a value were missing. - Strict equality.
===compares numbers without converting types. Plain numbers are fine here: the constraint keeps values within about 30,000 in size, far from the point where doubles lose integer precision. - Types are erased.
number[],booleanandnumberare checked by the compiler and vanish from the JavaScript it produces.
Complexity
Time: O(n log n). Sorting dominates. The scan afterwards visits each index once and does constant work per index, so it adds O(n).
Space: O(1) extra, as far as this code goes. It creates no arrays of its own and changes the input in place. The engine's sort may use some temporary memory inside, which the statement's count does not include.
Edge cases and common mistakes
- One element.
[1]returns 1 immediately. - The single value is first or last. In
[1, 2, 2]the answer sits at index 0, so there is no left neighbour. In[1, 1, 2]it sits at the last index, so there is no right neighbour. The guards in the helper handle both. - Negative values.
[-1, -1, -5]returns-5. The numeric comparator sorts negatives correctly, but the default sort would not. - Forgetting the comparator. Equal numbers have equal text, so even the default string order puts copies next to each other, and this particular scan would still find the answer. The order itself is wrong, though (10 before 9), and any code that relies on numeric order would break. Always pass the comparator when sorting numbers.
- Checking only one neighbour. Looking only to the left would report the first element of every pair. Both sides must have no match.
- Invalid input. For
[1, 1, 2, 2], which has no single value, the loop finishes and the method returns the fallback-1.
Testing
[2, 2, 1]gives1.[4, 1, 2, 1, 2]gives4.[1]gives1.[-1, -1, -5]gives-5.[10, 9, 9]gives10.