摩尔投票:主要解决的问题,如何在任意多的候选人中(选票无序),选出获得票数最多的那个。(该得票数要大于总票数的一半以上)
时间复杂度:O(N)
空间复杂度:O(1)
形象化描述:
想象着这样一个画面:会议大厅站满了投票代表,每个都有一个牌子上面写着自己所选的候选人的名字。然后选举意见不合的(所选的候选人不同)两个人,会打一架,并且会同时击倒对方。显而易见,如果一个人拥有的选票比其它所有人加起来的选票还要多的话,这个候选人将会赢得这场“战争”,当混乱结束,最后剩下的那个代表(可能会有多个)将会来自多数人所站的阵营。但是如果所有参加候选人的选票都不是大多数(选票都未超过一半),那么最后站在那的代表(一个人)并不能代表所有的选票的大多数。因此,当某人站到最后时,需要统计他所选的候选人的选票是否超过一半(包括倒下的),来判断选票结果是否有效。
算法步骤:
分为两个阶段:对抗(pairing)阶段和计数(counting)阶段
对抗阶段: 两个不同选票的人进行对抗,并会同时击倒对方,当剩下的人都是同一阵营,即相同选票时,结束。
计数阶段: 对最后剩下的人进行选票统计,判断选票是否超过总票数的一半,选票是否有效。
对抗阶段的简化:
我们不需要选票不同就大干一架,我们可以采取一个更加文明的方式。在场有个很聪明的人,他目光扫了一遍所有的选票,在脑子里记住了两件事,当前的候选人的名字cand和他对应的计数器k(k初始化为0,并不是cand的选票数)。看每个人的选票时,先看下k是否为0,如果为0就将cand更新为他看到的马上看到的候选人的姓名,并将k的值更新为1,此时的k代表的是cand更新后人的阵营人数。观察每个人的选票的过程,如果这个人的选票和cand相同,则将k的值+1,否则,将k的值-1,最后的cand可能胜选,还需要统计他的总票数是否超过一半。
算法证明:
假设共有n个代表(一人一票,选票总数为n)。当聪明人看到第i个代表的选票时( 1 ≤ i ≤ n ) ,前面他已经看到的所有选票可以分为两组,第一组是k个代表赞同cand;另一组是选票可以全部成对(选票不同)抵销。当处理完所有的选票时,如果存在大多数,则cand当选。
假设存在一个x其不同于cand,但拥有的选票超过n / 2。但因为第二组的选票可以全部成对抵销,所以x最多的选票数为( n − k ) / 2 ,因此x必须要收到第一组的选票才能超过一半,但是第一组的选票都是cand的,出现矛盾,假设不成立。
所以,如果存在大多数,cand就是那个。
总结:只有这样,选择的对象才能有压倒性的胜利,即才能百分百确定他被选中。
1. 如果最多选一个代表,那么他的票数要至少超过1/2的总票数
2. 如果最多选两个代表,每人票数至少要超过1/3的总票数
3. 如果最多选m个代表,那他们的票数至少要超过1/(m+1)的总票数
算法演示:
网页链接
代码框架:
public int majorityElement(int[] nums) {
int cand = 0, k = 0;
//对抗阶段,最后的cand有可能就是选票最多的候选人
for (int i = 0; i < nums.length; i++){
if(k == 0){
cand = nums[i];
k = 1;
}
else {
if(nums[i] == cand){
k += 1;
}
else {
k--;
}
}
}
//计数阶段,看是否是大多数
int cnt = 0;
for (int num:nums) {
if(num == cand)
cnt++;
}
//表示未超过一半
if (cnt <= nums.length/2){
cand = -1;
}
return cand;
}
题目描述:
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:[3,2,3]
输出:3
示例 2:
输入:[2,2,1,1,1,2,2]
输出:2
进阶:
尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions/xm77tm/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码:
public class LC2 {
//方法一:摩尔投票法。
/*
摩尔投票:主要解决的问题,如何在任意多的候选人中(选票无序),选出获得票数最多的那个。
时间复杂度:O(N)
空间复杂度:O(1)
想象着这样一个画面:会议大厅站满了投票代表,每个都有一个牌子上面写着自己所选的候选人的名字。然后
选举意见不合的(所选的候选人不同)两个人,会打一架,并且会同时击倒对方。显而易见,如果一个人拥有
的选票比其它所有人加起来的选票还要多的话,这个候选人将会赢得这场“战争”,当混乱结束,最后剩下的
那个代表(可能会有多个)将会来自多数人所站的阵营。但是如果所有参加候选人的选票都不是大多数
(选票都未超过一半),那么最后站在那的代表(一个人)并不能代表所有的选票的大多数。因此,当某人
站到最后时,需要统计他所选的候选人的选票是否超过一半(包括倒下的),来判断选票结果是否有效。
算法步骤:分为两个阶段:对抗(pairing)阶段和计数(counting)阶段
对抗阶段:两个不同选票的人进行对抗,并会同时击倒对方,当剩下的人都是同一阵营,即相同选票时,结束。
计数阶段:对最后剩下的人进行选票统计,判断选票是否超过总票数的一半,选票是否有效。
*/
//摩尔投票法模板
public int majorityElement(int[] nums) {
int cand = 0, k = 0;
//对抗阶段
for (int i = 0; i < nums.length; i++){
if(k == 0){
cand = nums[i];
k = 1;
}
else {
if(nums[i] == cand){
k += 1;
}
else {
k--;
}
}
}
//计数阶段
int cnt = 0;
for (int num:nums) {
if(num == cand)
cnt++;
}
//表示未超过一半
if (cnt <= nums.length/2){
cand = -1;
}
return cand;
}
//方法一:Map 时间复杂度O(N) 空间复杂度O(N),边遍历数组,边查看是否在map中,若在数组中
//则取出对应的value,并将其+1,并判断其是否超过了一半,超过了,该数就是出现次数最多的数
//否则更新map中的value。若map不存在,则将其value+1,并存到数组中。
public int majorityElement1(int[] nums){
//key存放数组中的元素,value存放该元素出现的个数
Map<Integer ,Integer> map = new HashMap<>();
for (int num: nums) {
//getOrDefault(Object key,V defaultValue)
//当map中存在key时,使用key对应得value值;不存在则使用默认的defaultValue值
int cnt = map.getOrDefault(num,0) + 1;
if(cnt > nums.length/2){
return num;
}
map.put(num,cnt);
}
return -1;
}
//方法二:排序,时间复杂度O(NlogN),空间复杂度O(NlogN)
// 使用快排将数组nums进行排序,此时的多数元素一定是nums[nums.length/2]
public int majorityElement2(int[] nums){
Arrays.sort(nums);
return nums[nums.length/2];
}
//方法三:摩尔投票法(达到进阶要求)。很精妙的方法
//在该题目中,不妨就可以理解为,票数最多的那个人是一个阵营,其余的候选人是一个阵营。
//遍历一边数组,如果是同阵营的人就将其加入阵营,如果不是就派出一个与其同归于尽,
//即两人投票均无用,相当于无效票。这样最后剩下的票一定是投给得票人最多的。
//因为题目已经给出,是多数元素,即肯定存在一个元素在数组中出现的次数大于n/2
//所以不需要在进行计数阶段,对抗阶段,最后剩下的便是多数元素
public int majorityElement3(int[] nums) {
int cand = 0, k = 0;
for (int num : nums) {
if (k == 0){
cand = num;
k = 1;
}
else{
if(num == cand)
k++;
else k--;
}
}
return cand;
}
//方法四:位运算+二进制。很精妙的方法。我们通过二进制来判断每一位上出现次数最多的是0还是1
//从而给最终结果的该位上赋值,这样当我们把32位全确定一遍后,最终得到的结果即为我们想要的结果
public int majorityElement4(int[] nums){
//设ans初始化为0,即二进制32位全为0,用来记录出现次数最多的数
long ans = 0 ;
//tmp代表第i+1位为1的数
long tmp = 1;
//int型是32位,所以我们只需要遍历二进制的每一位即可
for (int i = 0; i < 32; i++){
//cnt表示所有数字当前位置为1的个数,比如i=0的时候,我们
//可以认为他表示的是所有数字二进制位中最右边为1的数的个数。
int cnt = 0;
//内层循环,遍历数组,去统计,从而得到ans第i位
for (int j = 0; j < nums.length; j++){
//判断数字nums[j]的第i个位置是否为1(i从0开始)
//如果是1,计数器cnt++
if ((nums[j] & tmp) == tmp){
cnt++;
}
//如果cnt大于数组长度的一半,那么该众数的二进制在此位置上一定为1
//我们不妨将ans的二进制上该为的0改为1
//这里可以采取或的操作,ans = ans | tmp
//即,将ans第i个位置上的0改为1
if (cnt > nums.length/2){
ans =ans | tmp;
//因为已经得到ans该位置上是1了,所以就不要继续判断了,进而去判断下一位
break;
}
}
tmp *= 2;
}
return (int) ans;
}
public static void main(String[] args) {
LC2 obj = new LC2();
int[] nums = new int[]{2,2,1,1,1,2,2};
System.out.println(obj.majorityElement4(nums));
}
}