【算法】击败 99% 算法!多种方法查找数组中的多数元素

一、问题

给定一个包含 n 个元素的数组 nums,找到其中的多数元素。多数元素是指在 nums 中出现次数严格大于 ⌊ n/2 ⌋ 的元素。

保证: 为了简化问题,数组 nums 非空,且一定存在多数元素。不需要处理不存在多数元素的情况。

要求: 返回该多数元素。

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

示例 1:
输入:nums = [3,2,3]
输出:3

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

二、问题分析

输入规模 n 的影响:

  • 对于小规模的 n,简单的方法(如哈希表或排序)可能就足够了,因为它们的常数因子可能比较小。
  • 对于大规模的 n,时间复杂度为 O(n log n) (例如排序) 或者空间复杂度为 O(n) (例如哈希表) 的算法可能会变得不可接受。 这时,需要寻找时间复杂度为 O(n) 且空间复杂度为 O(1) 的算法,如摩尔投票法。

因为保证存在多数元素,所以不需要额外判断数组中是否存在多数元素的情况。 这减少了算法的复杂度和判断逻辑。

摩尔投票法(一种更高效的算法,只需 O(n) 的时间和 O(1) 的空间。)依赖于多数元素存在的保证。如果不存在多数元素,摩尔投票法可能会返回一个错误的“候选者”。

不同解法的优缺点:

解法时间复杂度空间复杂度优点缺点适用场景
哈希表O(n)O(n)简单易懂,适用于各种数据类型空间复杂度较高,需要额外的哈希表存储空间数据类型不限,n 不是非常大的情况
排序O(n log n)O(1) 或 O(n)实现简单,有些排序算法是原地排序(O(1) 空间)时间复杂度较高,不适用于大规模数据对空间复杂度有较高要求,可以接受 O(n log n) 时间复杂度的情况
摩尔投票法O(n)O(1)时间复杂度最低,空间复杂度最低,非常高效相对难以理解,依赖于“多数元素存在”的保证大规模数据,对时间和空间复杂度都有严格要求的情况

更深层次的思考:

  • 摩尔投票法的原理: 可以把数组想象成一个战场,多数元素是士兵,其他元素是敌军。 每次遇到不同的元素,就让一个士兵和一个敌军同归于尽。 由于多数元素数量超过一半,最终剩下的士兵一定是多数元素。

  • 可扩展性: 如果题目修改为找到出现次数大于 ⌊ n/k ⌋ 的所有元素呢? 摩尔投票法可以扩展吗? (可以,但需要更复杂的实现)

  • 随机算法: 可以随机选择数组中的一个元素,并统计其出现次数。 如果出现次数大于 ⌊ n/2 ⌋,那么它就是多数元素。否则,继续随机选择。 这种算法的平均时间复杂度是 O(n),但最坏情况下是 O ( n 2 ) O(n^2) O(n2)

三、算法实现

想到的解法:

  • 哈希表: 统计每个元素的出现次数,找到次数大于 ⌊ n/2 ⌋ 的元素。
  • 排序: 排序后,位于 nums[⌊ n/2 ⌋] 的元素一定是多数元素。
  • 摩尔投票法: 一种更高效的算法,只需 O(n) 的时间和 O(1) 的空间。

3.1、哈希表

使用哈希映射(HashMap)来存储每个元素以及出现的次数。对于哈希映射中的每个键值对,键表示一个元素,值表示该元素出现的次数。

方法一:哈希映射。

  1. 计数: 遍历数组 nums,使用哈希映射(字典)统计每个元素出现的次数。键为数组元素,值为其出现次数。
  2. 查找最大值: 遍历哈希映射,找到出现次数最多的元素。

方法二:哈希映射 + 打擂台(推荐)。 在遍历数组 nums 并构建哈希映射的同时,使用“打擂台”的方法记录当前出现次数最多的元素。这样可以避免在哈希映射构建完成后再次遍历查找最大值。

代码实现:

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        map<int, int> counts;
        int count = 0;
        int ret = nums[0];
        for (auto& num : nums) {
            ++counts[num];
            if (counts[num] > count) {
                ++count;
                ret = num;
            }
        }
        return ret;
    }
};

3.2、排序

如果将数组 nums 排序(无论是单调递增还是单调递减),则位于 nums[n / 2] 的元素一定是众数。
在这里插入图片描述
代码实现:

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        std::sort(nums.begin(), nums.end());
        return nums[nums.size()/2];
    }
};

3.3、摩尔投票法(推荐)

假设将数组 nums 中的众数记为 +1,将其他非众数元素记为 -1。 将所有这些值加总求和,所得结果必然大于 0。 这个大于 0 的结果直接反映了众数的数量超过了其他所有非众数元素的数量之和。

Boyer-Moore 算法是一种高效的查找众数的算法。 它维护一个候选众数 candidate 和它的计数 count。 初始化时,candidate 可以是任意值,count 初始化为 0。

遍历数组 nums 的每个元素 x

  1. 如果 count 为 0: 将当前元素 x 赋值给 candidate

  2. 如果 x 等于 candidate:count 加 1。

  3. 如果 x 不等于 candidate:count 减 1。

遍历完成后,candidate 即为数组中可能的众数。 需要注意的是,Boyer-Moore 算法只能保证在存在众数的情况下找到它。 如果不确定众数一定存在,需要额外验证 candidatenums 中出现的次数是否超过数组长度的一半。

代码实现:

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int count = 0;
        int ret = 0;
        for (auto& num : nums) {
            if (count == 0)
                ret = num;
            if (ret == num)
                ++count;
            else
                -- count;
        }
        return ret;
    }
};

四、问题变体:【n/3】

4.1、问题

给定一个大小为 n 的整数数组 nums,找到其中所有出现次数超过 ⌊ n/3 ⌋ 次的元素。换句话说,我们需要找出数组中的所有多数元素,这里的多数元素定义为出现次数大于 n/3 的元素。

示例 1:

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

示例 2:

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

示例 3:

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

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

4.2、问题分析

  • 数组 nums 可能包含重复元素。
  • 数组中可能存在 0 个、1 个或 2 个满足条件的元素。 (为什么最多只有2个? 如果超过2个,那么他们的数量加起来肯定超过 n 了)
  • 需要返回所有满足条件的元素,顺序不做要求。

4.3、算法思路

哈希表(HashMap)计数: 遍历数组,使用哈希表记录每个元素出现的次数。然后遍历哈希表,找出出现次数超过 ⌊ n/3 ⌋ 的元素。

  • 时间复杂度: O(n),其中 n 是数组的长度。
  • 空间复杂度: O(n),最坏情况下,所有元素都不同,哈希表需要存储 n 个元素。
  • 优点: 实现简单,容易理解。
  • 缺点: 空间复杂度较高。

排序: 对数组进行排序。排序后,相同的元素会聚集在一起。然后遍历排序后的数组,统计每个元素出现的次数,找出出现次数超过 ⌊ n/3 ⌋ 的元素。

  • 时间复杂度: O(n log n),主要时间消耗在排序上。
  • 空间复杂度: O(1) 或 O(n),取决于排序算法。原地排序算法(如堆排序)空间复杂度为 O(1),非原地排序算法(如归并排序)空间复杂度为 O(n)。
  • 优点: 如果可以使用原地排序算法,空间复杂度较低。
  • 缺点: 时间复杂度较高。

摩尔投票法(Boyer-Moore Voting Algorithm)的扩展: 由于数组中最多存在两个出现次数超过 ⌊ n/3 ⌋ 的元素,我们可以维护两个候选元素 candidate1candidate2,以及它们对应的计数 count1count2

  • 遍历数组:
    • 如果当前元素等于 candidate1,则 count1 加 1。
    • 如果当前元素等于 candidate2,则 count2 加 1。
    • 如果当前元素既不等于 candidate1 也不等于 candidate2
      • 如果 count1 为 0,则将当前元素赋值给 candidate1count1 设为 1。
      • 否则,如果 count2 为 0,则将当前元素赋值给 candidate2count2 设为 1。
      • 否则,将 count1count2 都减 1。
  • 遍历完成后,candidate1candidate2 是可能的多数元素。需要再次遍历数组,验证它们出现的次数是否超过 ⌊ n/3 ⌋
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)
  • 优点: 时间复杂度和空间复杂度都较低,效率较高。
  • 缺点: 实现稍微复杂一些,需要仔细考虑各种情况。
解法时间复杂度空间复杂度优点缺点
哈希表O(n)O(n)简单易懂空间复杂度高
排序O(n log n)O(1) 或 O(n)空间复杂度较低(如果使用原地排序)时间复杂度高
摩尔投票法O(n)O(1)时间复杂度和空间复杂度都较低,效率较高实现稍复杂,需要仔细考虑各种情况

4.4、哈希表

遍历数组,使用哈希表记录每个元素出现的次数。然后遍历哈希表,找出出现次数超过 ⌊ n/3 ⌋ 的元素。

代码实现:

class Solution {
public:
    vector<int> majorityElement(vector<int>& nums) {
        unsigned n = nums.size();
        map<int, int> counts;
        vector<int> results;
        for (unsigned i = 0; i < n; ++i) {
            ++counts[nums[i]];
        }
        for (auto& num : counts) {
            if (num.second > n / 3)
                results.emplace_back(num.first);
        }
        return results;
    }
};

4.5、排序

对数组进行排序。排序后,相同的元素会聚集在一起。然后遍历排序后的数组,统计每个元素出现的次数,找出出现次数超过 ⌊ n/3 ⌋ 的元素。

代码实现:

class Solution {
public:
    vector<int> majorityElement(vector<int>& nums) {
        unsigned n = nums.size();
        vector<int> results;
        std::sort(nums.begin(), nums.end());
        int index = 0;
        unsigned i = 0;
        for (i = 0; i < n; ++i) {
            if (nums[i] == nums[index])
                continue;
            if ((i - index) > (n / 3))
                results.emplace_back(nums[index]);
            index = i;
        }
        if ((i - index) > (n / 3))
            results.emplace_back(nums[index]);
        return results;
    }
};

4.6、摩尔投票法(推荐)

由于数组中最多存在两个出现次数超过 ⌊ n/3 ⌋ 的元素,可以维护两个候选元素 candidate1candidate2,以及它们对应的计数 count1count2
遍历数组:

  • 如果当前元素等于 candidate1,则 count1 加 1。
  • 如果当前元素等于 candidate2,则 count2 加 1。
  • 如果当前元素既不等于 candidate1 也不等于 candidate2
    • 如果 count1 为 0,则将当前元素赋值给 candidate1count1 设为 1。
    • 否则,如果 count2 为 0,则将当前元素赋值给 candidate2count2 设为 1。
    • 否则,将 count1count2 都减 1。

遍历完成后,candidate1candidate2 是可能的多数元素。需要再次遍历数组,验证它们出现的次数是否超过 ⌊ n/3 ⌋

代码实现:

class Solution {
public:
    vector<int> majorityElement(vector<int>& nums) {
        unsigned n = nums.size();
        pair<int, int> c1 = make_pair(0, 0);
        pair<int, int> c2 = make_pair(0, 0);
        for (unsigned i = 0; i < n; ++i) {
            if (nums[i] == c1.first)
                ++c1.second;
            else if (nums[i] == c2.first)
                ++c2.second;
            else if (c1.second == 0) {
                c1.first = nums[i];
                ++c1.second;
            } else if (c2.second == 0) {
                c2.first = nums[i];
                ++c2.second;
            } else {
                --c1.second;
                --c2.second;
            }
        }
        int count1 = 0;
        int count2 = 0;
        vector<int> results;
        for (unsigned i = 0; i < n; ++i) {
            if (nums[i] == c1.first)
                ++count1;
            else if (nums[i] == c2.first)
                ++count2;
        }
        if (count1 > n / 3)
            results.emplace_back(c1.first);
        if (count2 > n / 3)
            results.emplace_back(c2.first);
        return results;
    }
};

五、总结

剖析了查找数组中多数元素(出现次数大于 n/2 或 n/3)的问题。针对 n/2 的情况,详细讲解了哈希表、排序以及高效的摩尔投票法,并分析了各自的优缺点。对于进阶的 n/3 问题,同样提供了多种解法,包括摩尔投票法的扩展应用。

掌握解决多数元素问题的多种策略,并在时间和空间复杂度之间做出最佳权衡。无论数据规模大小,都能找到适合的解决方案!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion 莱恩呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值