写在前面
觉得写得好,有所收获,记得点个关注和点个赞,不胜感激。
今天遇到了一个新的计算众数的算法,觉得特别有用而且有趣,非常奇妙。然后又拜读了算法原作者的论文,觉得强无敌。这里贴出原作者的论文。引用原论文中的摘要开做本篇博文的开题(自己理解翻译原论文,不准确不要喷我,毕竟我还在考雅思的路上,嘿嘿嘿)。
给定一个数组A,其中含有 n n n 个元素,在数组A中出现超过 n / 2 n/2 n/2 次的元素,也就是出现次数超过 n / 2 n/2 n/2 的众数。现在的问题要求找到数组中是否存在这样的元素,其出现次数超过 n / 2 n/2 n/2。现在本文就来讨论在线性复杂度下,计算成本最优的算法。
理解摩尔投票算法
我们来思考上面的那个问题,你会发现其实如果不考虑时间复杂度或者空间复杂度的话,我们可以通过暴力计数的方式,统计数组中出现的元素的重复次数。这种方法很直接,很暴力,不过想要成为变牛逼,就不能只会大多数人都知道的暴力算法。所以Boyer-Moore Voting Algorithm也就出来了。让我们来理解它吧。
其实算法的思路不难,我们可以把算法理解成两两对抗,如果下一个数不同,那么就对当前数 − 1 -1 −1,这么说可能很晦涩。我们用图来理解,在看下面的图解之前,我们先确定一件事儿,就是如果数组中存在这么一个数,它的出现次数大于 n / 2 n/2 n/2 ,那么它是唯一的,这个我就不多解释,你细品就知道了。
首先我们用两个变量,分别为
c
u
r
cur
cur 以及
c
o
u
n
t
count
count,记录当前元素以及当前元素尚存(过程中会被抵消)的次数。两个变量分别初始化为
n
u
m
s
[
0
]
nums[0]
nums[0] 以及
0
0
0,如下。
然后我们遍历数组,首先遇到第一个元素A,发现相同,那么
c
u
r
cur
cur 不变,
c
o
u
n
t
count
count 计数加一
接着继续移动,遇到元素B 发现元素不相同,那么相互抵消,所以
c
u
r
cur
cur 不变,
c
o
u
n
t
count
count 计数减一
这个时候,继续移动到下一个元素,发现还是不同,而且当前的
c
o
u
n
t
count
count 计数为
0
0
0,那么这个时候将当前元素
c
u
r
cur
cur 赋值为移动的元素 C ,并且
c
o
u
n
t
count
count 计数加一。
按照上面的思路继续移动,一直到最结尾的过程如下:
一直到最后,发现当前变量
c
u
r
cur
cur 和
c
o
u
n
t
count
count 分别是A和
3
3
3,这个时候算法还没有结束。因为我们仔细想一想,最后的
c
o
u
n
t
count
count 不为零,就能说明当前的元素符合要求,及出现的次数超过了
n
/
2
n/2
n/2 么?当然不行。
遍历结束之后,当 c o u n t count count 不为零时,只能说明 c u r cur cur 是目前数组中出现次数最多的一个元素,也就意味着它有可能是我们要找的符合的要求的元素,它如果不可能,那么其他元素也就不可能。
那么我们知道结论之后,如何判断他的次数是否符合要求呢?很简单,再遍历一次数组就可以了,通过对该元素进行计数,进行判断。这样子,我们是算法的时间复杂度将会是 O ( 2 N ) O(2N) O(2N),也就是符合线性时间复杂度,而且空间复杂度为 O ( 1 ) O(1) O(1) 。
出现次数要求是 n/3或者n/k怎么办?
搞懂了上面的 n / 2 n/2 n/2 ,这些问题还会难么?不就是一个拓展的问题么?我们只需要明确一点,如果要求是 n / 3 n/3 n/3 ,那么数组中符合要求的元素最多只有两个, n / k n/k n/k 也是同样,最多只有 k − 1 k - 1 k−1 个。明确了这个,我们顶多就是多预设几个变量的问题。以下用 n / 3 n/3 n/3 为例,进行图解。
同样的,同时设置四个变量,两个用来记录元素,两个用来记录元素出现的次数,并且记录元素的变量同时付赋初值为第一个元素,记录次数的两个变量统计赋初值为0。
通过第一次移动,发现元素相同,因为是按顺序先比对第一组元素计数(如果比对成功就直接进行下一个循环),所以第一次元素计数加一
接着移动,发现下一个元素B不相同,那么检查当前两组值中,谁的计数为空,然后复制为B,计数加一。
继续移动到下一个元素B,检查是否已经存在于两组元素中,如果存在,在对应的那组数中,计数加一
继续移动,发现下一个元素C,在两组数中都找不到对应的元素,那么,两组数计数同时减一。
移动到下一个元素B,那么B的那组数计数加一
移动到下一个元素C,发现不存在与两组数中,两组数应该同时减一,不过由于元素A的那组数的计数已经为零了,所以,替换为C,B的那组数计数正常减一。
按照如山的规则,一直遍历到最后的一个元素即可。过程如下。
一直到最后,发现两组数中的计数都不为零,所以可以知道,元素A和元素B都有可能是我们要找的符合要求的元素,所以这个时候,我们还是老样子遍历数组进行技术就可以了。一直扩展到k,也是同样的思路,就是多定义几个变量的事情。
例题代码
public int majorityElement(int[] nums) {
int solider = nums[0];
int count=0;
for(int i=0;i<nums.length;i++){
if(nums[i]==solider){
count++;
}else{
if(count==0){
solider=nums[i];
count++;
}else {
count--;
}
}
}
return solider;
}
public List<Integer> majorityElement(int[] nums) {
List<Integer> res = new ArrayList<>();
if (nums == null || nums.length == 0) return res;
int cand1 = nums[0], count1 = 0;
int cand2 = nums[0], count2 = 0;
for (int num : nums) {
if (cand1 == num) {
count1++;
continue;
}
if (cand2 == num) {
count2++;
continue;
}
if (count1 == 0) {
cand1 = num;
count1++;
continue;
}
if (count2 == 0) {
cand2 = num;
count2++;
continue;
}
count1--;
count2--;
}
count1 = 0;
count2 = 0;
for (int num : nums) {
if (cand1 == num) count1++;
else if (cand2 == num) count2++;
}
if (count1 > nums.length / 3) res.add(cand1);
if (count2 > nums.length / 3) res.add(cand2);
return res;
}