算法专题:投票法

文章目录

    • 169.多数元素(找频率>n/2,且多数元素一定存在)
      • 思路
      • 完整版
      • 补充:
      • 注意点
    • 面试题 17.10. 主要元素(找频率>n/2,但多数元素不一定存在)
      • 思路
      • 完整版
    • 229.多数元素Ⅱ(找频率>n/3)
      • 思路
      • 最开始的写法
      • 修改完整版
      • debug测试:解答错误
    • 总结:找频率>n/3元素与找>n/2元素的区别

看这篇总结:【算法】摩尔投票法 找 多数元素_小威W的博客-CSDN博客

169.多数元素(找频率>n/2,且多数元素一定存在)

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入:nums = [3,2,3]
输出:3

示例 2:

输入:nums = [2,2,1,1,1,2,2]
输出:2

提示:

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

思路

这道题目是找出现频率高于n/2的元素,是投票法。

投票法(Boyer-Moore Voting Algorithm)是一种用于在数组中查找主要元素的算法,主要元素定义为一个元素出现次数超过数组长度的一半。它并不一定能找到频率最高的元素,例如在数组 [1, 2, 2, 3, 3, 3] 中,频率最高的元素是 3,但没有元素出现次数超过数组长度的一半,因此投票法不会返回任何元素。如果数组 [1, 1, 2, 2, 3, 3, 3, 3] 中,频率最高的元素也是主要元素,这时投票法会返回元素 3

如果一个元素是数组的多数元素(出现次数超过数组长度的一半),那么即使我们把它和其他每个不同的元素一一抵消(每次都从数组中删除两个不同的元素),最后剩下的一定是这个多数元素

  • 基本的投票法是找**数组中出现频率超过半数(必须是超过不能是等于)**的元素。
  • 数组中出现频率超过半数的元素,一定只有一个

完整版

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int ans=-1,count=0;
        for(int num:nums){
            if(count==0) ans = num;
            //这样写的话,else会和直接相邻的上一个if组成if-else
            if(num==ans) count++;
            else count--;
        }
        return ans;//注意ans就是多数元素!
    }
};

补充:

在C++中,else语句总是与最近的一个未配对的if语句进行配对。因此,上面写法中,else语句是与第二个if语句配对的,形成了一个if-else结构。

上面是比较直观的写法,也可以换成另一种逻辑更清晰的:

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int ans=-1,count=0;
        for(int num:nums){
            if(count==0) ans = num;
            count += (num==ans)?1:-1;     
        }
        return ans;//注意ans就是多数元素!
    }
};

注意点

  • ans就是多数元素,因为只要count没有抵消到0ans就会存放当前累积的频率最大的(还没有被抵消掉的)候选结果,只有count抵消到0了,才会重新开始寻找下一个相同的元素。
  • 可以举出用例{2,2,2,2,3,2,3,3,3,3}来进行尝试,各占一半是会被抵消掉的
  • 上面这个写法,基于的大前提是数组中一定存在多数元素。否则,类似{2,2,2,2,3,2,3,3,3,3,4}这样的用例,得到的多数元素就会是4(因为前面2和3都抵消了),但是这个用例实际上是没有多数元素的。

面试题 17.10. 主要元素(找频率>n/2,但多数元素不一定存在)

数组中占比超过一半的元素称之为主要元素。给你一个 整数 数组,找出其中的主要元素。若没有,返回 -1 。请设计时间复杂度为 O(N) 、空间复杂度为 O(1) 的解决方案。

示例 1:

输入:[1,2,5,9,5,9,5,5,5]
输出:5

示例 2:

输入:[3,2]
输出:-1

示例 3:

输入:[2,2,1,1,1,2,2]
输出:2

思路

本题是多数元素不一定存在的情况,也就是说,我们根据投票法找到元素之后,需要进行二阶段的判断判断这个元素出现次数是不是真的>n/2

对抗阶段的代码与上一题 169.多数元素 相同,但是本题需要增加计数阶段,也就是验证元素出现次数。

完整版

  • 如果count=0,先赋初值
class Solution {
public:
    int majorityElement(vector<int>& nums) {
        //对抗阶段,先用投票法抵消找出一个备选值
        int ans=0,count=0;
        for(int num:nums){
            if(count==0) ans=num;//ans不需要赋特别大的初值因为会直接覆盖
            count+= (num==ans)?1:-1;
        }
        //计数阶段,验证这个备选值是不是真的出现次数高于n/2
        int cnt=0;
        for(int num:nums){
            if(num==ans) cnt++;
        }
        //判断次数是不是超过n/2
        if(cnt>nums.size()/2){
            return ans;
        }
        return -1;

    }
};

229.多数元素Ⅱ(找频率>n/3)

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

示例 1:

输入:nums = [3,2,3]
输出:[3]

示例 2:

输入:nums = [1]
输出:[1]

示例 3:

输入:nums = [1,2]
输出:[1,2]

提示:

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

思路

这道题的问题是:在一个数组中找到所有出现次数大于n/3的元素。

大于n/3的元素,且不包含相等,也就是说一个数组里最多只有两个这样的元素。和基础的投票法思路相同,我们可以先预设两个备选值,再看后面的元素经过count的抵消,能不能继续保留这两个备选值。

算法分为两个阶段:

  1. 对抗阶段:遍历数组,找出两个候选的多数元素a1a2。对于遍历到的每个元素num,如果num等于a1a2,则增加a1a2的计数;否则,如果a1a2的计数为0,将num设置为a1a2,并将对应的计数设置为1;如果numa1a2都不相等,并且a1a2的计数都不为0,则numa1a2互相抵消,a1a2的计数都减1
  2. 计数阶段:再次遍历数组统计a1a2的出现次数。如果a1a2的出现次数大于n/3,则将其添加到结果列表。

最开始的写法

  • 本题没有说是不是真的存在多数元素,因此也需要计数阶段进行验证
  • 这种写法是错误的,因为三目运算符不能一次性执行多行语句,而在if-else中,我们需要同时更新候选元素num1和count1,再同时更新num2和count2。如果想用三目运算符,需要运用pair数据结构来确保一次性更新两个元素。
class Solution {
public:
    vector<int> majorityElement(vector<int>& nums) {
        //对抗阶段,写法和上面的主要元素是一样的
        int num1=0,count1=0;
        int num2=0,count2=0;
        for(int num:nums){
            //先赋初值
            if(count1==0)  num1=num;
            else if(count2==0)  num2=num;
            //累积频率,这里count2的更新是有问题的,count2最开始没有赋值是不能-1的
            count1 += (num1==num)?1:-1;
            count2 += (num2==num)?1:-1;      
        }
        //计数阶段,验证是不是真的>n/3
        int cnt1=0,cnt2=0;
        for(int num:nums){
            c1+=(num=nums1)?1:0;
            c2+=(num=nums2)?1:0;
        }
        //存储结果
        vector<int>result;
        if(c1>num.size()/3) result.push_back(nums1);
        if(c2>num.size()/3) result.push_back(nums2);
        return result;

    }
};

修改完整版

  • 注意,不能先判断count=0,因为本题有两个候选元素,防止影响两个候选元素的累加!
  • 元素num不等于任何一个候选,且不需要替代任何一个候选的时候,才进行抵消
class Solution {
public:
    vector<int> majorityElement(vector<int>& nums) {
        //对抗阶段,因为题目提示中的最大值是10^9,因此设置初始值为1e9+1
        int num1 = 1e9+1,cnt1=0;
        int num2 = 1e9+1,cnt2=0;
        for(int num:nums){//这种写法num就是nums[i]的数值,没有下标概念
            if(num==num1) cnt1++;
            else if(num==num2) cnt2++;
            else if(cnt1==0){
                num1=num;
                cnt1++;
            }
            else if(cnt2==0){
                num2=num;
                cnt2++;
            }
            else{//出现新元素,且不等于任何一个候选,且不需要替代任何一个候选,就进行抵消
                cnt1--;
                cnt2--;
            }
        }
        //对抗阶段结束后,num1就是第一个数字,num2就是第二个数字
        cnt1=0;
        cnt2=0;
        for(int num:nums){
            cnt1+=(num==num1)?1:0;
            cnt2+=(num==num2)?1:0;
        }
        vector<int>result;
        if(cnt1>nums.size()/3) result.push_back(num1);
        if(cnt2>nums.size()/3) result.push_back(num2);
        return result;
        

    }
};

debug测试:解答错误

算法专题:投票法_第1张图片

这个错误是因为最开始,对抗阶段的逻辑出现了错误。对抗阶段逻辑最开始的写法:

        //对抗阶段,因为题目提示中的最大值是10^9,因此设置初始值为1e9+1
        int num1 = 1e9+1,cnt1=0;
        int num2 = 1e9+1,cnt2=0;
        for(int num:nums){//这种写法num就是nums[i]的数值,没有下标概念
            //这里的写法是错误的,有两个候选元素都需要累积!
            if(cnt1==0){
                num1=num;
                cnt1++;
            }
            else if(cnt2==0){
                num2=num;
                cnt2++;
            }
            else if(num==num1) cnt1++;
            else if(num==num2) cnt2++;
            else{
                cnt1--;
                cnt2--;
            }
        }

这里的问题在于,本题我们有两个候选元素,我们必须先判断这两个候选元素自身能不能进行累积,才能判断count是否==0!

否则例如上图中{2,2}这种用例,只有一个候选元素在累积,那么另一个元素count=0的判定如果放在前面,就会导致原有的==num1的元素没有被累积到

修改为先判定num==num1/num==num2,再判断count==0的情况,即可

总结:找频率>n/3元素与找>n/2元素的区别

这两个问题之间的主要区别在于正在寻找的元素的数量

在查找主要元素的问题中,出现次数超过了n/2的只可能有一个元素。在这种情况下,当count变为0时可以安全地开始考虑一个新的候选元素,因为不可能有两个元素都出现次数超过n/2。所以当count为0时,可以肯定之前的元素不可能是主要元素,所以重新设置一个候选主要元素是合理的。

然而,当寻找的是出现次数超过n/3的元素时,一个数组有可能存在两个这样的元素。

所以,不能直接在count为0时就更改候选元素,必须先判断新元素是不是==num1/==num2!如果直接在count=0的时候更改候选元素,可能会导致另一个候选元素无法累积例如{2,2}这样的情况

也就是说,n/3的情况,我们需要同时追踪两个候选元素(num1和num2)并维护它们的count。出现一个新元素。首先进行两个候选元素的累积判断才能再判断这两个候选元素的count是否为0,为0的时候进行替换

你可能感兴趣的:(算法模板与专题整理,算法,数据结构,c++)