Problem overview
The method secondLargest receives nums, an Array[Int], and returns the second largest value. Duplicates of the maximum don't count as a second place. For [2, 7, 11, 8, 11, 8, 3, 11] the maximum is 11, appearing three times, and the second largest value is 8. The array is guaranteed to contain at least two distinct values, and the follow-up asks for a single pass without sorting.
Approach: track the top two values
Sorting the array and reading the second element from the end is simple but costs O(n log n), and duplicates of the maximum would still need to be skipped. Instead, scan once while keeping two variables: maxVal, the largest value so far, and second, the largest value seen that is smaller than maxVal.
Each number falls into one of three cases:
- It beats
maxVal. The old maximum becomes the runner-up, and the number becomes the new maximum. - It is smaller than
maxValbut bigger thansecond. It becomes the new runner-up. - Anything else, which includes a repeat of the maximum, changes nothing.
Both variables start at Int.MinValue, the smallest possible Int. Any real element beats that, so the first iteration needs no special treatment.
Trace [2, 7, 11, 8, 11]:
- 2: beats the minimum value, so
secondbecomesInt.MinValueandmaxValbecomes 2. - 7: beats 2, so
second = 2andmaxVal = 7. - 11: beats 7, so
second = 7andmaxVal = 11. - 8: smaller than 11 and bigger than 7, so
second = 8. - 11: equal to
maxVal, so nothing changes.
Continuing with the rest of the example, 8 is smaller than 11 and bigger than 7, so it only changes second; the next 11 and the next 8 change nothing, because neither is strictly between the two trackers; and 3 is below second. The final second is 8.
A simpler alternative
If one pass isn't required, you can drop the duplicates, sort, and take the second element from the end. For the example, nums.distinct.sorted is Array(2, 3, 7, 8, 11), and nums.distinct.sorted.reverse(1) gives 8. The code is shorter, but it allocates new arrays and takes O(n log n) time. The scan above answers the follow-up question with no sorting and no extra arrays.
Step by step
- Declare
maxValandsecondasvars, both set toInt.MinValue. - Loop over
numswithfor (num <- nums), so each element can update the trackers. - Test for a new maximum with
num > maxVal. - Demote, then replace. Inside that branch, copy
maxValintosecondfirst and only then assignnumtomaxVal. In the other order the old maximum would be lost. - Add an
else iffor values strictly belowmaxValand strictly abovesecond. The first bound keeps repeats of the maximum out. The second keeps a smaller value from replacing a better runner-up. - Assign
numtosecondin that branch. - Return
secondafter the loop.
The solution in Scala
class Solution {
def secondLargest(nums: Array[Int]): Int = {
var maxVal = Int.MinValue
var second = Int.MinValue
for (num <- nums) {
if (num > maxVal) {
second = maxVal
maxVal = num
} else if (num < maxVal && num > second) {
second = num
}
}
return second
}
}
The Scala details worth a closer look:
Int.MinValueis -2147483648.Intin Scala has 32 bits, and arithmetic on it wraps silently on overflow. This solution only compares and copies values, so it never overflows.varandfor. The loop body assigns tovars declared outside it, which Scala allows. Withval, the assignments would not compile.&&and chainedif/else if. Exactly one branch runs per element, and theelse ifcondition is only evaluated when the first test fails.- Explicit
return. The finalreturn secondworks, although Scala code usually ends withsecondas the last expression.
Complexity
Time: O(n). One pass, with a constant number of comparisons per element.
Space: O(1). Two integers, regardless of the array size. Together with the single pass, this answers the follow-up: the work grows in step with the array, where sorting would grow faster, and nothing has to be copied.
Edge cases and common mistakes
- Duplicates of the maximum.
[5, 5, 3]returns 3. Without thenum < maxValtest, the second 5 would be copied intosecondand the method would return 5. - Negative numbers.
[-5, -3]returns -5. Starting both variables at 0 instead ofInt.MinValuewould give a wrong answer here. - Two elements.
[1, 2]returns 1. - Descending input.
[9, 1, 2]returns 2: the 1 becomes the runner-up first, then the 2 replaces it. - Overwriting in the wrong order. Assigning
maxVal = numbeforesecond = maxValmakes both equal tonum. - Fewer than two distinct values.
[5, 5]returns -2147483648, becausesecondnever changes from its starting value. The array is guaranteed to have two distinct values, so this input doesn't occur in the lesson. - A real
Int.MinValue. In an array such as[Int.MinValue, 5], the correct answer is alsoInt.MinValue, so the sentinel and the real value can't be told apart. That is harmless here, but it is the reason the "at least two distinct values" guarantee matters.
Testing
[2, 7, 11, 8, 11, 8, 3, 11]gives8.[5, 5, 3]gives3.[-5, -3]gives-5.[1, 2]gives1.[9, 1, 2]gives2.
