LeetCode 229. 多数元素 II
问题描述
给定一个大小为 n
的整数数组,找出所有出现次数 大于 ⌊n/3⌋
的元素。要求时间复杂度 O(n),空间复杂度 O(1)。
示例:
示例 1:
输入:nums = [3,2,3]
输出:[3]
示例 2:
输入:nums = [1]
输出:[1]
示例 3:
输入:nums = [1,2]
输出:[1,2]
算法思路
摩尔投票法扩展:
- 核心思想:出现次数 > n/3 的元素最多有两个
- 维护两个候选元素:
- 遍历数组,用两个候选元素
candidate1
,candidate2
和计数器count1
,count2
记录 - 遇到相同元素:对应计数器+1
- 遇到不同元素:两个计数器-1(抵消)
- 计数器归零时:更新候选元素
- 遍历数组,用两个候选元素
- 验证阶段:重新统计候选元素的真实出现次数
代码实现
class Solution {
public List<Integer> majorityElement(int[] nums) {
List<Integer> result = new ArrayList<>();
if (nums == null || nums.length == 0) return result;
// 初始化两个候选元素和计数器
int candidate1 = nums[0], candidate2 = nums[0];
int count1 = 0, count2 = 0;
// 第一轮遍历:找出可能的候选元素
for (int num : nums) {
// 更新候选元素1
if (num == candidate1) {
count1++;
}
// 更新候选元素2
else if (num == candidate2) {
count2++;
}
// 替换候选元素1
else if (count1 == 0) {
candidate1 = num;
count1 = 1;
}
// 替换候选元素2
else if (count2 == 0) {
candidate2 = num;
count2 = 1;
}
// 抵消操作
else {
count1--;
count2--;
}
}
// 第二轮遍历:验证候选元素
count1 = 0;
count2 = 0;
for (int num : nums) {
if (num == candidate1) count1++;
else if (num == candidate2) count2++;
}
// 检查是否满足条件
int n = nums.length;
if (count1 > n / 3) result.add(candidate1);
if (count2 > n / 3) result.add(candidate2);
return result;
}
}
算法分析
- 时间复杂度:O(n)
两次独立遍历(候选选择 + 验证) - 空间复杂度:O(1)
仅使用常数空间存储候选元素和计数器
算法过程
nums=[1,1,1,3,3,2,2,2]
:
0: num=1 → cnt1==0 → cnt1=1
1: num=1 → cnt1=2
2: num=1 → cnt1=3
3: num=3 → 不属于c1/c2且cnt2=0 → c2=3, cnt2=1
4: num=3 → cnt2=2
5: num=2 → 不属于c1/c2 → 抵消:cnt1=2, cnt2=1
6: num=2 → 不属于c1/c2 → 抵消:cnt1=1, cnt2=0
7: num=2 → cnt2==0 → c2=2, cnt2=1
候选:c1=1, c2=2
验证:
c1=1: 出现3次 > 2.67 → 有效
c2=2: 出现3次 > 2.67 → 有效
结果:[1,2]
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] nums1 = {3,2,3};
System.out.println("Test 1: " + solution.majorityElement(nums1)); // [3]
// 测试用例2:两个多数元素
int[] nums2 = {1,1,1,3,3,2,2,2};
System.out.println("Test 2: " + solution.majorityElement(nums2)); // [1,2]
// 测试用例3:单一候选
int[] nums3 = {0,0,0,3,4,5};
System.out.println("Test 3: " + solution.majorityElement(nums3)); // [0]
// 测试用例4:无多数元素
int[] nums4 = {1,2,3,4};
System.out.println("Test 4: " + solution.majorityElement(nums4)); // []
// 测试用例5:全相同元素
int[] nums5 = {5,5,5,5};
System.out.println("Test 5: " + solution.majorityElement(nums5)); // [5]
// 测试用例6:三个候选但只有两个有效
int[] nums6 = {2,2,1,3,1,3,2,3}; // [2,2,1,3,1,3,2,3] → 2:3, 3:3, 1:2 → n/3=2.67 → 2和3满足
System.out.println("Test 6: " + solution.majorityElement(nums6)); // [2,3]
}
关键点
-
候选元素初始化:
- 初始计数器设为零
- 候选元素值不重要(会被覆盖)
-
遍历顺序:
- 先检查是否匹配已有候选
- 再检查计数器是否为零
- 最后执行抵消操作
-
验证必要性:
- 摩尔投票法保证真多数元素一定在候选元素中
- 但候选元素不一定是多数元素,必须验证
-
抵消逻辑:
- 每次抵消相当于移除三个不同的元素
- 不影响最终多数元素的相对数量
常见问题
1. 为什么最后要重新验证?
抵消操作可能引入非多数元素作为候选(如 [1,2,3]
中候选可能是 3
,但实际不满足条件)。
2. 处理元素全相同的情况?
算法能正确处理:第一个元素成为候选,后续元素增加计数,最终验证通过。
3. 最坏情况时间复杂度?
严格 O(n),两次线性遍历。
4. 为什么用 else if
链?
确保每个元素只触发一个操作(匹配、更新候选或抵消)。
5. 空数组处理?
开头添加检查:if (nums == null || nums.length == 0) return result;