摩尔投票
摩尔投票
先给出例题,Leetcode 上的 面试题 17.10. 主要元素]
题目描述:
数组中占比超过一半的元素称之为主要元素。给你一个 整数 数组,找出其中的主要元素。若没有,返回 -1 。请设计时间复杂度为 O(N) 、空间复杂度为 O(1) 的解决方案。
样例1:
输入:[1,2,5,9,5,9,5,5,5]
输出:5
样例2:
输入:[3,2]
输出:-1
样例3:
输入:[2,2,1,1,1,2,2]
输出:2
最为直接的做法是,对给定数组挨个计数(这使用哈希表),然后返回 出现次数超过n/2
的元素
import java.util.HashMap;
import java.util.Map;
public class test {
public int majorityElement(int[] nums) {
Map<数组中元素,该元素出现次数>
//Map<Integer, Integer> map = new HashMap<Integer, Integer>();
int n = nums.length;
for (int num : nums) {
//遍历统计 nums元素
map.put(num, map.getOrDefault(num, 0) + 1);
}
//遍历哈希表,并找出其 值 大于 n/2 的对应 键
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() > n / 2) {
return entry.getKey();
}
}
return -1;
}
public static void main(String[] st) {
test ss = new test();
ss.majorityElement(new int[] { 3, 2, 3 });
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
那么问题来了,你是否可以实现一个时间复杂度为O(n),空间复杂度为O(1)的算法呢?
接下来就到了要介绍的摩尔投票,其介绍如下:
博耶-摩爾多數投票算法(英語:Boyer–Moore majority vote algorithm),中文常作多數投票算法、摩爾投票算法等,是一種用來尋找一組元素中占多數元素的常數空間級時間複雜度算法。這一算法由羅伯特·S·博耶和J·斯特羅瑟·摩爾在1981年發表[1],也是處理數據流的一種典型算法。
算法思想:
进行了不同元素之间的两两的抵消,如果真有众数,则必然最终得以保留下来。值得注意的一点是,如果该数组中没有众数的话,最终则会得到一个该数组中的一个无效元素。
具体实现:
维护一个候选元素candidate
,他可以为nums
中的任意值。以及其对应的出现次数count
,初始值为0(进第一遍循环前)。
- 如果遍历到
nums[i]
时当前count的值为0(遍历至此,他的出现次数被抵消完了),则将nums[i]
中的值赋给candidate
,并设count
= 1,表示该数字出现了一次。- 如果遍历到
nums[i]
的值与candidate
不相符,则count
-1 - 如果遍历到
nums[i]
的值与candidate
是一致,则count
+1
- 如果遍历到
- 由于nums中不一定存在众数,即最终的
candidate
为nums中的一个无效元素。所以要进行第二次遍历,检验candidate
是否为众数。
设想一下,如果每一个元素都只出现了一次,即不存在众数的情况,那么所有元素的出现次数都会被抵消完,而count所对应的
candidate
又具有随机性,因此,最终的candidate
为随机的一个元素
代码实现:
import java.util.HashMap;
import java.util.Map;
public class test {
public int majorityElement(int[] nums) {
// 候选者
int candidate = -1;
// 候选者对应出现的次数
int count = 0;
for (int num : nums) {
// 若当前遍历到的出现次数为0,则将candidate赋值为当前遍历到的num
// 注意:由于需要保证刚赋值完时该数值的出现次数要为一,
// 所以需要进行下面的三木表达式判断,而不是直接else : count+1
if (count == 0) {
candidate = num;
}
//如果candidate 与当前遍历到的num相同,则其candidate出现次数+1,否则当前的出现次数-1
count += candidate == num ? 1 : -1;
// 循环里的执行语句可改为下面几条
// if (count == 0) {
// candidate = num;
// count = 1;
// }
// else {
// if (candidate == num) {
// count++;
// }
// else {
// count--;
// }
// }
}
// 进行第二轮遍历,验证candidate是否为众数
//重置计数器
count = 0;
for (int num : nums) {
if (candidate == num) {
count++;
}
}
//是否满足条件,若是,则返回candidate,否则-1
return count > nums.length / 2 ? candidate : -1;
}
public static void main(String[] st) {
test ss = new test();
ss.majorityElement(new int[] { 3, 2, 3 });
}
}
-
时间复杂度:O(n),其中 n 是数组 nums 的长度。需要遍历数组两次。
-
空间复杂度:O(1)。只需要常数的额外空间。
变形:
将不仅仅局限于查找出现次数为超过 n/2 的单个元素,还可以拓展到 查找出现次数为 n/k
不难得出,出现次数大于 n/k 元素个数最多可以有(k-1) 个,因为若当其个数超过 k-1个时,设为 k个,此时总的元素会有sum > (n/k) ×k = n,不满足题目所给的元素总个数为n,又或是不满足当前统计的是出现次数大于 n/k ,因为若是出现次数小于 n/k,则当统计到的元素个数超过(k-1)时,元素总数sum也是有可能为n的。
因此这时我们可以尝试使用一般的摩尔投票法找出那 (k-1) 个候选元素,注意,这里和上面的情况一样,不一定全都是满足条件,因此还需要进行第二次遍历进行确认。
上述做法正确性的关键是:若存在出现次数超过 n / k 的数,最后必然会成为这 k - 1 个候选者之一。
我们可以通过「反证法」来进行证明:若出现次数超过 n / k 的数 x 最终没有成为候选者。
有两种可能会导致这个结果:
- 数值 x 从来没成为过候选者:
如果 x 从来没成为过候选者,那么在遍历 x 的过程中,必然有 k - 1 个候选者被减了超过 n / k 次,假设当前 x 出现次数为 C,已知 C > n / k,此时总个数为 (k - 1) * C + C = C * k
再根据 C > n / k,可知 C * k > n,而我们总共就只有 n 个数,因此该情况恒不成立。
- 数值 x 成为过候选者,但被逐出替换了:
同理,被逐出替换,说明发生了对 x 出现次数减一的动作(减到 0),每次的减一操作,意味着有其余的 k - 2 个候选者的出现次数也发生了减一动作,加上本身被遍历到的当前数 num[i],共有 k - 1 个数字的和 x 被一同统计。
因此,根据我们摩尔投票的处理过程,如果 x 成为过候选者,并被逐出替换,那么同样能够推导出我们存在超过 n 个数。
综上,如果存在出现次数超过 n / k 的数,其必然会成为 k - 1 个候选者之一。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class BoyerMooreMajorityVoteAlgorithm {
Map<Integer, Integer> candidate;
Map<Integer, Integer> count;
int k;
List<Integer> fun(int[] nums, int _k) {
this.k = _k;
// 若所要求的出现的次数大于数组的长度,则说明该数组中的所有元素(不包含重复)都满足要求
if (k > nums.length) {
Set<Integer> set = new HashSet<Integer>();
for (int num : nums) {
set.add(num);
}
return new ArrayList<Integer>(set);
} else {
List<Integer> ans = new ArrayList<Integer>();
int len = nums.length;
// 创建 候选表<nums[i],其对应的出现次数>
candidate = new HashMap<Integer, Integer>();
// 创建计数器
count = new HashMap<Integer, Integer>();
for (int num : nums) {
// 顺便计个数
count.put(num, count.getOrDefault(num, 0) + 1);
check(num);
}
// 进行第二次遍历,将候选人员中满足出现次数大于 n/k 的人员放入 ans中
for (Map.Entry<Integer, Integer> entry : candidate.entrySet()) {
// 遍历候选表,全员出现次数-1
if (count.get(entry.getKey()) > len / k) {
ans.add(entry.getKey());
}
}
return ans;
}
}
void check(int num) {
// 判断该元素是否存在于候选表中,且要满足其出现次数大于0
if (candidate.containsKey(num) && candidate.get(num) > 0) {
// 若存在,则将其出现次数更新+1
candidate.put(num, candidate.get(num) + 1);
}
// 若候选人数暂未满足 k-1 个,则将现在则为纳入候选队列
else if (candidate.size() < k - 1) {
candidate.put(num, 1);
}
// 若当前候选人员满足 k-1 个,则判断其中是否含有出现次数为0的候选人员,
//有则将其更新为num 没有则将全员-1
else if (candidate.size() == k - 1) {
// 若存在出现次数为0的候选者
if (candidate.containsValue(0)) {
for (Map.Entry<Integer, Integer> entry : candidate.entrySet()) {
// 遍历候选表,寻找那个出现次数为0的候选人员,并将替换为num
if (entry.getValue() == 0) {
candidate.remove(entry.getKey());
candidate.put(num, 1);
break;
}
}
}
else {
for (Map.Entry<Integer, Integer> entry : candidate.entrySet()) {
// 遍历候选表,全员出现次数-1
candidate.replace(entry.getKey(), entry.getValue() - 1);
}
}
}
}
public static void main(String[] st) {
BoyerMooreMajorityVoteAlgorithm mr = new BoyerMooreMajorityVoteAlgorithm();
List<Integer> list = mr.fun(new int[] {1}, 3);
for (int num : list) {
System.out.println(num);
}
}
}
类似的例题,Leetcode 上的 229. 求众数 II
题目描述:
给定一个大小为 n 的整数数组,找出其中所有出现超过
⌊ n/3 ⌋
次的元素。
示例1:
输入:[3,2,3]
输出:[3]
示例2:
输入:nums = [1]
输出:[1]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程