414. 第三大的数
问题描述
给你一个非空数组 nums
,返回此数组中第三大的数。如果不存在,则返回数组中最大的数。
注意:答案必须是整数,且需要考虑整数溢出问题。
示例:
输入: nums = [3,2,1]
输出: 1
解释: 第三大的数是 1。
输入: nums = [1,2]
输出: 2
解释: 第三大的数不存在,所以返回最大数 2。
输入: nums = [2,2,3,1]
输出: 1
解释: 注意,要求的是第三大的数,1 是第三大的数。
算法思路
核心思想:维护前三大的数
- 关键:只需要跟踪最大的三个不同数值
- 去重处理:相同的数视为同一个排名
- 边界情况:如果不同数少于3个,返回最大值
方法一:一次遍历维护三个变量
- 使用
first
、second
、third
分别记录第一、第二、第三大的数 - 遍历数组,根据当前数更新这三个变量
- 初始化为
Long.MIN_VALUE
避免整数溢出问题
方法二:使用有序集合(TreeSet)
- 用 TreeSet 自动去重和排序
- 保留最大的三个数
- 最后判断集合大小
代码实现
方法一:一次遍历(推荐)
class Solution {
/**
* 找到数组中第三大的数,如果不存在则返回最大数
*
* @param nums 非空整数数组
* @return 第三大的数,如果不存在则返回最大数
*/
public int thirdMax(int[] nums) {
// 使用Long.MIN_VALUE作为初始值,避免与Integer.MIN_VALUE混淆
// 因为Integer.MIN_VALUE可能是有效数据
long first = Long.MIN_VALUE; // 最大值
long second = Long.MIN_VALUE; // 第二大值
long third = Long.MIN_VALUE; // 第三大值
// 遍历数组中的每个数
for (int num : nums) {
// 如果当前数已经存在于前三名中,跳过(去重)
if (num == first || num == second || num == third) {
continue;
}
// 更新前三大的数
if (num > first) {
// 当前数比最大值还大
third = second; // 原第二大变成第三大
second = first; // 原最大变成第二大
first = num; // 当前数成为最大
} else if (num > second) {
// 当前数介于最大和第二大之间
third = second; // 原第二大变成第三大
second = num; // 当前数成为第二大
} else if (num > third) {
// 当前数介于第二大和第三大之间
third = num; // 当前数成为第三大
}
// 如果num <= third,不需要更新
}
// 检查是否存在第三大的数
// 如果third仍然是初始值,说明不同数少于3个
if (third == Long.MIN_VALUE) {
return (int)first; // 返回最大值
} else {
return (int)third; // 返回第三大的数
}
}
}
方法二:使用TreeSet
import java.util.TreeSet;
class Solution {
/**
* 使用TreeSet的解法
*
* @param nums 非空整数数组
* @return 第三大的数或最大数
*/
public int thirdMax(int[] nums) {
// 使用TreeSet自动去重和排序(降序)
TreeSet<Integer> set = new TreeSet<>((a, b) -> b - a);
// 添加所有元素(自动去重)
for (int num : nums) {
set.add(num);
// 优化:只保留最大的3个数
if (set.size() > 3) {
set.pollLast(); // 移除最小的元素
}
}
// 如果少于3个不同数,返回最大值
if (set.size() < 3) {
return set.first();
} else {
// 获取第三大的数(从大到小的第3个)
int count = 0;
for (int num : set) {
count++;
if (count == 3) {
return num;
}
}
return set.first(); // 理论上不会执行到这里
}
}
}
算法分析
-
时间复杂度:
- 方法一:O(n) - 一次遍历
- 方法二:O(n log 3) ≈ O(n) - TreeSet操作最多3个元素
-
空间复杂度:
- 方法一:O(1) - 只使用三个变量
- 方法二:O(1) - TreeSet最多存储3个元素
-
方法对比:
- 方法一:效率最高,空间最优,推荐使用
- 方法二:代码简洁,但常数因子较大
算法过程
nums = [2,2,3,1]
:
初始化:
first = Long.MIN_VALUE
second = Long.MIN_VALUE
third = Long.MIN_VALUE
遍历过程:
num = 2
2 > Long.MIN_VALUE
→first=2, second=MIN, third=MIN
num = 2
(重复)- 跳过(已存在)
num = 3
3 > 2
→third=MIN, second=2, first=3
num = 1
1 < 3
且1 < 2
且1 > MIN
→third=1
最终状态:
first=3, second=2, third=1
third ≠ MIN_VALUE
→ 返回1
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准情况
int[] nums1 = {3,2,1};
System.out.println("Test 1: " + solution.thirdMax(nums1)); // 1
// 测试用例2:少于3个不同数
int[] nums2 = {1,2};
System.out.println("Test 2: " + solution.thirdMax(nums2)); // 2
// 测试用例3:有重复元素
int[] nums3 = {2,2,3,1};
System.out.println("Test 3: " + solution.thirdMax(nums3)); // 1
// 测试用例4:全相同元素
int[] nums4 = {1,1,1};
System.out.println("Test 4: " + solution.thirdMax(nums4)); // 1
// 测试用例5:包含Integer.MIN_VALUE
int[] nums5 = {-3, -2, -1, Integer.MIN_VALUE};
System.out.println("Test 5: " + solution.thirdMax(nums5)); // -3
// first=-1, second=-2, third=-3
// 测试用例6:大数
int[] nums6 = {1,2,3,4,5};
System.out.println("Test 6: " + solution.thirdMax(nums6)); // 3
// 测试用例7:负数
int[] nums7 = {-1,-2,-3};
System.out.println("Test 7: " + solution.thirdMax(nums7)); // -3
}
关键点
-
去重处理:
- 相同的数视为同一个排名
- 需要在更新前检查是否已存在
-
整数溢出问题:
- 不能用
Integer.MIN_VALUE
作为初始值 - 因为
Integer.MIN_VALUE
可能是有效数据 - 使用
Long.MIN_VALUE
避免混淆
- 不能用
-
更新顺序:
- 必须从大到小判断
- 先检查是否大于
first
,再second
,最后third
-
边界情况:
- 数组长度为1
- 所有元素相同
- 只有两种不同元素
常见问题
-
为什么用long而不是int?
- 因为需要一个比所有int都小的初始值
Long.MIN_VALUE < Integer.MIN_VALUE
- 这样
Long.MIN_VALUE
不会与任何有效int值冲突
-
如果用Integer.MIN_VALUE作为初始值会怎样?
// 错误示例 int first = Integer.MIN_VALUE;
- 当数组包含
Integer.MIN_VALUE
时,无法区分是初始值还是有效值 - 会导致错误判断是否存在第三大的数
- 当数组包含
-
能否用排序解决?
Arrays.sort(nums); // 去重后找第三大的数
- 时间复杂度O(n log n),不如一次遍历高效
-
算法的直观理解:
- 想象有三个奖杯:金、银、铜
- 遍历每个参赛者,根据成绩更新奖杯
- 成绩最好的拿金牌,次好的拿银牌,第三好的拿铜牌
- 最后如果铜牌有人拿,就返回铜牌成绩;否则返回金牌成绩
-
为什么更新时要按顺序?
- 必须先检查最大值,否则会错误更新
- 例如,如果先检查
num > third
,那么first
的更新会出错