一、题目描述
给定一个大小为 n 的数组 nums,返回其中的多数元素。
多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的 ,并且给定的数组总是存在多数元素。
示例 1:
输入: nums = [3,2,3]
输出: 3
示例 2:
输入: nums = [2,2,1,1,1,2,2]
输出: 2
提示:
- 1 <= n <= 5 * 10^4
- -10^9 <= numsi <= 10^9
- 输入保证数组中一定有一个多数元素
二、解题思路总览
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| Boyer-Moore 投票算法 | O(n) | O(1) | 票数相消,多数元素必胜 |
| 排序后取中位数 | O(n log n) | O(1) | 多数元素一定在中间 |
| 哈希表统计 | O(n) | O(n) | 统计每个元素出现次数 |
投票算法是最优解,同时满足 O(n) 时间和 O(1) 空间。
三、方法一:Boyer-Moore 投票算法(推荐)
3.1 核心思想
多数元素出现次数 > n/2,意味着它比其他所有元素加起来都多。
票数相消:将数组中的元素两两配对消去,剩余的必然是多数元素。
具体做法:
- 维护一个候选人
candidate和票数count - 遇到相同元素,票数 +1
- 遇到不同元素,票数 -1
- 当票数归零时,换当前元素为新的候选人
3.2 算法流程图
数组: [2,2,1,1,1,2,2], n=7, 多数元素出现次数 > 3.5 即至少4次
众数 = 2(出现4次)或 1(出现3次,不满足条件)
+------------------------------------------------------------+
| 初始化: candidate = nums[0] = 2, count = 1 |
+------------------------------------------------------------+
+------------------------------------------------------------+
| i=1: nums[1]=2 与 candidate 相同 |
| count++ -> count=2 |
| candidate = 2 |
+------------------------------------------------------------+
2
[2, 2, ...] count=2
+------------------------------------------------------------+
| i=2: nums[2]=1 与 candidate 不同 |
| count-- -> count=1 |
| candidate = 2 |
+------------------------------------------------------------+
2
[2, 2, 1] count=1
+------------------------------------------------------------+
| i=3: nums[3]=1 与 candidate 不同 |
| count-- -> count=0 |
| count归零,更换候选人: candidate = nums[3] = 1 |
| count重置为1 |
+------------------------------------------------------------+
1
[2, 2, 1, 1] count=1
+------------------------------------------------------------+
| i=4: nums[4]=1 与 candidate 相同 |
| count++ -> count=2 |
| candidate = 1 |
+------------------------------------------------------------+
1
[2, 2, 1, 1, 1] count=2
+------------------------------------------------------------+
| i=5: nums[5]=2 与 candidate 不同 |
| count-- -> count=1 |
| candidate = 1 |
+------------------------------------------------------------+
1
[2, 2, 1, 1, 1, 2] count=1
+------------------------------------------------------------+
| i=6: nums[6]=2 与 candidate 不同 |
| count-- -> count=0 |
| count归零,更换候选人: candidate = nums[6] = 2 |
| count重置为1 |
+------------------------------------------------------------+
2
[2, 2, 1, 1, 1, 2, 2] count=1
最终 candidate = 2,即为多数元素
相消的直观理解:
数组: [2, 2, 1, 1, 1, 2, 2]
配对相消(相同元素配对,不同元素抵消):
[2, 2] -> 抵消
[1, 1] -> 抵消
[1] -> 剩下 1
[2, 2] -> 抵消
实际过程(票数法):
票数: 2 -> 1 -> 0(换人) -> 1 -> 2 -> 1 -> 0(换人) -> 1
候选人: 2 -> 2 -> 2 -> 1 -> 1 -> 1 -> 1 -> 2
最终候选人 = 2
3.3 完整代码(投票算法)
cpp
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = nums[0];
int count = 1;
for (int i = 1; i < nums.size(); i++) {
if (candidate == nums[i]) {
count++;
} else {
count--;
}
if (count == 0) {
candidate = nums[i];
count = 1;
}
}
return candidate;
}
};
3.4 count 初始化为 1 的版本
cpp
class Solution {
public:
int majorityElement(vector<int>& nums) {
int n = nums.size();
int count = 1;
int ans = nums[0];
for (int i = 1; i < n; i++) {
if (ans != nums[i]) {
count--;
} else {
count++;
}
if (count < 0) {
count = 1;
ans = nums[i];
}
}
return ans;
}
};
两种写法的区别:
| 版本 | 初始 count | 触发换人的条件 |
|---|---|---|
| 标准版 | count = 0 | count == 0 时换人 |
| count=1版 | count = 1 | count < 0 时换人 |
本质相同,只是初始化不同导致的边界处理差异。
3.5 复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 遍历数组一次 |
| 空间 | O(1) | 只用两个变量 |
四、方法二:排序后取中位数
4.1 核心思想
多数元素出现次数 > n/2,排序后它一定会占据数组的中间位置。
关键结论 :排序后下标为 n/2 的元素一定是多数元素。
4.2 算法流程图
数组: [2,2,1,1,1,2,2] -> 排序后: [1,1,1,2,2,2,2]
+------------------------------------------------------------+
| 数组长度 n = 7 |
| 中位数下标 = n/2 = 3 |
+------------------------------------------------------------+
排序后数组:
+---+---+---+---+---+---+---+
| 1 | 1 | 1 | 2 | 2 | 2 | 2 |
+---+---+---+---+---+---+---+
^
下标 3
nums[3] = 2,即为多数元素
为什么是 n/2?
多数元素出现次数 > n/2
例如 n=7,多数元素至少出现 4 次
排序后,多数元素占用的位置:
- 如果集中在左边,至少占据下标 0,1,2,3
- 如果集中在右边,至少占据下标 3,4,5,6
- 如果分散分布,也至少占据下标 3
所以下标 n/2 处一定是多数元素
4.3 完整代码
cpp
class Solution {
public:
int majorityElement(vector<int>& nums) {
sort(nums.begin(), nums.end());
return nums[nums.size() / 2];
}
};
4.4 复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n log n) | 排序的时间复杂度 |
| 空间 | O(1) | 若原地排序 |
五、方法三:哈希表统计
5.1 核心思想
遍历数组,统计每个元素出现的次数,最后找出次数大于 n/2 的元素。
5.2 算法流程图
数组: [2,2,1,1,1,2,2], n=7, 阈值 = n/2 = 3.5
+------------------------------------------------------------+
| 遍历数组,统计每个元素出现次数 |
+------------------------------------------------------------+
统计: 1 -> 3次
2 -> 4次
+------------------------------------------------------------+
| 找出次数 > 3 的元素 |
+------------------------------------------------------------+
count[2] = 4 > 3 -> 答案 = 2
结果: 2
5.3 完整代码
cpp
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> freq;
int threshold = nums.size() / 2;
for (int num : nums) {
freq[num]++;
if (freq[num] > threshold) {
return num;
}
}
return 0; // 不会走到这里
}
};
5.4 复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 遍历数组一次 |
| 空间 | O(n) | 哈希表存储所有数字 |
六、方法对比总结
| 方法 | 时间复杂度 | 空间复杂度 | 推荐指数 |
|---|---|---|---|
| Boyer-Moore 投票算法 | O(n) | O(1) | ★★★★★ |
| 排序后取中位数 | O(n log n) | O(1) | ★★★☆☆ |
| 哈希表统计 | O(n) | O(n) | ★★☆☆☆ |
为什么投票算法是最优解?
1. 时间 O(n),满足要求
2. 空间 O(1),满足要求
3. 代码简洁
4. 体现数学思维(票数相消)
投票算法的正确性证明:
- 多数元素出现次数 > n/2
- 将数组分成两部分:多数元素部分和其他元素部分
- 多数元素数量 > 其他元素数量
- 两两配对消除后,剩余的一定是多数元素
七、面试追问 FAQ
| 问题 | 回答 |
|---|---|
| Q: 投票算法的正确性如何保证? | 多数元素出现次数 > n/2,每次不同元素相消,最多消除所有非多数元素,剩余的必是多数元素 |
| Q: 投票算法为什么空间是 O(1)? | 只用两个变量 candidate 和 count,不依赖额外数据结构 |
| Q: 如果有多个出现次数 > n/3 的元素呢? | 需要扩展投票算法,维护两个候选人和两个计数器(摩尔投票的扩展) |
| Q: 排序后取中位数的方法有什么局限? | 需要修改原数组(排序),且时间复杂度是 O(n log n) |
| Q: 投票算法和哈希表方法哪个更快? | 理论上相同 O(n),实际投票算法常数更小,且不需要额外空间 |
| Q: 如何证明排序后下标 n/2 一定是多数元素? | 反证法:假设不是,则多数元素出现次数 <= n/2,矛盾 |
八、相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 169. 多数元素 | 简单 | Boyer-Moore 投票算法 |
| 229. 多数元素 II | 中等 | 扩展投票算法(两个候选人) |
| 面试题 17.10. 主要元素 | 简单 | 投票算法变形 |
九、总结
| 要点 | 说明 |
|---|---|
| 核心条件 | 出现次数 > n/2,必存在 |
| 投票算法 | 票数相消,最终候选人就是答案 |
| 排序法 | 多数元素必在中间位置 |
| 时间复杂度 | O(n) |
| 空间复杂度 | O(1) |
| 扩展思考 | 多数元素 II(出现次数 > n/3,需要两个候选人) |