摩尔选票算法
算法介绍
博耶-摩尔多数投票算法(英语:Boyer–Moore majority vote algorithm),中文常作多数投票算法、摩尔投票算法等,是一种用来寻找一组元素中占多数元素的常数空间级时间复杂度算法。
这一算法应用的问题原型是在集合中寻找可能存在的多数元素,这一元素在输入的序列重复出现并占到了序列元素的一半以上;在第一遍遍历之后应该再进行一个遍历以统计第一次算法遍历的结果出现次数,确定其是否为众数;如果一个序列中没有占到多数的元素,那么第一次的结果就可能是无效的随机元素。
摩尔选票的主要目的是为了解决找到一个数组中出现次数大于 n/2 次的元素,因此首先以解决这个问题为例进行说明。
具体的算法流程如下所示:
- 初始化元素
m
并且将当前的计数器i
赋值为 0, - 第一次遍历整个序列
- 如果当前的计数器
i
的值为 0,则说明当前的元素m
未能满足要求(即出现次数大于 n /2),因此需要重置当前m
的值为当前元素 - 如果当前计数器
i
的值大于 0,则说明当前的元素m
有可能是满足要求的;然后在与当前的处理元素进行比较,如果与m
相等则将计数器i
+ 1,否则将计数器 -1(因为是要解决 "出现次数大于 n / 2",因此满足条件的元素经过这么处理后最终会保留原值在m
中)
- 如果当前的计数器
- 最后会得到
m
的最终值,这个值可能有效的答案,但也可能不是的,因此需要再次遍历元素序列确定是否是出现次数大于 n/2 次的元素
一个 Java 的实现版本如下:
class Solution {
public int majorityElement(int[] nums) {
int n = nums.length;
int x = 0, cnt = 0;
// 第一次遍历找到可能大于 n/2 的元素
for (int val : nums) {
if (cnt == 0)
x = val;
cnt += x == val ? 1 : -1;
}
cnt = 0;
// 第二次遍历确认是不是一个有效的答案
for (int val : nums)
if (val == x) cnt++;
return cnt > n / 2 ? x : -1;
}
}
算法扩展
实际上,可以扩展该算法将其应用于解决 “出现次数大于 n/k 次的元素集合”。
首先,在一个元素序列中,出现次数超过 n/k 的元素的个数最多只有 k - 1 个。
可以首先假设存在 k 个,但是 k 个刚好是一个临界条件,不符合原问题的描述,因此一定会小于 k 个。
因为元素的个数是离散的,如果存在 k - 1 个的话,那么就相当于将第 k 个元素序列平均地分散到前 k - 1 个元素集合中,使得刚好符合题意,因此出现次数超过 n/k 的元素序列最多只有 k - 1个
借鉴上文的 摩尔选票,可以通过一个数组来维护可能满足条件的元素,然后对每个元素采用 摩尔选票 算法进行相同的处理。
具体实现如下所示:
class Solution {
public List<Integer> majorityElement(int[] nums, int k) {
final int n = nums.length;
final int[] cnts = new int[k - 1]; // 记录可能为满足条件的元素的出现次数
final int[] xs = new int[k - 1]; // 当前可能为满足条件的元素的序列
/*
在处理过程中,可能存在别的元素占用当前元素记录数的情况,同时为了避免处理重复的元素,
因此使用一个 HashMap 来记录当前序列中已经存在的元素情况
*/
final Map<Integer, Integer> map = new HashMap<>(k - 1);
for (int i = 0; i < nums.length; ++i) {
// 只有当前处理的元素不在可能序列中或者不是一个新添加元素时,才需要将整个序列的出现次数减少
boolean subFlag = false;
for (int j = 0; j < cnts.length; ++j) {
// 当前元素在可能的序列中,需要将它出现的记录数 +1
if (cnts[j] != 0 && xs[j] == nums[i]) {
subFlag = false;
cnts[j]++;
break;
}
// 如果当前处理的元素已经在 map 中,那么就不需要重新设置值了
if (map.containsKey(nums[i])) {
continue;
}
// 一个新的元素
if (cnts[j] == 0 && ++cnts[j] >= 0) {
subFlag = false;
map.remove(xs[j]); // 移除 j 位置的可能元素
xs[j] = nums[i];
map.put(nums[i], j); // 记录当前的元素
break;
}
subFlag = true;
}
if (subFlag) {
for (int j = 0; j < cnts.length; ++j) {
cnts[j]--;
// 移除已经不满足条件的可能元素
if (cnts[j] == 0) {
map.remove(xs[j]);
/*
对于的序列数组也需要修改,因为在之后的处理中可能会存在重复的元素。
设置为一个不可能出现的值即可
*/
xs[j] = Integer.MIN_VALUE;
}
}
}
}
Arrays.fill(cnts, 0);
// System.out.println(Arrays.toString(xs));
List<Integer> ans = new ArrayList<>();
// 再次确认是有效的元素
for (int num : nums) {
for (int j = 0; j < cnts.length; ++j) {
if (xs[j] == num) cnts[j]++;
}
}
for (int j = 0; j < cnts.length; ++j)
if (cnts[j] > n / k) ans.add(xs[j]);
return ans;
}
}
参考: