摩尔投票法(多数投票法) + LC 多数元素

本文介绍了摩尔投票法,一种在O(n)时间复杂度和O(1)空间复杂度下找出数组中多数元素的算法。通过模拟选举过程中的对抗和计数阶段,确保找到的元素票数超过总票数的一半。此外,还提供了多种不同方法实现,包括使用Map统计、排序以及位运算优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

摩尔投票:主要解决的问题,如何在任意多的候选人中(选票无序),选出获得票数最多的那个。(该得票数要大于总票数的一半以上)

时间复杂度: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));
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值