LeetCode 229. 多数元素 II
题目描述

题意分析
返回数组中出现次数严格大于
floor(n / 3)的所有不同数值,答案可能为空,也可能有一个或两个值。若有三个值都超过三分之一,总次数就会超过数组长度,所以不可能有更多答案。与保证多数元素存在的题目不同,本题不保证候选一定合格。进阶要求线性时间、常数辅助空间,因此需要少量候选筛选,再验证实际频次。
解法:扩展 Boyer-Moore 投票
核心思路
[!blue]
一次删掉三个互不相同的值,不会把真正超过三分之一的元素彻底删光。设它原有
c次且3c > n,若这一组含它,删后仍有3(c - 1) > n - 3;若不含它,它的占比只会更高。因此可以不断抵消三种不同值,只保留剩余候选。用
cand1、cand2表示至多两种未抵消的值,count1、count2表示各自剩余票数。新值命中某个有效候选就加票;没有命中但有空位,就让它占据空位并获得第一票;两位均有效且都不匹配时,当前值与两个候选各抵消一次,将两个计数同时减一。判断顺序必须先匹配已有的有效候选,再填空位。否则新值即使与另一个候选相同,也可能占据空席,让同一个值同时出现在两个位置,破坏三种不同值抵消的前提。
每一步都把已读部分表示为若干已删除的异值三元组,加上最多两种剩余值。真正的多数一定保留在最终的有效候选中,所以不会漏掉;但较少出现的值也可能因扫描末尾的顺序留下,单靠投票不能确认答案。
因此第二次扫描原数组,重新统计有效候选的实际次数,只输出严格超过
n / 3的候选。投票余量与真实频次用途不同,不能直接拿剩余票数判断阈值。
解题步骤
- 初始化两个候选席位及零票数,零票表示当前席位无效。
- 遍历数字,依次尝试命中已有候选、占用空席,最后才执行三值抵消。
- 保留最终有效候选,另设实际计数器,再完整扫描一次原数组。
- 将实际次数严格大于
nums.length / 3的候选加入结果。
代码实现
class Solution {
// 投票阶段维护两个候选值和对应票数,遇到第三种不同数字时同时抵消三者各一次。
public List<Integer> majorityElement(int[] nums) {
int cand1 = 0;
int cand2 = 0;
int count1 = 0;
int count2 = 0;
// 先匹配已有候选再填空席,避免同一值同时占据两席
for (int num : nums) {
if (count1 > 0 && num == cand1) {
count1++;
} else if (count2 > 0 && num == cand2) {
count2++;
} else if (count1 == 0) {
cand1 = num;
count1 = 1;
} else if (count2 == 0) {
cand2 = num;
count2 = 1;
} else {
count1--;
count2--;
}
}
// 投票余量不是真实频次,重新计数验证最终候选
int actual1 = 0;
int actual2 = 0;
for (int num : nums) {
if (count1 > 0 && num == cand1) {
actual1++;
} else if (count2 > 0 && num == cand2) {
actual2++;
}
}
List<Integer> res = new ArrayList<>();
if (actual1 > nums.length / 3) {
res.add(cand1);
}
if (actual2 > nums.length / 3) {
res.add(cand2);
}
return res;
}
}
func majorityElement(nums []int) []int {
// 投票阶段维护两个候选值和对应票数,遇到第三种不同数字时同时抵消三者各一次。
cand1, cand2 := 0, 0
count1, count2 := 0, 0
// 先匹配已有候选再填空席,避免同一值同时占据两席
for _, num := range nums {
if count1 > 0 && num == cand1 {
count1++
} else if count2 > 0 && num == cand2 {
count2++
} else if count1 == 0 {
cand1 = num
count1 = 1
} else if count2 == 0 {
cand2 = num
count2 = 1
} else {
count1--
count2--
}
}
// 投票余量不是真实频次,重新计数验证最终候选
actual1, actual2 := 0, 0
for _, num := range nums {
if count1 > 0 && num == cand1 {
actual1++
} else if count2 > 0 && num == cand2 {
actual2++
}
}
res := make([]int, 0)
if actual1 > len(nums)/3 {
res = append(res, cand1)
}
if actual2 > len(nums)/3 {
res = append(res, cand2)
}
return res
}
复杂度分析
- 时间复杂度:$O(n)$,投票与验证各一次。
- 空间复杂度:$O(1)$,最多两名候选及计数。
关键点总结
[!green]
- 数量上限决定只需两个候选,三种异值抵消保证真正多数不会消失。
- 先命中已有候选,再接纳新值,确保两个有效候选始终不同。
- 投票只缩小候选范围,第二次实际计数才判断资格。
- 零票席位的旧值没有效力,验证时也只统计仍有效的候选。
易错点总结
[!yellow]
- 直接返回最终候选,会把未达阈值的残留值误当成答案。
- 使用抵消后的票数验证,可能把真正多数误删,应重新统计原数组。
- 先填空位再匹配已有候选,可能让同一值占据两席。
- 阈值是严格大于,不能把恰好出现三分之一的值加入结果。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 169. 多数元素 | 简单 | 阈值从超过一半降到超过三分之一,最多候选数从1变成2。 |
| 面试题 17.10. 主要元素 | 简单 | 同样不能仅凭抵消后的候选直接返回,最终需要重新统计真实次数验证阈值。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!