Problem overview
The function secondLargest receives an IntArray named nums and returns its second largest value. Duplicates of the maximum don't count as second place: in [2, 7, 11, 8, 11, 8, 3, 11] the maximum is 11, which appears three times, and the second largest value is 8. The array always holds at least two distinct values.
The follow-up asks for one pass and no sorting, and that is what this lesson builds.
Approach: keep two trackers
The easy way is to sort the array, then walk back from the end until the value changes. It works, but it costs O(n log n) to order data when you only want the top two values. A single scan can do the job with two variables: maxVal, the largest value seen so far, and second, the largest value seen so far that is strictly smaller than maxVal.
Each element num falls into one of three cases:
num > maxVal: a new maximum. The old maximum becomes the runner-up andnumtakes first place.num < maxValandnum > second: a better runner-up. Replacesecond.- Anything else:
numequals the maximum, or it is no better than the current runner-up. Ignore it.
Trace the example. Both trackers start at Int.MIN_VALUE. Reading 2 gives maxVal = 2. Reading 7 gives maxVal = 7 and second = 2. Reading 11 gives maxVal = 11 and second = 7. Reading 8 is below 11 and above 7, so second = 8. The next 11 equals the maximum and is ignored, so duplicates of the maximum can't take second place. The next 8 and then 3 change nothing, and the final 11 is ignored. The function returns 8.
The sorting route is short in Kotlin. nums.distinct().sortedDescending()[1] removes repeated values, orders what is left from largest to smallest and takes the element at index 1; for the example it gives 8. Both calls build new collections, so that costs O(n log n) time and O(n) extra space. The two-tracker scan gives the same answer with neither cost, and it also shows a pattern that returns later in greedy problems: keep only the few values that can still change the answer, and update them as each element arrives.
The invariant that makes the scan correct is simple. After every element, maxVal is the largest value read so far and second is the largest value read so far that is strictly smaller than maxVal. Each of the three cases keeps both statements true, so when the loop ends they describe the whole array.
Step by step
- Declare
maxValandsecondasvar, both set toInt.MIN_VALUE. Any real element is at least that large, so the first element needs no special treatment. - Loop over
numswithnumas the loop variable, so that every element can update the trackers. - Test for a new maximum with
num > maxVal. - Demote before you overwrite. Inside that branch, copy
maxValintosecondfirst and then assignnumtomaxVal. In the opposite order, the old maximum is lost. - Add an
else iffor values strictly belowmaxValand strictly abovesecond. The first strictness keeps ties with the maximum out, and the second avoids useless writes. - Update
secondtonumin that branch. - Return
secondafter the loop.
The solution in Kotlin
class Solution {
fun secondLargest(nums: IntArray): Int {
var maxVal = Int.MIN_VALUE
var second = Int.MIN_VALUE
for (num in nums) {
if (num > maxVal) {
second = maxVal
maxVal = num
} else if (num < maxVal && num > second) {
second = num
}
}
return second
}
}
The Kotlin details worth a closer look:
Int.MIN_VALUEis the smallestInt, -2147483648. It acts as a "nothing yet" marker, which works because noIntin the array can be smaller.for (num in nums)iterates over the elements directly, with no index. The loop variable is read-only, which is fine because the code only reads it.varis needed. Both trackers are reassigned, sovalwould not compile.if/else ifis used as a statement. At most one branch runs per element, and theelseinelse ifis what keeps a new maximum from also going through the second test.- The array is left alone. The function only reads
nums. Callingnums.sort()instead would reorder the caller's array in place, which is a side effect the scan avoids.
Complexity
Time: O(n). One loop visits each element once and does a constant number of comparisons.
Space: O(1). Two integers and the loop variable, regardless of the array size.
Edge cases and common mistakes
- Duplicates of the maximum.
[5, 5, 1]returns 1, not 5. The strictnum < maxValtest is what makes duplicates of the maximum harmless. With<=, the second 5 would become the runner-up. - Negative numbers.
[-5, -2, -9]returns -5. Starting the trackers at 0 would be a bug here, because no negative value would ever beat 0;Int.MIN_VALUEavoids this. - Increasing order. In
[1, 2, 3, 4], every element is a new maximum, and the demotion step carries the previous maximum intosecond. The result is 3. - Decreasing order. In
[9, 7, 5], 9 is the maximum and 7 takes second place through theelse ifbranch. The result is 7. - Wrong order inside the first branch. Assigning
maxVal = numbeforesecond = maxValsets both tonum. - No second distinct value. The lesson promises two distinct values, but for
[3, 3, 3]the function returnsInt.MIN_VALUE, sincesecondnever changes. Callers who can't promise that should check for it.
Testing
[2, 7, 11, 8, 11, 8, 3, 11]gives 8.[5, 5, 1]gives 1.[1, 2]gives 1.[-5, -2, -9]gives -5.[7, 7, 7, 7, 1]gives 1.[1, 2, 3, 4]gives 3.