问题背景
在算法面试和实际编程中,我们经常会遇到这样一个问题:给定一个大小为 n 的数组,找出其中出现次数超过 ⌊n/2⌋ 的元素。这类问题考验了程序员对数据结构和算法的理解和灵活运用。
解题方法一:哈希表统计法
算法思路
哈希表是解决此类问题最直观的方法。通过使用哈希表(unordered_map)记录每个元素出现的次数,我们可以在遍历过程中实时判断是否存在出现次数超过数组长度一半的元素。
代码实现
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> counts;
int mid = nums.size()/2;
for (int n: nums) {
counts[n]++;
if (counts[n] > mid) {
return n;
}
}
return 0;
}
};
算法分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
- 优点:实现简单,逻辑清晰
- 缺点:需要额外的哈希表空间
解题方法二:摩尔投票法
算法背景
摩尔投票法是解决此类问题的一种优雅且高效的算法。其核心思想可以形象地比喻为"无限大乱斗"。
想象着这样一个画面:会议大厅站满了投票代表,每个都有一个牌子上面写着自己所选的候选人的名字。然后选举意见不合的(所选的候选人不同)两个人,会打一架,并且会同时击倒对方。显而易见,如果一个人拥有的选票比其它所有人加起来的选票还要多的话,这个候选人将会赢得这场"战争",当混乱结束,最后剩下的那个代表(可能会有多个)将会来自多数人所站的阵营。但是如果所有参加候选人的选票都不是大多数(选票都未超过一半),那么最后站在那的代表(一个人)并不能代表所有的选票的大多数。因此,当某人站到最后时,需要统计他所选的候选人的选票是否超过一半(包括倒下的),来判断选票结果是否有效。
演进过程
初级版:元素对消
最初级的实现思路是通过多次循环删除对撞元素:
class Solution {
public:
int majorityElement(vector<int>& nums) {
int i = 0;
while(i < nums.size()-1) {
if (nums[i] != nums[i+1]) {
nums.erase(nums.begin()+i, nums.begin()+i+2);
if (i > 0) {
i--;
}
}
else {
i++;
}
}
return nums[0];
}
};
栈实现版
为避免频繁删除,可以使用栈来记录和对消元素:
class Solution {
public:
int majorityElement(vector<int>& nums) {
stack <int> repeatElement;
for (int n: nums) {
if (repeatElement.empty() || repeatElement.top() == n) {
repeatElement.push(n);
}
else {
repeatElement.pop();
}
}
return repeatElement.top();
}
};
升级版:变量记录
最终优化版本直接使用计数变量,不需要额外的数据结构:
class Solution {
public:
int majorityElement(vector<int>& nums) {
int cand = 0, counts = 0;
for (int num: nums) {
if (counts == 0) {
counts++;
cand = num;
}
else {
if (num == cand) {
counts++;
}
else {
counts--;
}
}
}
return cand;
}
};
算法分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 优点:空间复杂度低,实现简单
- 缺点:仅适用于必定存在多数元素的情况
算法总结
方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
哈希表法 | O(n) | O(n) | 实现简单 | 需要额外空间 |
摩尔投票法 | O(n) | O(1) | 空间效率高 | 仅适用特定场景 |
结语
通过对比不同的解题思路,我们可以看到解决同一个问题可以有多种方法。选择合适的算法不仅要考虑时间和空间复杂度,还要根据具体的应用场景和数据特点。