Problem overview
The method removeDuplicates receives an array of integers and returns an array in which every distinct value appears once. The order of the result doesn't matter. For [2, 3, 1, 1, 4, 3, -2, 1] the distinct values are 2, 3, 1, 4 and -2, so any arrangement of those five numbers is correct.
Approach: sort, then compact
A simple idea is to keep a second array and add each value to it only if it hasn't been added yet. Checking "has it been added?" is slow on an unsorted array. Sorting fixes that: equal values become neighbours, so a value is new exactly when it differs from the element before it.
That suggests a cleaner technique that needs no second array. Walk through the sorted array with an index i and keep a second index p that marks where the last distinct value was placed. Every time arr(i) is new, move p one step right and copy arr(i) there. The distinct values pile up at the front of the array, in ascending order, and the first p + 1 elements are the answer. The sort costs O(n log n) and the pass costs O(n).
Here is the idea on [1, 1, 2, 3, 3], which is already sorted. Initially p = 0. At i = 2 the value 2 differs from 1, so p becomes 1 and arr(1) is set to 2. At i = 3, the value 3 differs from 2, so p becomes 2 and arr(2) is set to 3. At i = 4 the value 3 equals its predecessor and nothing happens. The array is now [1, 2, 3, 3, 3] and the first p + 1 = 3 elements, [1, 2, 3], are the result.
Step by step
- Make a sorted copy.
val arr = nums.sortedputs equal values next to each other. Everything after this works onarr. - Declare
pas avarset to 0. It is the index where the last distinct value sits. The first element is always kept, so index 0 is already correct. - Loop over the rest.
1 until arr.lengthstarts at the second element, so each element has a predecessor to compare with. - Test for a new value. If
arr(i)differs fromarr(i - 1), this value appears for the first time. - Record it. Increase
pby 1, then copyarr(i)toarr(p), right after the distinct values found so far. - Return the prefix.
arr.slice(0, p + 1)holds thep + 1distinct values. Being the last expression, it is the method's result.
The solution in Scala
class Solution {
def removeDuplicates(nums: Array[Int]): Array[Int] = {
val arr = nums.sorted
var p = 0
for (i <- 1 until arr.length) {
if (arr(i) != arr(i - 1)) {
p += 1
arr(p) = arr(i)
}
}
arr.slice(0, p + 1)
}
}
The Scala details worth knowing:
valwith a mutable array.arris aval, so the name can't be reassigned, but the array's elements can still be changed. The linearr(p) = arr(i)is legal for that reason.- Assignment into an array uses parentheses too.
arr(p) = valueis how Scala writes what other languages write asarr[p] = value. - Writing while reading is safe. The write position
pis never ahead of the read positioni, so a write never destroys a value the loop still has to look at. In particulararr(i - 1)is only ever overwritten with its own value. - No
++in Scala. Usep += 1. slice(0, p + 1)excludes its end. That is why it isp + 1, notp. The result is a new array, so the method returns an array of the right length, not the old one with junk at the end.- The input is not modified.
sortedreturns a copy, so the caller'snumskeeps its order.
Complexity
Time: O(n log n). Sorting is O(n log n), the loop is a single O(n) pass, and the final slice copies at most n elements.
Space: O(n) in this version, because sorted makes a copy of the input and slice makes another. The compacting idea itself needs only two indices. Sorting nums in place with scala.util.Sorting.quickSort would reduce the extra memory to the returned slice, at the price of changing the caller's array.
Edge cases and common mistakes
- Empty array. The loop doesn't run, and
slice(0, 1)of an empty array is an empty array. The result is empty without an exception. - One element.
[7]returns[7]. - All values equal.
[5, 5, 5]never movesp, and the result is[5]. - Returning the whole array. After compacting, the tail still holds leftover values. Without the slice,
[1, 1, 2, 3, 3]would return[1, 2, 3, 3, 3]. - Slicing to
p. This drops the last distinct value. - Skipping the sort. The neighbour comparison only finds duplicates that are adjacent. On the unsorted
[1, 2, 1]it would keep both 1s. - Negative numbers. They sort before zero and need no special handling. For the example input the result is
[-2, 1, 2, 3, 4].
Testing
[2, 3, 1, 1, 4, 3, -2, 1]gives[-2, 1, 2, 3, 4], which is the five distinct values in ascending order.[7]gives[7].[]gives[].[5, 5, 5]gives[5].
