【算法】摩尔投票
【摩尔投票】
问题: (majority element)若有一个数组L,长度为n,找出是否有一个数N,N的出现次数大于等于n/2。
问题不算太难,一般可以通过遍历计数,或者排序找中位数的办法来解决。但是如果要求时间复杂度是O(n),空间复杂度是O(1),那么恐怕就没那么简单了。摩尔投票算法正好是这么一个O(n)和O(1)的算法。
● 描述
声明m=0和cnt=0两个变量后面用。
遍历数组,当cnt为0时将当前遍历到的元素赋值给m,然后cnt+=1。若cnt不为0,且遍历到的元素等于当前的m,那么cnt += 1,;若cnt不为0,但是遍历到的元素也不等于当前的m,那么cnt -= 1。最终遍历完成之后,变量m的值就是我们要找的那个majority element。
为什么这个算法奏效?我们姑且从感性的角度来理解一下。既然是投票,那么就把这个数组YY成一个投票人的集合。这些投票人心中都有自己想要投的人,然后我们要找出的就是哪个候选人得票能过半的。因为我们无法做到同时听取所有人的想法,所以我们采取一种“淘汰制”选择。首先第一个人上台说明他想要推举的候选人比如说a。如果第二个人也是推举a的,那么可以认为a的人气是两人份的。第三人如果不是推举a的,那么他将减少a的一份人气。因为选举是要求最终得票过半,所以对于候选人a来说,每一个人不投票给他,就相当于他损失了一票。若第四人也不投a,则a的人气归零,和其他候选人重回起跑线,但此时站在候选台上的仍然是a。此时第五人若想就可以推举他想选的人b,把a给挤下来,b的人气为1。以此类推… 从大局上讲,如果a的支持者势力足够强大,那么无论a被打倒多少次,最终还是能够回到候选台上。这边势力强大的具体量化就是a的支持者至少达到n/2人。这样即使前一半人都支持a,后一半人都不支持a,最终a的人气归零,但是还是保证站在候选台上的是a。
上面就是对投票算法的一个粗浅且感性的理解。
● 代码:
def vote(a): m,cnt = 0,0 for n in a: if cnt == 0: m = n cnt = 1 elif n == m: cnt += 1 else: cnt -= 1 return m
上述过程中并没有对投票者支持的人非常分散的情况作出判断。即无法完成选举的时候不会给出无法完成的错误,而是返回了接近数组末尾的某个“势力相对较强”的元素。如果需要对是否超过n/2做判断那么可以再去遍历依次数组,看到底m元素出现了几次,是否达到标准即可。
● 更复杂一点
如果将问题换成,找出所有出现次数大于n/3次的元素呢。显然,这种元素最多只能有两个。所以我们可以使用投票算法,将有可能是符合要求的两个元素找出来,然后再看他们是否都超过了n/3来判断是否选择它们作为需要选出来的元素。
具象到选举中来,那么可以认为现在要选的人是两个。而且这两个人竞选的位置是平级,不分先后的,所以热门候选人a和热门候选人b的支持者之间不构成直接竞争。因此,算法就变成了,第一人推荐a,a走上甲候选台。第二人推荐b,b走上乙候选台,而此时对a不构成影响所以a的人气不减。如果第三人推荐的是c,那么a和b的人气都要减一份,都归零了(显然不能只减一个人的人气,否则另一个人就可能会出现明明支持者很少,但是由于推举的顺序比较靠前所以当选的bug)。如果此时第四人支持的不是a或者b而是d,那么就可以从甲乙任意一个候选台中挤走一个。比如挤走a,之后d的人气是1,而另一个候选台上的b仍然是0人气。这么循环下去,由于a和b不互相竞争,所以通过这个算法得到的a和b是所有候选人中相对强势的两个。
代码的实现也不复杂:
def vote(a): m,n = 0,0 cm,cn = 0,0 for i in a: if cm == 0: m = i; cm += 1 elif cn == 0: n = i; cn += 1 elif i == m: cm += 1 elif i == n: cn += 1 else: cm -= 1; cn -= 1 return m,n
* 其实摩尔投票,主要是为了能够在O(n)的时间和O(1)的空间解决问题。如果没有这些限制,那么使用HashMap或者其他的一些什么方法则要比这种方法好理解得多多。