📌 题目链接:169. 多数元素 - 力扣(LeetCode)
🔍 难度:简单 | 🏷️ 标签:数组、双指针、原地操作
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)
🧠 题目分析
给定一个大小为 n 的整型数组 nums,要求找出多数元素(Majority Element) —— 即在数组中出现次数 严格大于 ⌊n/2⌋ 的那个元素。
题目保证:
- 数组非空;
- 多数元素一定存在。
💡 关键洞察:
由于多数元素的出现次数 > n/2,意味着它在数量上“压倒性”地超过其他所有元素之和。这种特性使得我们可以设计出非常高效的算法,甚至无需额外空间!
🎯 核心算法及代码讲解:Boyer-Moore 投票算法 ✅
📘 算法背景
Boyer-Moore 投票算法(Boyer–Moore Majority Vote Algorithm)由 Robert S. Boyer 和 J Strother Moore 于 1981 年提出,专门用于在线性时间、常数空间内找出数组中的多数元素(前提是多数元素存在)。
✅ 面试高频考点!这是少数能在 O(1) 空间下解决“众数”问题的经典算法,常被用于考察候选人对“抵消思想”和“状态维护”的理解。
🧩 核心思想:“同归于尽” + “最后幸存者即为答案”
- 将多数元素视为“正军”,其他元素视为“敌军”。
- 每当遇到一个与当前候选者相同的元素,就“增兵”(count++);
- 遇到不同元素,则“两败俱伤”(count--);
- 当 count 归零,说明当前候选者已被“消灭”,换下一个元素作为新候选者。
由于多数元素数量 > n/2,最终剩下的候选者必然是多数元素。
📜 算法步骤(C++ 行注释版)
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = -1; // 初始化候选多数元素(任意值均可)
int count = 0; // 计数器,记录当前候选者的“净优势”
for (int num : nums) {
if (num == candidate)
++count; // 遇到相同元素,支持者+1
else if (--count < 0) { // 遇到不同元素,先抵消(count--),若归零或负
candidate = num; // 则更换候选者为当前元素
count = 1; // 新候选者初始支持数为1
}
}
return candidate; // 最终 candidate 即为多数元素
}
};
🔍 注意:
else if (--count < 0)是关键写法!
先执行count = count - 1,再判断是否< 0。若为真,说明当前候选者已无“净支持”,需更换。
❓ 为什么这个算法正确?
我们用数学归纳法 + 抵消模型来理解:
- 假设多数元素为
M,出现k > n/2次; - 其他所有元素总和
< n/2; - 在遍历过程中,每一对
(M, 非M)可以相互抵消; - 由于
M的数量 > 非M 总数,M 必然无法被完全抵消; - 因此,最后剩下的候选者只能是
M。
🎓 进阶思考:该算法能否用于找“出现超过 n/3 次”的元素?
→ 可以!但需要维护两个候选者(因为最多有两个元素满足 > n/3)。这就是 “扩展 Boyer-Moore”,见 LeetCode 229 题。
🧭 解题思路(分步拆解)
- 明确问题约束:多数元素存在且唯一,出现次数 > n/2。
- 排除暴力法:O(n²) 时间不可接受。
- 考虑哈希表:O(n) 时间 + O(n) 空间,可行但不满足进阶要求。
- 考虑排序:O(n log n) 时间,中位数即答案,但时间不够优。
- 想到“抵消”策略:利用多数元素的“数量优势”,设计 O(n) + O(1) 算法。
- 实现 Boyer-Moore 投票:维护候选者与计数器,一次遍历完成。
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 是否满足进阶要求 |
|---|---|---|---|
| 暴力枚举 | O(n²) | O(1) | ❌ |
| 哈希表 | O(n) | O(n) | ❌ |
| 排序 | O(n log n) | O(log n)(递归栈) | ❌ |
| 随机化 | 期望 O(n) | O(1) | ✅(但有随机性) |
| 分治 | O(n log n) | O(log n) | ❌ |
| Boyer-Moore 投票 | O(n) | O(1) | ✅✅✅ |
✅ Boyer-Moore 是唯一同时满足 O(n) 时间 + O(1) 空间的确定性算法!
💻 完整代码
C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = -1;
int count = 0;
for (int num : nums) {
if (num == candidate)
++count;
else if (--count < 0) {
candidate = num;
count = 1;
}
}
return candidate;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<int> nums1 = {3, 2, 3};
cout << sol.majorityElement(nums1) << "\n"; // 输出: 3
vector<int> nums2 = {2, 2, 1, 1, 1, 2, 2};
cout << sol.majorityElement(nums2) << "\n"; // 输出: 2
return 0;
}
JavaScript
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
let candidate = -1;
let count = 0;
for (const num of nums) {
if (num === candidate) {
count++;
} else if (--count < 0) {
candidate = num;
count = 1;
}
}
return candidate;
};
// 测试
console.log(majorityElement([3, 2, 3])); // 3
console.log(majorityElement([2, 2, 1, 1, 1, 2, 2])); // 2
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!