Problem overview
The function singleNumber receives a non-empty slice of integers, nums, in which every value appears exactly 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. For [1] it is 1, since a lone element is the single one by definition. The values lie between -30,000 and 30,000, and the slice has up to 30,000 elements.
The statement allows O(n log n) time and O(1) extra space, which is the budget that sorting fits in.
Approach: sort, then look for the element without a twin
The obvious ways to count occurrences cost memory: a map from value to count uses O(n) extra space. Sorting avoids that. After sorting in ascending order, equal values sit next to each other, so every value that appears twice forms a pair of adjacent equal elements.
The single value is the only one with no equal neighbour. For [4, 1, 2, 1, 2], the sorted slice is [1, 1, 2, 2, 4]. The 1s are a pair, the 2s are a pair, and 4 has a different value on its left and nothing on its right. That's the answer.
So the test for position index in a sorted slice has two halves. The left neighbour must not equal it (or there is no left neighbour, at index 0), and the right neighbour must not equal it (or there is none, at the last index). The two checks need guards so that they never read outside the slice.
Faster approaches exist, but this one needs nothing beyond the standard library's sort.
Step by step
- Write the helper
isSingleNumber(nums, index)first, returning abool. It answers one question: does the value atindexlack an equal neighbour? - Check the left neighbour. If
index > 0andnums[index-1] == nums[index], the value has a twin on its left, so returnfalse. The conditionindex > 0comes first, sonums[-1]is never evaluated, because&&stops as soon as its left side is false. - Check the right neighbour the same way: if
index < len(nums)-1andnums[index] == nums[index+1], returnfalse. - Return
trueat the end. If neither side matched, the value has no twin. - In
singleNumber, handle a one-element slice early: iflen(nums) == 1, returnnums[0]. The helper would also handle that case, but the early return skips the sort and the loop. - Sort
numsin place withsort.Ints. This is the step that sets the running time. - Loop over every index
iand call the helper. The first index for which it returnstrueholds the answer, so returnnums[i]. - Return -1 after the loop. Valid input always has a single number, so this line is unreachable in practice. Go still requires the function to end with a return statement.
The solution in Go
func isSingleNumber(nums []int, index int) bool {
if index > 0 && nums[index-1] == nums[index] {
return false
}
if index < len(nums)-1 && nums[index] == nums[index+1] {
return false
}
return true
}
func singleNumber(nums []int) int {
if len(nums) == 1 {
return nums[0]
}
sort.Ints(nums)
for i := 0; i < len(nums); i++ {
if isSingleNumber(nums, i) {
return nums[i]
}
}
return -1
}
The Go details worth knowing:
- The
sortimport.sort.Intslives in the standardsortpackage, which the file must import:import "sort". Without it the compiler reports "undefined: sort". If you work outside the lesson's editor, add the import yourself. - Sorting changes the caller's slice. A slice is a small header that points to an array. Passing it to a function copies the header and not the elements, so
sort.Ints(nums)reorders the caller's data. AftersingleNumber([]int{4, 1, 2, 1, 2})runs on a variablea,aholds[1 1 2 2 4]. Copy the slice first if you still need the original order. - Short-circuit evaluation. In
index > 0 && nums[index-1] == nums[index]the second operand is evaluated only when the first is true. That's what makes the guard work. Readingnums[-1]panics at run time with "index out of range". - Booleans can be returned directly. The helper returns
falseortruefrom insideifblocks, and the callisSingleNumber(nums, i)is itself a validifcondition, so no comparison withtrueis needed. - Functions can be declared in any order, but here the helper sits above
singleNumberbecause that's the order the lesson builds them.
Complexity
Time: O(n log n). Sorting costs O(n log n). The loop then makes at most one pass over the slice, calling a helper that does a constant amount of work, so it adds O(n), which the sort dominates.
Space: O(1) extra, apart from the sort's own bookkeeping. The slice is sorted in place and no new slice or map is created. The sort routine uses a small amount of stack for its recursion.
Edge cases and common mistakes
- One element.
[1]returns 1 through the early check. - The single value is the smallest or the largest. In
[1, 2, 2]the answer 1 is at index 0, where there is no left neighbour. In[3, 3, 9]the answer 9 is at the last index, where there is no right neighbour. The guardsindex > 0andindex < len(nums)-1are what make both cases work. - Negative numbers.
[-3, 5, -3]sorts to[-3, -3, 5]and returns 5. Sorting treats negatives like any other value. - Checking only one side. If the helper looked only at the left neighbour, the first element of every pair would pass the test. In
[1, 1, 2]it would return the first 1 instead of 2. - Skipping the guards. Reading
nums[index-1]at index 0 ornums[index+1]at the last index panics. - Forgetting to sort. On the unsorted
[2, 1, 2]the first 2 has no left neighbour and its right neighbour is 1, so the helper would accept it and return 2, although 1 is the single value.
Testing
[2, 2, 1]gives1.[4, 1, 2, 1, 2]gives4.[1]gives1.[-3, 5, -3]gives5.[7, 3, 3, 8, 8]gives7.[3, 3, 9]gives9.