LeetCode 229. Majority Element II【摩尔投票法】中等

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给定一个大小为 n 的整数数组,找出其中所有出现超过 ⌊ n/3 ⌋ 次的元素。

示例 1:

js 复制代码
输入:nums = [3,2,3]
输出:[3]

示例 2:

js 复制代码
输入:nums = [1]
输出:[1]

示例 3:

js 复制代码
输入:nums = [1,2]
输出:[1,2]

提示:

  • 1 <= nums.length <= 5 * 10^4
  • -10^9 <= nums[i] <= 10^9

进阶: 尝试设计时间复杂度为 O(n)、空间复杂度为 O(1)的算法解决此问题。


题目集合:

解法 摩尔投票法

在之前的169和面试题17.10中,我们使用「摩尔投票」在 O ( 1 ) O(1) O(1) 空间复杂度内找到了出现次数超过一半的元素,即出现次数大于 n / 2 n / 2 n/2 的数。并使用了例子进行辅助证明。有兴趣的可以参考论文的证明过程,论文地址:MJRTYA Fast Majority Vote Algorithm

对于本题,我们需要统计出现次数超过 n / 3 n / 3 n/3 的数。不失一般性的将其拓展为「统计出现次数超过 n / k n / k n/k 的数」

可以证明,出现次数超过 n / k n / k n/k 的数最多只有 k − 1 k - 1 k−1 个 。否则必然违背「数总共只有 n n n 个」或者「当前统计的是出现次数超过 n / k n / k n/k 的数」的前提条件。当明确了符合要求的数的数量之后,我们可以使用有限变量来代表这 k − 1 k - 1 k−1 个候选数及其出现次数。

然后使用「摩尔投票」的标准做法,在遍历数组时同时 c h e c k check check 这 k − 1 k - 1 k−1 个数,假设当前遍历到的元素为 x x x :

  • 如果 x x x 本身是候选者的话,则对其出现次数加一;
  • 如果 x x x 本身不是候选者,检查是否有候选者的出现次数为 0 0 0 :
    • 若有,则让 x x x 代替其成为候选者,并记录出现次数为 1 1 1 ;
    • 若无,则让所有候选者的出现次数减一
  • 当处理完整个数组后,这 k − 1 k - 1 k−1 个数可能会被填满,但不一定都是符合出现次数超过 n / k n / k n/k 要求的。需要进行二次遍历,来确定候选者是否符合要求,将符合要求的数加到答案。

上述做法正确性的关键是:若存在出现次数超过 n / k n / k n/k 的数,最后必然会成为这 k − 1 k - 1 k−1 个候选者之一。我们可以通过「反证法」来进行证明:若出现次数超过 n / k n / k n/k 的数 x x x 最终没有成为候选者。有两种可能会导致这个结果:

  • 数值 x x x 从来没成为过候选者:如果 x x x 从来没成为过候选者,那么在遍历 x x x 的过程中,必然有 k − 1 k-1 k−1 个候选者被减了超过 n / k n/k n/k 次,假设当前 x x x 出现次数为 C C C ,已知 C > n / k C > n / k C>n/k ,此时总个数为
    ( k − 1 ) ∗ C + C = C ∗ k (k−1)∗C+C=C∗k (k−1)∗C+C=C∗k
    再根据 C > n / k C > n / k C>n/k ,可知 C ∗ k > n C * k > n C∗k>n ,而我们总共就只有 n n n 个数,因此该情况恒不成立。
  • 数值 x x x 成为过候选者,但被逐出替换了:同理,被逐出替换,说明发生了对 x x x 出现次数减一的动作(减到 0 0 0 ),每次减一操作,意味着有其余的 k − 2 k - 2 k−2 个候选者的出现次数也发生了减一动作,加上本身被遍历到的当前数 n u m [ i ] num[i] num[i] ,共有 k − 1 k-1 k−1 个数字和 x x x 被一同统计。 因此,根据我们摩尔投票的处理过程,如果 x x x 成为过候选者,并被逐出替换,那么同样能够推导出我们存在超过 n n n 个数。
  • 综上,如果存在出现次数超过 n / k n/k n/k 的数,其必然会成为 k − 1 k-1 k−1 个候选者之一

一个对摩尔投票法非常形象的比喻是:多方混战。我们把这道题比作一场三方混战,战斗结果一定只有最多两个阵营幸存,其他阵营被歼灭。数组中的数字即代表某士兵所在的阵营。

我们维护两个潜在幸存阵营 A A A 和 B B B 。遍历数组:

  • 如果遇到了属于 A A A 或者属于 B B B 的士兵,则把士兵加入 A A A 或 B B B 队伍中,该队伍人数加一。继续遍历。
  • 如果遇到了一个士兵既不属于 A A A 阵营,也不属于 B B B 阵营,这时有两种情况:
    1. A A A 阵营和 B B B 阵营都还有活着的士兵,那么进行一次厮杀,参与厮杀的三个士兵全部阵亡: A A A 阵营的一个士兵阵亡, B B B 阵营的一个士兵阵亡,这个不知道从哪个阵营来的士兵也阵亡。继续遍历。
    2. A A A 阵营或 B B B 阵营已经没有士兵了。这个阵营暂时消失了。那么把当前遍历到的新士兵算作新的潜在幸存阵营,这个新阵营只有他一个人。继续遍历。
  • 大战结束,最后 A A A 和 B B B 阵营就是初始人数最多的阵营。判断一下 A , B A,B A,B 的人数是否超过所有人数的三分之一即可。
cpp 复制代码
class Solution {
public:
    vector<int> majorityElement(vector<int> & nums, int k){
        vector<int> cand(k-1, INT_MIN);
        vector<int> cnt(k-1, 0);
        for(int i = 0; i < nums.size(); i++){
            bool oldcand = false;
            for(int j = 0; j < k-1; j++){
                if(cand[j] == nums[i]){
                    cnt[j]++;
                    oldcand = true;
                    break;
                }
            }
            if(!oldcand){
                bool newcand = false;
                for(int j = 0; j < k-1; j++){
                    if(cnt[j] == 0){
                        cand[j] = nums[i];
                        cnt[j] = 1;
                        newcand = true;
                        break;
                    }
                }
                if(!newcand){
                    for(int j = 0; j < k-1; j++){
                        cnt[j]--;
                    }
                }
            }
        }
        vector<int> res; 
        int target = nums.size() / k;
        for(int i = 0; i < k-1; i++){
            int count = 0;
            for(int j = 0; j < nums.size(); j++){
                if(cand[i] == nums[j]){
                    count++;
                }
            }
            if(count > target){
                res.push_back(cand[i]); 
            }
        }
        return res;
    }
    vector<int> majorityElement(vector<int>& nums) {
        return majorityElement(nums, 3);    
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)
相关推荐
好记性+烂笔头32 分钟前
hot100-438. 找到字符串中所有字母异位词
算法
六点半8881 小时前
【C/C++】速通涉及string类的经典编程题
c语言·开发语言·c++·算法
@听风吟1 小时前
力扣之182.查找重复的电子邮箱
大数据·javascript·数据库·sql·leetcode
学地理的小胖砸2 小时前
【高分系列卫星简介】
开发语言·数码相机·算法·遥感·地理信息
__AtYou__2 小时前
Golang | Leetcode Golang题解之第417题太平洋大西洋水流问题
leetcode·golang·题解
yanyanwenmeng2 小时前
matlab基础
开发语言·算法·matlab
##晴天小猪3 小时前
ByteTrack多目标跟踪流程图
人工智能·算法·目标检测·机器学习·目标跟踪
Ddddddd_1583 小时前
C++ | Leetcode C++题解之第421题数组中两个数的最大异或值
c++·leetcode·题解
ly-how3 小时前
leetcode练习 二叉树的层序遍历
算法·leetcode
疑惑的杰瑞3 小时前
[数据结构]算法复杂度详解
c语言·数据结构·算法