理解并实现摩尔投票算法

闻道有先后,术业有专攻,如是而已。——韩愈《师说》

1 问题定义

首先,摩尔投票算法不是解决一组数求众数问题,它的约束还要多一个。即找到 n 个数中出现次数超过 n/2 的数。也就是说给定 n 个数只有两个结果,要么不存在次数超过 n/2 的数,要么存在次数超过 n/2 的数,如果不存在就返回不存在,如果存在就返回是哪个数次数超过 n/2 。
力扣参考: 面试题 17.10. 主要元素169. 多数元素

2 理解算法

首先,该问题可以用任何求众数的算法解决,但是时间空间效率较佳的是摩尔投票法,O(1)的空间复杂度,O(n)的时间复杂度。摩尔投票法充分利用了超过 n/2 这个条件。我喜欢用同归于尽去理解的它。举个例子,假设六大门派围攻光明顶,明教教徒众多,一个明教教徒和任意一个门派的教徒同归于尽,如果明教教徒足够多,那么取胜的就是明教,反之,明教就被灭了。

3 具体算法逻辑

遍历数组,维护两个变量count和num_now,分别统计当前出现次数最多的元素次数和数值。初始化count=0,num_now=nums[0]。这些数nums[i](教徒)依次登上擂台。
遍历到任意一个数(教徒)有3种结果:
1.count=0。即擂台现在是空的,那么登上擂台,count=1,num_now=nums[i];
2.count!=0 && num_now==nums[i]。即擂台上有人,都是我这个门派的,那我加入他们,count++;
3.count!=0 && num_now!=nums[i]。即擂台上有人,但是是其他门派的,让他们派出一个人和我同归于尽,count- -;

4 java实现

4.1 力扣【169.多数元素】

class Solution {
    
    
    public int majorityElement(int[] nums) {
    
    
        //维护两个变量
        int count=0;//出现次数
        int num_now=0;//当前的数字
        for(int i=0;i<nums.length;i++){
    
    
            //当前无人,站上擂台
            if(count==0){
    
    
                num_now = nums[i];
                count++;
            }
            //有和我一样的人,站队
            else if(nums[i]==num_now) count++;
            //擂台上的和我不一样,同归于尽
            else count--;
        }
        //返回
        return num_now;
    }
}

4.2力扣【面试题 17.10. 主要元素】
注意该题和上述题还有些不一样,它不保证给的数组一定有出现次数超过 n/2 的数,所以还要加判断。

class Solution {
    
    
    public int majorityElement(int[] nums) {
    
    
        //维护两个变量
        int count=0;//出现次数
        int num_now=0;//当前的数字
        for(int i=0;i<nums.length;i++){
    
    
            //当前无人,站上擂台
            if(count==0){
    
    
                num_now = nums[i];
                count++;
            }
            //有和我一样的人,站队
            else if(nums[i]==num_now) count++;
            //擂台上的和我不一样,同归于尽
            else count--;
        }
        //统计确认是不是大于n/2
        count=0;
        for(int i=0;i<nums.length;i++){
    
    
            if(nums[i]==num_now) count++;
        }
        if(count>nums.length/2) 
            return num_now;
        else 
            return -1;
    }
}

5 进阶

力扣:229. 求众数 II 给定一个大小为 n 的数组,找出其中所有出现超过 ⌊ n/3 ⌋ 次的元素。
此时光明顶就可能不是只有一个赢家了,最多有两个门派成为赢家。所以需要维护两套count和now_num。遍历来了一个新的数有五种可能:
1.站上擂台,成立门派1;
2.站上擂台,成立门派2;
3.加入门派1;
4.加入门派2;
5.三个人一起同归于尽。
有很多博主和力扣的解答在判断语句里面写的很简洁,确实可以优化,但是读起来要多思考一下。这里面要注意的点就是,成立门派的时候首先要判断你属不属于另一边的门派,不能立两个中央啊。另外就是在统计最后出现次数的个数的时候,要考虑 num_now1 和 num_now2 是同一个数的情况(输入就一个数)。

class Solution {
    
    
    public List<Integer> majorityElement(int[] nums) {
    
    
        //保存输出的list
        ArrayList<Integer> out = new ArrayList<Integer>();
        //为空检测
        if(nums.length==0) return out;
        //维护四个变量
        int count1 = 0;//出现次数
        int num_now1 = nums[0];//当前的数字
        int count2 = 0;//出现次数
        int num_now2 = nums[0];//当前的数字
        for (int i = 0; i < nums.length; i++) {
    
    
            //新成立门派1
            if (count1 == 0 && num_now2 != nums[i]) {
    
    
                num_now1 = nums[i];
                count1++;
                continue;
            }
            //新成立门派2
            if (count2 == 0 && num_now1 != nums[i]) {
    
    
                num_now2 = nums[i];
                count2++;
                continue;
            }
            //加入门派1
            if (num_now1 == nums[i]) {
    
    
                count1++;
                continue;
            }
            //加入门派2
            if (num_now2 == nums[i]) {
    
    
                count2++;
                continue;
            }
            //不是上面所有情况,同归于尽
            count1--;
            count2--;
        }
        //统计最多的两个数的个数
        count1 = 0;
        count2 = 0;
        for (int i = 0; i < nums.length; i++) {
    
    
            if (nums[i] == num_now1)
                count1++;
            else if(nums[i] == num_now2)
                count2++;
        }
        //如果超过1/3就返回
        if (count1 > nums.length / 3)
            out.add(num_now1);
        if (count2 > nums.length / 3)
            out.add(num_now2);
        return out;
    }
}

如果是出现次数大于 n/4 呢?n/5呢?又如何解决。

6 总结

该题没有涉及复杂的数据结构,但是几个if-else的应用非常精妙,值得好好品味,continue的使用也可以很好的减少代码量。刷十题不如好好总结一题,扩展衍生举一反三。

猜你喜欢

转载自blog.csdn.net/weixin_44215363/article/details/108356550