1. 前言
摩尔投票算法可以在线性时间复杂度 O ( n ) O(n) O(n) 和常数空间复杂度 O ( 1 ) O(1) O(1) 的前提下找出一组数据中的多数元素,多数元素的定义是:假设一共有 n n n 个数,那么多数元素就是出现次数严格大于 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n⌋ 的数.
2. 分治思想
以 LeetCode169-多数元素 为例,要找出一个数组中的多数元素,可以采用分治思想.
![](https://cdn.jsdelivr.net/gh/xiaok0707/MyPhotos@master/img/20200731230917.png)
在这道题目中,明确了给定的数组一定存在多数元素,那么根据多数元素的定义可知,多数元素有且只有一个,假设多数元素为 x x x,序列为 a 0 , a 1 , . . . a n − 1 a_0,a_1,...a_{n-1} a0,a1,...an−1, x x x 的出现次数大于 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n⌋.
我们将原数组分成左右两个部分,假设左半部分的长度为一个偶数 l e n L lenL lenL,右半部分的长度为 l e n R lenR lenR, n = l e n L + l e n R n=lenL+lenR n=lenL+lenR,且让多数元素 x x x 在左半部分出现的次数恰好为 L 2 \frac{L}{2} 2L 即左半部分元素个数的一半.
此时如果只考虑原数组的右半部分,那么原数组右半部分的的多数元素应该是什么呢?
答案依然是
x
x
x,原因很简单,
x
x
x 在左右部分出现的次数之和就是
x
x
x 在原数组中出现的次数,所以有如下公式.
l
e
n
L
2
+
x
在
右
半
部
分
出
现
的
次
数
=
x
出
现
的
总
次
数
>
⌊
l
e
n
L
+
l
e
n
R
2
⌋
=
l
e
n
L
2
+
⌊
l
e
n
R
2
⌋
\frac{lenL}{2}+x在右半部分出现的次数=x出现的总次数>\lfloor\frac{lenL+lenR}{2}\rfloor=\frac{lenL}{2}+\lfloor\frac{lenR}{2}\rfloor
2lenL+x在右半部分出现的次数=x出现的总次数>⌊2lenL+lenR⌋=2lenL+⌊2lenR⌋
通过移项即可得到.
x
在
右
半
部
分
出
现
的
次
数
>
⌊
l
e
n
R
2
⌋
x在右半部分出现的次数>\lfloor\frac{lenR}{2}\rfloor
x在右半部分出现的次数>⌊2lenR⌋
所以只考虑右半部分,
x
x
x 依然是多数元素,只是这样一来就将问题的规模减小了,摩尔投票算法就借助的这样的分治思想.
3. 算法过程
维护一个候选者 cand
和它所得票数 cnt
,然后从左到右依次遍历数组中的所有元素,假设当处理的元素是
a
i
a_i
ai.
- 如果 c n t = 0 cnt=0 cnt=0,那么就令 c a n d = a i cand=a_i cand=ai ,也就是把 a i a_i ai 当成多数元素,然后令 c n t = 1 cnt=1 cnt=1 .
- 如果 c n t ≠ 0 cnt\neq0 cnt=0,那么就比较 a i a_i ai 和 c a n d cand cand 的值,如果两者相等,则令 c n t + = 1 cnt+=1 cnt+=1 表示投赞成票,如果不等,则令 c n t − = 1 cnt-=1 cnt−=1 表示投反对票.
处理完毕后,cand
即为原数组的多数元素.
以下面的数组为例,遍历时得到的 c a n d cand cand 和 c n t cnt cnt 的值如下所示,可以看到当 c n t = 0 cnt=0 cnt=0 的时候,此时的多数元素 c a n d cand cand 刚好在之前的区间中出现了一半,所以可以根据第 2 节提到的分治思想,将问题规模变小,直接求解后续数组的多数元素即可.
a: [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
cand: 7 7 7 7 7 7 5 5 5 5 5 5 7 7 7 7
cnt: 1 2 1 2 1 0 1 0 1 2 1 0 1 2 3 4
class Solution {
public:
int majorityElement(vector<int>& nums) {
int cand=0,cnt=0;
for(int e:nums){
if(!cnt){
cand=e;
cnt=1;
}
else{
if(cand==e) ++cnt;
else --cnt;
}
}
return cand;
}
};
4. 推广
摩尔投票算法可以做进一步的推广,可以在线性时间复杂度 O ( n ) O(n) O(n) 和常数空间复杂度 O ( 1 ) O(1) O(1) 的前提下找出长度为 n n n 的一组数据中出现次数严格大于 ⌊ n k ⌋ \lfloor \frac{n}{k} \rfloor ⌊kn⌋ 的数,以 LeetCode229-求众数 II 为例,这道题目要求得到出现次数大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor ⌊3n⌋ 的数.
![](https://cdn.jsdelivr.net/gh/xiaok0707/MyPhotos@master/img/20200802222933.png)
满足要求的数最多只有 2 个,所以可以定义两个候选者 cand1
和 cand2
,以及它们的票数 cnt1
和 cnt2
,然后从左到右依次遍历数组中的所有元素,假设当处理的元素是
a
i
a_i
ai.
- 如果 a i = c n t 1 a_i=cnt_1 ai=cnt1 或者 a i = c n t 2 a_i=cnt_2 ai=cnt2,那么就令 c n t 1 + = 1 cnt_1+=1 cnt1+=1 或 c n t 2 + = 1 cnt_2+=1 cnt2+=1 表示给相应候选者投赞成票.
- 如果 c n t 1 = 0 cnt_1=0 cnt1=0,那么就令 c a n d 1 = a i , c n t 1 = 1 cand_1=a_i,cnt_1=1 cand1=ai,cnt1=1.
- 如果 c n t 2 = 0 cnt_2=0 cnt2=0, 那么就令 c a n d 2 = a i , c n t 2 = 1 cand_2=a_i,cnt_2=1 cand2=ai,cnt2=1.
- 如果上述条件都不满足,则投给 c a n d 1 cand_1 cand1 和 c a n d 2 cand_2 cand2 反对票,即令 c n t 1 − = 1 , c n t 2 − = 1 cnt_1-=1,cnt_2-=1 cnt1−=1,cnt2−=1.
和之前的思路一样,如果 c n t 1 = 0 cnt_1=0 cnt1=0 或者 c n t 2 = 0 cnt_2=0 cnt2=0,说明候选者在当前区间中刚好出现了 1 3 \frac{1}{3} 31 次,真正满足要求的候选者在后续区间中出现的次数一定会大于区间长度的 1 3 \frac{1}{3} 31 ,所以直接求解后续数组即可.
需要注意的是将 cand1
和 cand2
初始化成不同的数值;因为此题目中没有说明答案一定存在,所以在得到最终结果后,需要再次遍历原数组检查候选者是否满足条件.
class Solution {
public:
vector<int> majorityElement(vector<int>& nums) {
int n=nums.size();
int cand1=0,cand2=1;
int cnt1=0,cnt2=0;
for(int e:nums){
if(e==cand1) ++cnt1;
else if(e==cand2) ++cnt2;
else if(!cnt1){
cand1=e;
cnt1=1;
}
else if(!cnt2){
cand2=e;
cnt2=1;
}
else{
--cnt1;
--cnt2;
}
}
vector<int> ans;
cnt1=cnt2=0;
for(int e:nums){
if(e==cand1) ++cnt1;
else if(e==cand2) ++cnt2;
}
if(cnt1>n/3) ans.push_back(cand1);
if(cnt2>n/3) ans.push_back(cand2);
return ans;
}
};