多数元素 图解题解
找出现超过一半的数,哈希表能做,但摩尔投票只用两个变量、一次扫描,空间直接打到 O(1)。
想象一场计票:手里只握一张「当前候选」和一个「票数」。遇到和候选相同的数,票数+1;遇到不同的数,票数-1;票数降到 0,就把候选换成当前这个数、票数重置为 1。不需要找谁来配对,只是顺序扫、顺序更新。题目保证多数派超过一半,哪怕候选途中被换掉,最终留在手里的必然还是那个多数元素。
这道题到底在问什么
- 输入
- nums = [2,2,1,1,2,1,2,2,2]
- 输出
- 2(在 9 个数里出现了 6 次,过半)
最优解:为什么这么做
一句话答案:LeetCode 169 多数元素的最优解是摩尔投票一次遍历:维护候选人 cand 和票数 count,遇到相同的数加一票、不同的数减一票、票数归零就换当前数做新候选。因为多数元素出现次数严格过半,怎么抵消都消不完,扫完留下的候选必是它,时间 O(n)、空间 O(1)。
这道题真正在问什么
给长度为 n 的数组,题目保证其中有一个元素出现次数超过 ⌊n/2⌋(严格过半),要求把它找出来。「保证存在」和「严格过半」是决定解法的两个前提:过半意味着这个元素比其余所有元素加起来还多,这份悬殊的差距正是后面投票抵消法的底气。
哈希计数和排序为什么不是最优
用哈希表数每个元素的出现次数,O(n) 时间就能找到答案,但要 O(n) 空间;排序后取中间位置的数也一定是多数元素(它过半,无论怎么排都会覆盖中点),但排序要 O(n log n)。当面试官追问「能不能 O(n) 时间加 O(1) 空间」时,这两条路都被堵死——需要一种只用常数个变量、扫一遍就能锁定多数派的办法,摩尔投票(Boyer-Moore 投票算法)就是为此而生。
摩尔投票:不同的数互相抵消
投票法的直觉是打擂台:让一个非多数元素和一个多数元素配对同归于尽,两边各消耗一个。多数元素比其余全部加起来还多,哪怕每个「杂牌」都精准地拉一个多数元素垫背,多数元素也必有剩余。落到代码上只要两个变量:cand 是当前押注的候选人,count 是它的净胜票——遇到相同的数票数加一,遇到不同的数票数减一,减到零说明前面这段完全打平,就换当前数当新候选、票数重置为一。
为什么最后站住的候选一定是答案
把数组按「count 归零的时刻」切成若干段,每个打平的段内部恰好完全抵消,而任何一段里多数元素至多占一半。它在全数组严格过半,被各段抵消掉的部分凑不够它的总数,所以最后必然剩下一段消不平、且盈余的是多数元素——它就是收尾时的 cand。反过来,如果题目不保证多数元素存在,这条推理的前提就塌了:最后的 cand 只是嫌疑人,必须再扫一遍数它的真实出现次数,确认是否真的过半。
复杂度与两个容易写错的点
时间 O(n):数组扫一遍,每个元素只做一次比较和一次加减。空间 O(1):全程只有 cand 和 count 两个变量,不开哈希表。实现上最常见的错:一是 count 归零时忘了换候选人,投票链条直接断掉;二是把「同票加一、异票减一」写反,候选人会被自己人抵消、被对手抬轿,结果完全错乱。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住两个变量:cand 是「现在押注的候选人」,count 是「它还剩多少票」。票数归零就换人,相同加票、不同减票。
- 4开始投票前:还没有候选人 cand,票数 count=0。指针 i 还没出发。
- 5指针 i 走到下标 0,这一格是 2。此刻票数为 0,没有候选人能压住它,于是它成为新候选人。
- 6票数本是 0,于是 2 当选新候选人(标绿的就是它),票数 count 重置为 1。
- 7指针 i 走到下标 1,这一格是 2。它和当前候选人一样,给候选人加一票。
- 8它和候选人 2 相同,给候选人添一票,票数升到 2(标绿表示又一张支持票)。
- 9指针 i 走到下标 2,这一格是 1。它和当前候选人不同,抵消候选人一票。
- 10它和候选人 2 不同,互相抵消一票,候选人票数降到 1(标红表示一张反对票)。
- 11指针 i 走到下标 3,这一格是 1。它和当前候选人不同,抵消候选人一票。
- 12它和候选人 2 不同,互相抵消一票,候选人票数降到 0(标红表示一张反对票)。
- 13指针 i 走到下标 4,这一格是 2。此刻票数为 0,没有候选人能压住它,于是它成为新候选人。
- 14票数本是 0,于是 2 当选新候选人(标绿的就是它),票数 count 重置为 1。
- 15指针 i 走到下标 5,这一格是 1。它和当前候选人不同,抵消候选人一票。
- 16它和候选人 2 不同,互相抵消一票,候选人票数降到 0(标红表示一张反对票)。
- 17指针 i 走到下标 6,这一格是 2。此刻票数为 0,没有候选人能压住它,于是它成为新候选人。
- 18票数本是 0,于是 2 当选新候选人(标绿的就是它),票数 count 重置为 1。
- 19指针 i 走到下标 7,这一格是 2。它和当前候选人一样,给候选人加一票。
- 20它和候选人 2 相同,给候选人添一票,票数升到 2(标绿表示又一张支持票)。
- 21指针 i 走到下标 8,这一格是 2。它和当前候选人一样,给候选人加一票。
- 22它和候选人 2 相同,给候选人添一票,票数升到 3(标绿表示又一张支持票)。
- 23一趟扫完,最后站住的候选人就是 2(标绿的就是它出现的位置,共 6 个,确实过半)。它就是多数元素。
⚠️ 容易写错的地方
✗ 错:以为最后的 cand 不一定是多数元素
✓ 对:本题保证多数元素存在,cand 就是答案
题目承诺出现次数超过一半,抵消后活下来的必然是它;若不保证,才需再扫一遍计数验证
✗ 错:count 为 0 时忘了换候选人
✓ 对:count==0 时把当前数设为新 cand、count 置 1
不换人就没法把后面更强势的数顶上来,投票逻辑断掉
✗ 错:把「同票/异票」判断写反
✓ 对:x==cand 加票,x!=cand 减票
写反会让候选人被同伴抵消、被对手助攻,结果完全错乱
完整代码(Python / C++ / Java)
Python
def majorityElement(nums):
cand = None
count = 0 # 候选人票数
for x in nums:
if count == 0: # 没人了,换候选人
cand = x
count = 1
elif x == cand: # 同票 +1
count += 1
else: # 异票 -1
count -= 1
return candC++
int majorityElement(vector<int>& nums){
int cand = 0, count = 0;
for (int x : nums) {
if (count == 0) { cand = x; count = 1; }
else if (x == cand) count++;
else count--;
}
return cand;
}Java
public int majorityElement(int[] nums) {
int cand = 0, count = 0;
for (int x : nums) {
if (count == 0) { cand = x; count = 1; }
else if (x == cand) count++;
else count--;
}
return cand;
}复杂度
时间
O(n)
指针 i 把数组从头到尾扫一遍,每个元素只处理一次
空间
O(1)
只用 cand 和 count 两个变量,不开额外哈希表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 多数元素 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
cand 和 count 分别代表什么?+
cand 是「当前押注的候选人」,count 是「它现在还剩多少票」。票数归零就换新候选人,扫完 cand 就是多数元素。
如果题目不保证多数元素一定存在呢?+
那摩尔投票选出的 cand 只是「最有可能」的,需要再扫一遍数组数一下它真实出现的次数,确认是否超过一半。
除了摩尔投票还有别的解法吗?+
可以用哈希表统计每个数出现次数(O(n) 时间、O(n) 空间),或排序后取中位数(O(n log n))。摩尔投票胜在 O(1) 空间。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 多数元素 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。