Problem overview
The method singleNumber receives a non-empty array nums in which every value appears exactly twice, except one value that appears once. It returns that single value. The statement asks for O(n log n) time and O(1) extra space.
For [4,1,2,1,2] the answer is 4, because 1 and 2 each appear twice. For [2,2,1] the answer is 1, and for [1] it is 1.
Approach: sort, then find the value with no equal neighbor
Counting each value with a dictionary would work in linear time but uses extra memory proportional to the input. Comparing every pair of elements needs no memory but takes O(n²) time. Sorting sits between them.
After sorting in ascending order, equal values sit next to each other, so the duplicates form pairs: [4,1,2,1,2] becomes [1,1,2,2,4]. Every value that has a pair has an equal neighbor on one side. The single value is the only one whose neighbors on both sides are different, or missing because it sits at an end of the array. Scanning the sorted array for that value finds the answer.
The lesson's tests expect this approach, with sorting costing O(n log n). Later lessons on the same problem show faster solutions.
Step by step
- Write a helper
isSingleNumber(_ nums: [Int], _ index: Int) -> BoolabovesingleNumber. It says whether the element atindexhas no equal neighbor. - Check the left neighbor. If
index > 0andnums[index - 1]equalsnums[index], the element has a pair, so returnfalse. Theindex > 0test comes first so the code never reads before the start. - Check the right neighbor. If
index < nums.count - 1andnums[index]equalsnums[index + 1], returnfalsein the same way. - Return
trueat the end of the helper: neither neighbor matched. - Handle the one-element array in
singleNumber: ifnums.count == 1, returnnums[0]. The helper would handle it, but this skips the sort and the loop. - Sort a copy. Declare a constant
sortedholdingnums.sorted(). Function parameters are constants in Swift, sonumscan't be sorted in place. - Scan the indices of
sortedwith a loop variableiover0..<sorted.count. - Test each one. If
isSingleNumber(sorted, i)is true, returnsorted[i]. - Return -1 after the loop. The input always contains a single number, so this line is only a fallback that the compiler requires, because a function with a result must return on every path.
A walk through the scan
Take [4,1,2,1,2]. Sorting gives [1,1,2,2,4], and the loop asks the helper about each index in turn:
- Index 0 holds 1. There is no left neighbor, but the right neighbor is also 1, so the helper returns
false. - Index 1 holds 1. The left neighbor is 1, so it returns
falseat the first check. - Indexes 2 and 3 hold 2. Each has an equal neighbor, on the right and on the left respectively, so both return
false. - Index 4 holds 4. Its left neighbor is 2, and it has no right neighbor, because
4 < nums.count - 1is false. Neither check returns, so the helper returnstrue, and the method returnssorted[4], which is 4.
The scan always finds the answer, because every other value occupies two adjacent slots and gets a false from one side or the other.
The solution in Swift
class Solution {
private func isSingleNumber(_ nums: [Int], _ index: Int) -> Bool {
if index > 0 && nums[index - 1] == nums[index] {
return false
}
if index < nums.count - 1 && nums[index] == nums[index + 1] {
return false
}
return true
}
func singleNumber(_ nums: [Int]) -> Int {
if nums.count == 1 {
return nums[0]
}
let sorted = nums.sorted()
for i in 0..<sorted.count {
if isSingleNumber(sorted, i) {
return sorted[i]
}
}
return -1
}
}
The Swift details worth knowing:
sorted()versussort().sorted()returns a new array and leaves the original alone.sort()sorts avararray in place and returns nothing. Withvar arr = [3,1,2], the callarr.sorted()gives[1, 2, 3]whilearrstays[3, 1, 2]; afterarr.sort(),arris[1, 2, 3].- Short-circuit
&&. Inindex > 0 && nums[index - 1] == nums[index], the right side runs only when the left side is true. That is what makes the guard work: at index 0 the expressionnums[-1]is never evaluated. Reading it would crash the program, because Swift checks array bounds at run time. private. The helper is visible only inside the class. Calling it fromsingleNumberworks without any prefix.- The sorted copy and extra space.
nums.sorted()allocates a new array, so strictly speaking this usesO(n)memory, not theO(1)the statement mentions. - Argument labels. The underscores in
_ numsand_ indexremove the argument labels, so the call isisSingleNumber(sorted, i).
Complexity
Time: O(n log n). Sorting costs O(n log n). The scan then checks each index in constant time, which adds O(n).
Space: O(n) as written. The sorted copy holds n integers. The statement's O(1) assumes sorting the input itself in place; the algorithm is otherwise the same.
Edge cases and common mistakes
- One element.
[1]returns 1 right away. - Single value at either end.
[1,2,2]sorts to[1,2,2]and returns 1, using only the right neighbor.[1,1,2]returns 2, using only the left neighbor. - Negative numbers.
[-3,5,-3]sorts to[-3,-3,5]and returns 5. Sorting handles negatives like any other value. - Checking only one side. If the helper looked only to the right, the second element of every pair would look single. Both neighbors must be checked.
- Removing the bounds guards. At index 0,
nums[index - 1]reads index -1, and at the last index,nums[index + 1]reads past the end. Either crashes. - Looking at the unsorted array. In
[4,1,2,1,2]the equal values are not adjacent, so the neighbor test says nothing until the array is sorted.
Testing
[2,2,1]gives1.[4,1,2,1,2]gives4.[1]gives1.[-3,5,-3]gives5.[1,1,2]gives2.
