文章目录
一、问题
给定一个包含 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)来存储每个元素以及出现的次数。对于哈希映射中的每个键值对,键表示一个元素,值表示该元素出现的次数。
方法一:哈希映射。
- 计数: 遍历数组
nums
,使用哈希映射(字典)统计每个元素出现的次数。键为数组元素,值为其出现次数。 - 查找最大值: 遍历哈希映射,找到出现次数最多的元素。
方法二:哈希映射 + 打擂台(推荐)。 在遍历数组 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
:
-
如果
count
为 0: 将当前元素x
赋值给candidate
。 -
如果
x
等于candidate
: 将count
加 1。 -
如果
x
不等于candidate
: 将count
减 1。
遍历完成后,candidate
即为数组中可能的众数。 需要注意的是,Boyer-Moore 算法只能保证在存在众数的情况下找到它。 如果不确定众数一定存在,需要额外验证 candidate
在 nums
中出现的次数是否超过数组长度的一半。
代码实现:
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 ⌋
的元素,我们可以维护两个候选元素 candidate1
和 candidate2
,以及它们对应的计数 count1
和 count2
。
- 遍历数组:
- 如果当前元素等于
candidate1
,则count1
加 1。 - 如果当前元素等于
candidate2
,则count2
加 1。 - 如果当前元素既不等于
candidate1
也不等于candidate2
:- 如果
count1
为 0,则将当前元素赋值给candidate1
,count1
设为 1。 - 否则,如果
count2
为 0,则将当前元素赋值给candidate2
,count2
设为 1。 - 否则,将
count1
和count2
都减 1。
- 如果
- 如果当前元素等于
- 遍历完成后,
candidate1
和candidate2
是可能的多数元素。需要再次遍历数组,验证它们出现的次数是否超过⌊ 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 ⌋
的元素,可以维护两个候选元素 candidate1
和 candidate2
,以及它们对应的计数 count1
和 count2
。
遍历数组:
- 如果当前元素等于
candidate1
,则count1
加 1。 - 如果当前元素等于
candidate2
,则count2
加 1。 - 如果当前元素既不等于
candidate1
也不等于candidate2
:- 如果
count1
为 0,则将当前元素赋值给candidate1
,count1
设为 1。 - 否则,如果
count2
为 0,则将当前元素赋值给candidate2
,count2
设为 1。 - 否则,将
count1
和count2
都减 1。
- 如果
遍历完成后,candidate1
和 candidate2
是可能的多数元素。需要再次遍历数组,验证它们出现的次数是否超过 ⌊ 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 问题,同样提供了多种解法,包括摩尔投票法的扩展应用。
掌握解决多数元素问题的多种策略,并在时间和空间复杂度之间做出最佳权衡。无论数据规模大小,都能找到适合的解决方案!