【力扣】摩尔投票(多数元素)系列

【力扣】摩尔投票(多数元素)系列

Leetcode 0169 多数元素

题目描述:Leetcode 0169 多数元素

在这里插入图片描述

分析

  • 本题的考点:摩尔投票法

  • 本题存在很多做法

    (1)做法1:排个序,中间第 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor 2n个数就是答案,时间复杂度是 O ( n × l o g ( n ) ) O(n \times log(n)) O(n×log(n))的。

    (2)做法2:使用哈希表统计次数,空间复杂度是 O ( n ) O(n) O(n)的。

  • 上述做法都不是最优的,下面提供最优做法,即摩尔投票法。设置两个变量r、c,分别记录当前数据以及当前数据的个数。

  • 从前向后一次考察数组中每个数据x,如果c==0,则让r=x, c=1,表示现在有c个库存r,如果下一个元素值还是x,则让c++,即库存增加一个;如果不是x的话,则让c--,表示库存减少一个。则最后r就一定是答案。可以使用反证法证明。

  • 假设最后r不是答案,根据题目,这个多数元素出现次数比其余其他元素个数之和还多(即若有3个元素,多数元素至少有2;即若有4个元素,多数元素至少有3),其他元素不可能将多数元素消耗完,因此最后r一定是答案。

  • 与本题相关的题目有Leetcode 0229 求众数 II,这个题是让求解出现次数严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor 3n的元素,另外LC229还分析了如何求出现次数严格大于 ⌊ n k ⌋ \lfloor \frac{n}{k} \rfloor kn的元素。

代码

  • C++
class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int r = 0, c = 0;  // r: 当前存的数; c: 当前数的数量
        for (int x : nums) {
            if (!c) r = x, c = 1;
            else if (x == r) c++;
            else c--;
        }
        return r;
    }
};
  • Java
class Solution {
    public int majorityElement(int[] nums) {

        int r = 0, c = 0;
        for (int x : nums) {
            if (c == 0) {
                r = x;
                c = 1;
            } else if (r == x) c++;
            else c--;
        }
        return r;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)n为数组长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

Leetcode 0229 求众数 II

题目描述:Leetcode 0229 求众数 II

在这里插入图片描述

分析

  • 本题的考点:摩尔投票法

  • 与本题相关的题目有Leetcode 0169 多数元素,这个题是让求解出现次数严格大于 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor 2n的元素。这是一类题目。

  • 首先考虑如何解决本题,即找出出现次数严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor 3n的元素,之后再推广到 ⌊ n k ⌋ \lfloor \frac{n}{k} \rfloor kn。类似于LC169,本题需要开辟两个仓库r1、r2以及记录仓库对应元素出现次数的变量c1、c2。然后从左到右遍历整个数组,假设此时遍历到x,步骤如下:

    (1)如果仓库1有数据并且等于x,则让仓库1的数量c1++;否则如果仓库2有数据并且等于x,则让仓库2的数量c2++;否则如果仓库1是空的,让r1=x, c1++;否则如果仓库2是空的,让r2=x, c2++;否则说明两个仓库都是非空的,并且其中的元素都不等于x,则让c1--, c2--(相当于消耗了两个不同元素)。

    (2)之后再次遍历数组,统计一下r1, r2出现的次数,如果严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor 3n,则放入结果中。

  • 下面需要证明这种做法是正确的,即如果某个数据出现的次数严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor 3n,则其一定会出现在r1、r2中。分为三种情况讨论(因为最多只能有两个答案):

    (1)在遍历的过程中,如果r1、r2存储的元素不是答案,则如果x是答案的话,会从两个仓库中消耗两个不是答案的数据

    (2)在遍历的过程中,如果r1、r2中某个仓库存储的是答案x,当遍历到一个不是答案的数据时,此时当前元素会被消耗掉,另外有个不是答案的仓库中的数据也会被消耗掉一个,一共消耗掉两个不是答案的数据

    (3)在遍历的过程中,如果r1、r2中存储的都是答案,则两者出现次数都严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor 3n,剩余不是答案的数据不可能消耗完这两个答案对应的次数。

  • (3)显然成立,下面考虑(1)(2),这两种情况都会消耗掉两个数据。为了消耗掉我们的答案,我们需要两倍答案出现的次数的其他数据,这是不可能的,因为答案出现的次数严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor 3n的。若是这样的话,我们总共需要的数据个数为 3 × ( ⌊ n 3 ⌋ + 1 ) 3 \times (\lfloor \frac{n}{3} \rfloor + 1) 3×(3n+1),可以分类讨论得出这个数一定是大于n的。

  • 至此,证明了这种做法是正确的。

  • 上述写法使用C++实现。

  • 我们可以将这种做法推广到 ⌊ n k ⌋ \lfloor \frac{n}{k} \rfloor kn,我们需要的仓库数为k-1个。时间复杂度为 O ( n × k ) O(n \times k) O(n×k)。用Java实现这种写法。

代码

  • C++
class Solution {
public:
    vector<int> majorityElement(vector<int>& nums) {

        int r1, r2, c1 = 0, c2 = 0;  // r存储值, c存储值出现次数
        for (auto x : nums) {
            if (c1 && r1 == x) c1++;
            else if (c2 && r2 == x) c2++;
            else if (!c1) r1 = x, c1++;
            else if (!c2) r2 = x, c2++;
            else c1--, c2--;  // 两个仓库都非空,并且存储的值都不等于x
        }

        c1 = 0, c2 = 0;  // 使用c统计r出现的次数
        for (auto x : nums) {
            if (x == r1) c1++;
            else if (x == r2) c2++;
        }

        vector<int> res;
        int n = nums.size();
        if (c1 > n / 3) res.push_back(r1);
        if (c2 > n / 3) res.push_back(r2);

        return res;
    }
};
  • Java
class Solution {

    static final int K = 3;  // 统计出现次数超过n/K下取整的数

    public List<Integer> majorityElement(int[] nums) {

        int[] r = new int[K - 1], c = new int[K - 1];  // r存储值, c存储值出现次数
        Arrays.fill(c, 0);
        for (int x : nums) {
            boolean flag = false;
            for (int i = 0; i < K - 1; i++)
                if (c[i] != 0 && r[i] == x) {
                    c[i]++;
                    flag = true;
                    break;
                }
            if (flag) continue;
            for (int i = 0; i < K - 1; i++) 
                if (c[i] == 0) {
                    r[i] = x; c[i]++;
                    flag = true;
                    break;
                }
            if (flag) continue;
            for (int i = 0; i < K - 1; i++) c[i]--;  // 仓库都非空,并且存储的值都不等于x
        }

        Arrays.fill(c, 0);  // 使用c统计r出现的次数
        for (int x : nums) {
            for (int i = 0; i < K - 1; i++)
                if (r[i] == x) {
                    c[i]++;
                    break;
                }
        }

        List<Integer> res = new ArrayList<>();
        int n = nums.length;
        for (int i = 0; i < K - 1; i++)
            if (c[i] > n / K) res.add(r[i]);
        return res;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)n为数组长度。

  • 空间复杂度: O ( n ) O(n) O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值