最近在刷LeetCode的题的时候,发现一个特别巧妙的算法:Moore’s voting algorithm。
这个算法是解决这样一个问题:从一个数组中找出出现半数以上的元素。
Moore的主页上有这个算法的介绍:A Linear Time Majority Vote Algorithm和这个算法的一个简单示例演示:演示链接。
每次都找出一对不同的元素,从数组中删掉,直到数组为空或只有一种元素。 不难证明,如果存在元素e出现频率超过半数,那么数组中最后剩下的就只有e。
当然,最后剩下的元素也可能并没有出现半数以上。比如说数组是[1, 2, 3],最后剩下的3显然只出现了1次,并不到半数。排除这种false positive情况的方法也很简单,只要保存下原始数组,最后扫描一遍验证一下就可以了。
在算法执行过程中,我们使用常量空间实时记录一个候选元素c以及其出现次数f(c),c即为当前阶段出现次数超过半数的元素。
在遍历开始之前,该元素c为空,f(c)=0。
然后在遍历数组A时,如果f(c)为0,表示当前并没有候选元素,也就是说之前的遍历过程中并没有找到超过半数的元素。那么,如果超过半数的元素c存在,那么c在剩下的子数组中,出现次数也一定超过半数。因此我们可以将原始问题转化为它的子问题。此时c赋值为当前元素, 同时f(c)=1。
如果当前元素A[i] == c, 那么f(c) += 1。(没有找到不同元素,只需要把相同元素累计起来)
如果当前元素A[i] != c,那么f(c) -= 1。 (相当于删除1个c),不对A[i]做任何处理(相当于删除A[i])
如果遍历结束之后,f(c)不为0,那么元素c即为寻找的元素。上述算法的时间复杂度为O(n),而由于并不需要真的删除数组元素,我们也并不需要额外的空间来保存原始数组,空间复杂度为O(1)。
这证明了关键的一点:数组中从“c被赋值”到“f(c)减到0”的那一段可以被去除,余下部分的多数元素依然是原数组的多数元素。我们可以不断重复这个过程,直到扫描到数组尾部,那么f(c)必然会大于0,而且这个f(c)对应的c就是原数组的多数元素。
Boyer-Moore还有一个优点,那就是可以使用并行算法实现。相关算法可见Finding the Majority Element in Parallel
其基本思想为对原数组采用分治的方法,把数组划分成很多段(每段大小可以不相同),在每段中计算出candidate-count二元组,然后得到最终结果。
举个例子,原数组为[1,1,0,1,1,0,1,0,0]
划分1:
[1,1,0,1,1] –> (candidate,count)=(1,3)
划分2:
[0,1,0,0] –> (candidate,count)=(0,2)
根据(1,3)和(0,2)可得,原数组的多数元素为1.
正因为这个特性,考虑若要从一个非常大的数组中寻找多数元素,数据量可能多大数百G,那么我们甚至可以用MapReduce的方式来解决这个问题。
Boyer-Moore:A Linear Time Majority Vote Alogrithm,这是最基础的最大投票算法
。
原文中提到:decides which element of a sequence is in the majority, provided there is such an element.
,但是讲的有一些含糊。我再补充一下:在一次投票中,如果某一种投票出现的数量大于(这里必须是大于而不能是等于,否则在某些特殊条件下会得到错误结果)总投票,我们就认为这种投票是我们要找的 Majority Element。
参考 Leetcode 上的这道题:169.Majority Element
Given an array of size n, find the Majority Element. The Majority Element is the element that appears more than ⌊ n/2 ⌋ times.
You may assume that the array is non-empty and the Majority Element always exist in the array.
算法的具体思路是:假设在给定长度为 n 的数组中,Majority Element 出现的次数是 k
次,那么非 Majority Element 的出现次数就为 n-k
。如果我们能去掉这 n-k
个元素那么剩下的就全部是 Majority Element 了。
我们可以遍历数组,当碰到两个不一样的数字时,我们将这两个数字同时丢弃这两个数字中可能有一个为 Majority Element,也可能两个都不为Majority Element.因为k
大于 n/2
,所以在最差情况下(每次移除不同数字时都包含一个Majority Element),我们仍然能够保证最后得到的数字是Majority Element.
在网上看到的很多资料中,对这一步的解释都是略微有些问题的。很多人简单的将这一步解释为:找到一个Majority Element
,随后找到一个 非Majority Element
,并将他们一并移除,这其实是错误的。我们在循环的时候,并没有办法判定当前的数字是否为 Majority Element
,所以在移除的时候,我们可能是移除了一个 Majority Element
和一个 非Majority Element
,也有可能移除的是两个非Majority Element
。所以最后 count
的值是不确定的,但是它能够保证在最差情况下,剩余的仍然是 Majority Element。例如,[1,2,3,3,3] 和 [1,3,2,3,3] 这两个数组最后得到的 count
分别为 3 和 1,但是这并不影响答案的正确性。
这也是前面提到的Majority Element的数量必须大于n/2
的原因.
很容易算出最后剩余的Majority Element个数最少为: n - ((n - k) + (n - k)) = 2k - n。
public class Solution {
public int majorityElement(int[] nums) {
int candidate = 0;
for(int i = 0,count = 0; i < nums.length; i++){
//问题一: if 的判定顺序有要求吗?如果有要求的话应该是怎么样的呢?
if(count == 0){
count++;
candidate = nums[i];
}else if(candidate != nums[i]){
count--;
}else{
count++;
}
}
return candidate;
}
}
这个算法很经典,也很简单,毕竟不用自己想。
接下来,我们可以对这个算法做一些简单的扩展,我们当前定义的 Majority Element 的数量大于 n/2 的元素。
如果我们在投票只要满足投票数量超过 n/3 即认为它是最大投票,我们能不能求出这个值呢?
最大投票资料片: 229. Majority Element II
Given an integer array of size n, find all elements that appear more than
⌊ n/3 ⌋
times. The algorithm should run in linear time and in O(1) space.
思路依然同 Majority Element 一样,不同的是我们需要两个 Majority Element 的候选者,同时需要两个 count 分别对候选者进行计数。
count 为 candidate 当前出现的次数。count == 0 说明当前 candidate 对应的候选者已经被移除,我们需要设定一个新的候选者。
public class Solution {
public List majorityElement(int[] nums) {
//问题二:这里给 candidate0 candidate1 初始化值为 0,这会不会影响我们运行的结果?
int candidate0 = 0,candidate1 = 0,count0 = 0, count1 = 0;
for(int i = 0; i < nums.length; i++){
if(candidate0 == nums[i]){
//当前数字等于一号候选数字
count0++;
}else if(candidate1 == nums[i]){
//当前数字等于二号候选数字
count1++;
}else if(count0 == 0){
//当前数字不等于一号候选数字或二号候选数字
//同时必须满足 count 等于 0,因为如果 count != 0,说明还有候选数字在等待与它一组的另外两个数字
count0++;
candidate0 = nums[i];
}else if(count1 == 0){
count1++;
candidate1 = nums[i];
}else{
//只有 不满足以上所有条件我们才能对 count 进行减操作
count0--;
count1--;
}
}
//**问题三:这里能够省略 distinct() 吗?为什么?**
return Stream.of(candidate0, candidate1).distinct().filter(num -> {
int count = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] == num){
count++;
}
}
return count > nums.length / 3;
}).collect(Collectors.toList());
}
}
我们再梳理一遍思路:我们需要找到三个不同的数字,然后抛弃掉这三个数字:
首先要判断是否等于candidate
,如果等于candidate
那么对应的candidate
必须加一等待其他的数字来消除它
当有一个candidate
的count
为 0 时,说明该candidate
已经全部被消除,我们需要设定新的candidate
数字。
当一个数字不等于两个candidate
,同时两个candidate
的count
都不为零。这意味着当前这个数字就是这两个candidate
等待的第三个数字。于是这三个数字被移除,同时他们的count
都要减一。
这个算法到这里就结束了,时间复杂度是线性的 O(n),空间复杂度是 O(1)。接下来是问题解答时间:
问题一: if 的判定顺序有要求吗?如果有要求的话应该是怎么样的呢?
答案是有要求,细心的读者可能发现,在 Majority Element
中,我们对 count == 0
的判断在对 candidate == nums[i]
的判断之前,而在 Majority Element II
中则正好相反。
这是因为,count == 0
是用来判断对应 candidate
的当前存活量,在判断这一步之前,我们必须确保数组中当前数字不等于 两个 candidate
中的任意一个。否则,我们可能会在 count0!=0 && count1==0 && nums[i]==candidate0
时错误的将 nums[i] 赋值给 candidate1。
问题二:这里给 candidate0 candidate1 初始化值为 0,这会不会影响我们运行的结果?
不会,因为 candidate0
只会在第一次循环中使用,如果 candidate0 == nums[0]
,count++
不会引起任何问题。如果 candidate != nums[0]
那么我们此时 count==0
重新初始化 candidate0 == nums[0]
,同样不会有任何影响。
问题二扩充:如果我们初始化 int candidate0 = 0, candidate1 = 1
会不会影响我们的运行结果呢?
问题三:这里能够省略 distinct() 吗?为什么?
不能,尽管我们在循环中首先通过 if(candidate0 == nums[i])
和 else if(candidate1 == nums[i])
两个 if 判断,使得 candidate0 != candidate1
在绝大部分下成立,但是在一种极为特殊的情况下仍然可能会使得我们得到重复的数组。
试想当整个数组所有的数字都相等的时候,我们 candidate0
和 candidate1
这两个候选数字中,有一个数字将永远不会被重新赋值,也就是说,有一个数字将我们赋给的初值保持到了最后。
在我们的代码中,因为我们将两个候选数字都初始化 0,所以当数组 全为0 时会返回错误的结果。
这一点,我们可以通过将两个候选数字初始化为不同的数字来解决:int candidate0 = 0,candidate1 = 1
,这样我们就可以移除掉 distinct() 了
参考资料:
1.http://blog.csdn.net/chfe007/article/details/42919017
2.https://segmentfault.com/a/1190000004905350
3.http://www.cs.utexas.edu/~moore/best-ideas/mjrty/