Problem overview
The method singleNumber receives a non-empty Array[Int] called nums in which every value appears exactly twice, except one value that appears once. It returns that single value.
For [4,1,2,1,2] the answer is 4: the 1s and the 2s form pairs, and 4 stands alone. For [2,2,1] it is 1, and for [1] it is 1. The statement sets a budget of O(n log n) time and O(1) extra space, which points at sorting.
Approach: sort, then look for the value without a twin
In an unsorted array, the two copies of a value can be anywhere, so finding the unpaired one means comparing a lot of positions. After sorting, equal values sit next to each other. In [4,1,2,1,2] the sorted array is [1,1,2,2,4], with every pair adjacent. The single value is the only one that differs from both of its neighbors.
For a position i in the sorted array, the value is the single one when both of these hold:
- it is at index 0, or
nums(i - 1)is different fromnums(i); - it is at the last index, or
nums(i + 1)is different fromnums(i).
The lesson writes this test as a helper, isSingleNumber, which answers false as soon as it finds an equal neighbor. A loop over all positions returns the first value for which it answers true.
Step by step
- Declare the helper above
singleNumber:private def isSingleNumber(nums: Array[Int], index: Int): Boolean.privatehides it from code outside the class. - Check the left neighbor. If
index > 0 && nums(index - 1) == nums(index), returnfalse: this value has a twin on its left. The testindex > 0comes first so thatnums(-1)is never read. - Check the right neighbor. If
index < nums.length - 1 && nums(index) == nums(index + 1), returnfalse. The first test prevents reading past the end. - Return
trueat the end of the helper: neither neighbor matched. - Handle a single element. In
singleNumber, ifnums.length == 1, returnnums(0). The helper would also give the right result, but this skips the sort and the loop. - Sort in place with
java.util.Arrays.sort(nums), so the pairs become adjacent. - Loop over every index with
for (i <- 0 until nums.length). The range excludes its end, soistops at the last index. - Test and return. If
isSingleNumber(nums, i)is true, returnnums(i). - Add a fallback. After the loop,
return -1. The input always contains a single value, so this line is never reached for valid input. It is there because the method must return anInton every path.
The solution in Scala
class Solution {
private def isSingleNumber(nums: Array[Int], index: Int): 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
}
def singleNumber(nums: Array[Int]): Int = {
if (nums.length == 1) {
return nums(0)
}
java.util.Arrays.sort(nums)
for (i <- 0 until nums.length) {
if (isSingleNumber(nums, i)) {
return nums(i)
}
}
return -1
}
}
Scala details worth a closer look:
- Calling a Java method. Scala runs on the JVM and can call Java's standard library directly.
java.util.Arrays.sortaccepts theArray[Int]because it is a Javaint[]at run time. - In place versus a copy.
java.util.Arrays.sort(nums)reorders the caller's array. AftersingleNumber(a)witha = Array(4,1,2,1,2),aholds1,1,2,2,4. Scala's ownnums.sortedleaves the original untouched and returns a new array, so it would useO(n)extra space. - Short-circuit
&&. Inindex > 0 && nums(index - 1) == nums(index), the right side is evaluated only when the left side is true. That is what makes the guard work. Readingnums(-1)directly throwsArrayIndexOutOfBoundsException. returnin Scala. Scala methods usually give their last expression as the value, andreturnis rare. Here the lesson uses it because the methods must stop in the middle. Thereturn nums(i)inside theforbody is a non-local return, which the Scala 3.5 compiler reports as a warning. The code still runs and returns the right value.- Return type. The type
Intafter the parameter list is declared, and a method that usesreturnmust declare its result type.
Complexity
Time: O(n log n). Sorting takes O(n log n). The loop checks each position once, and each check looks at two neighbors, so it costs O(n). The sort dominates.
Space: O(1) extra. The array is sorted in place and no other array is created. The sort itself uses a small amount of stack space. This is the cost of changing the input: if you need the original order afterwards, sort a copy.
The statement mentions faster solutions. A single pass with the bitwise XOR operator, nums.reduce(_ ^ _), runs in O(n) time, because a value XORed with itself is 0 and 0 XOR a value is that value, so the pairs cancel. For [4,1,2,1,2] it also gives 4.
Edge cases and common mistakes
- One element.
[1]returns 1 from the early check. - The single value at either end. In
[1,1,2,2,4]the answer is the last element, and in[3,7,7,9,9]it is the first. The index guards make both ends work. - Negative numbers.
[-3,5,-3]returns 5. Sorting orders negatives before positives, and the neighbor test needs no change. - Checking only one neighbor. A test that looks only to the right returns 1 for
[1,1,2], because the second 1 has no equal neighbor on its right. Both sides are needed. - Missing guards. Without
index > 0orindex < nums.length - 1, the first or last position reads outside the array and throws. - The input has changed. The caller's array is sorted afterwards. This surprises code that reuses the array.
- Invalid input. If every value were paired, as in
[1,1,2,2], the loop finds nothing and the method returns the fallback -1.
Testing
[2,2,1]gives1.[4,1,2,1,2]gives4.[1]gives1.[-3,5,-3]gives5.[7,7,9,9,3]gives3.
