1. 问题描述
第二大元素问题是数组中的一个经典基础问题。
给定一个整数数组,要求找出其中的第二大元素。例如:
数组中最大的元素是 ,第二大的元素是 。
虽然这个问题看起来很简单,但实际写代码时要先弄清楚一个关键点:
第二大元素到底是”严格第二大”,还是”排序后的第二个元素”?
这两种题型的答案可能不一样。
2. 两种常见题型
2.1 严格第二大元素
严格第二大指的是:
在不同数值中,找出仅次于最大值的那个数。
例如:
最大值是 ,但因为重复的 不能再算作第二大,所以严格第二大是 。
这种题型更关注”不同数字之间的大小关系”。
2.2 排序意义上的第二大元素
排序意义上的第二大指的是:
将数组从大到小排序后,排在第二个位置的元素。
例如:
从大到小排序后仍然是 ,此时第二个位置是 ,所以排序意义上的第二大就是 。
这种题型允许重复元素参与排名。
小结写代码之前,先看清题目要求的是哪一种”第二大”。严格第二大需要去重,排序第二大允许重复。
3. 一次遍历解法(严格第二大)
对于严格第二大元素,可以维护两个变量: 记录当前最大值, 记录当前第二大值。
遍历数组时分三种情况:
- 如果当前元素 ,说明出现了新的最大值。此时原来的 变成第二大, 成为新的最大值。
- 如果当前元素 ,并且 ,说明它不是最大值,但比当前第二大更大,于是更新 。
- 如果当前元素等于 ,或者不超过 ,则跳过。
用前面的例子走一遍:
初始:max = null, second = null
遇到 3:max = 3遇到 1:second = 1遇到 5:second = 3, max = 5遇到 2:不更新(2 < second = 3)遇到 4:second = 4最终第二大元素是 。
Java 代码如下:
class Solution { public int secondLargest(int[] nums) { if (nums == null || nums.length < 2) { throw new IllegalArgumentException("数组长度至少为 2"); }
Integer max = null; Integer second = null;
for (int num : nums) { // 出现新的最大值:原来的 max 退为第二大 if (max == null || num > max) { second = max; max = num; // num 必须严格小于 max,才能作为严格第二大的候选 } else if (num < max && (second == null || num > second)) { second = num; } }
// 如果没有找到不同于 max 的数,说明严格第二大不存在 if (second == null) { throw new IllegalArgumentException("不存在严格第二大元素"); }
return second; }}- 时间复杂度:,只需要遍历一次数组。
- 空间复杂度:,只用了两个额外变量。
4. 分治法解第二大元素问题
第二大元素问题也可以用分治法解决。
实际做题时,一次遍历通常更简单、空间也更优。这里继续讲分治法,主要是为了理解递归拆分、结果合并和候选淘汰思想。
分治法的核心思想是:
分:把原问题拆成规模更小的子问题;
治:分别求解子问题;
合:把子问题的答案合并成原问题的答案。
放到第二大元素问题中,就是:
- 把数组分成左右两半;
- 分别求出左半部分的最大值和第二大值;
- 分别求出右半部分的最大值和第二大值;
- 将左右两边的结果合并,得到整个数组的最大值和第二大值。
4.1 合并过程举例
例如数组:
可以拆成左右两部分:
左半部分的结果是 ,。
右半部分的结果是 ,。
现在要把左右两部分合并。因为 ,所以整体最大值是 。
那么整体第二大元素只可能来自两个候选: 和 。
所以整体第二大是:
4.2 为什么不用考虑 left.second?
在上面的例子中, 为什么不需要参与全局第二大的竞争?
原因很直接:,而 已经在竞争全局第二大了。一个比候选者还小的值,不可能成为全局第二大。
这就是一种”候选淘汰”思想:
如果某个候选值已经被同一组里更大的值压住了,那么它就没有必要继续参与全局比较。
5. 分治法 Java 实现
class Solution { static class Pair { int max; Integer second;
Pair(int max, Integer second) { this.max = max; this.second = second; } }
public int secondLargest(int[] nums) { if (nums == null || nums.length < 2) { throw new IllegalArgumentException("数组长度至少为 2"); }
Pair ans = divide(nums, 0, nums.length - 1);
if (ans.second == null) { throw new IllegalArgumentException("不存在严格第二大元素"); }
return ans.second; }
private Pair divide(int[] nums, int left, int right) { // 单个元素区间:只有最大值,还不存在严格第二大 if (left == right) { return new Pair(nums[left], null); }
int mid = left + (right - left) / 2;
// 分别求左右子数组的最大值和第二大值 Pair leftPair = divide(nums, left, mid); Pair rightPair = divide(nums, mid + 1, right);
// 合并左右子问题的结果 return merge(leftPair, rightPair); }
private Pair merge(Pair a, Pair b) { if (a.max > b.max) { // a 贡献全局最大值,第二大只能来自 a.second 或 b.max return new Pair(a.max, maxNullable(a.second, b.max)); } else if (a.max < b.max) { // b 贡献全局最大值,第二大只能来自 a.max 或 b.second return new Pair(b.max, maxNullable(a.max, b.second)); } else { // 两边最大值相同,不能把重复最大值当成严格第二大 return new Pair(a.max, maxNullable(a.second, b.second)); } }
private Integer maxNullable(Integer a, Integer b) { // null 表示当前还没有严格第二大候选 if (a == null) { return b; } if (b == null) { return a; } return Math.max(a, b); }}这里用包装类型 Integer 表示 second,是为了区分两种情况:
second == null:当前区间里还不存在严格第二大元素。second有值:这个值就是当前区间的严格第二大元素。
不要用 Integer.MIN_VALUE 这类具体数值当作“不存在”的标记,因为数组元素本身也可能等于 Integer.MIN_VALUE。
最能体现分治思想的是这一段:
Pair leftPair = divide(nums, left, mid);Pair rightPair = divide(nums, mid + 1, right);
return merge(leftPair, rightPair);先分别解决左右两个子数组的问题,再把两个子问题的答案合并成整体答案。
6. 复杂度分析
每个元素在递归过程中都会被处理一次,合并时只进行常数次比较。所以时间复杂度为 。
由于递归调用会产生函数调用栈,递归深度为 ,所以空间复杂度为 。
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 一次遍历 | ||
| 分治法 |
可以看到,两种解法的时间复杂度都是 。一次遍历在空间上更优,而分治法的价值在于它展示了一种通用的问题拆解思路,这种思路可以推广到更复杂的问题中。
7. 总结
第二大元素问题看似简单,但要先区分题型:
- 如果要求”严格第二大”,重复的最大值不能算第二大。
- 如果要求”排序意义上的第二大”,重复元素可以参与排名。
常规做法是一次遍历,维护 和 ,时间 ,空间 。
分治做法则是把数组拆成左右两部分,分别求出每部分的最大值和第二大值,再通过合并得到整体结果。合并时的关键是:
哪一边贡献了全局最大值,就考虑那一边的第二大;另一边只考虑最大值。
这就是第二大元素问题中分治合并的核心。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时


















