先来看一道算法题:
数组中占比超过一半的元素称之为主要元素。给定一个整数数组,找到它的主要元素。若没有,返回-1
示例 :
1 | 输入:[1,2,5,9,5,9,5,5,5] |
方法一
看到这个题目第一想法是,先将数组排序,然后取数组中间值。因为超过数组半数的元素在排序之后必然会占据中点位置。
1 | func majorityElement(nums []int) int { |
时间复杂度为O(nlogn),就是排序算法的时间复杂度。
空间复杂度为O(1)。
但是这个实现只能处理存在主要元素的样本。
例如当样本为[1, 2, 3]
时,不存在主要元素,求中点得到的结果就是错误的。
方法二
也可以使用哈希表来统计元素的出现次数,然后遍历哈希表找到数量超出数组半数的元素。
1 | func majorityElement(nums []int) int { |
时间复杂度为O(n)。
空间复杂度为O(n)。
方法三
第三种方法就是本篇的主题,摩尔投票算法。
直接看算法名字的话有一些迷惑,但其实很好理解,把每个数字理解为一个参选者,数字出现的次数就是投给这个参选者的票数。
超过 1/2 的数字最多只可能有一个,每次找到两张不同的选票,将这两张选票消除,这个操作有两种情况:
- 两张中有一张是超过 1/2 的选票
- 两张都是少数的选票
消除后超过 1/2 的选票还是超过总数的 1/2。
1 | func majorityElement(nums []int) int { |
时间复杂度为O(n)。
空间复杂度为O(1)。
扩展
考虑超过 1/3 的情况
找到三个不同的值消除,同样有两种情况:
- 其中有一个是我们要找的那个超过 1/3 选票
- 三个都不是我们要找的
如果抵消的三个值都不是我们要找的,那么显然原本已经占比超过 1/3 的选票,在这轮操作之后将占比更高。
对于第一种情况,我们来简单证明一下。
有一类选票(同一个数值)超过总数的 1/3,我们假设它的数量是 m,总数是 n:
所以对于这种情况,只需要维护 2 个计数器和对应的值来抵消选票:
1 | func majorityElement(nums []int) int { |
再推广到 1/k 的情况
也是同样的道理,给出证明:
这时只需要维护 k-1 个计数器和对应的值来遍历数组就可以了。