在做LeetCode169.多数元素这道题时,我的第一反应完全想不到“摩尔投票法”这种高效解法,而是先想到了最直观的“哈希表计数”和“排序找中间元素”。相信很多新手和我一样,面对算法题时,会先从“能解决问题”的直观思路入手,再逐步探索更优的解法。今天这篇博客,就以我的思考历程为线索,带大家拆解这道题的三种解法,重点理解从“直观解法”到“最优解法”的进阶过程。
一、题目回顾:多数元素的定义与要求
先明确题目核心,避免理解偏差:
给定一个大小为 n 的数组 nums,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊n/2⌋ 的元素。
假设数组是非空的,并且给定的数组总是存在多数元素。
关键信息提炼:
1. 多数元素的核心特征:出现次数 > 数组长度的一半(比如数组长度5,出现次数≥3;长度6,出现次数≥4);
2. 题目保证数组非空且一定存在多数元素(无需处理“无多数元素”的边界);
3. 核心需求:找到这个多数元素(不要求原地修改,只需返回元素本身)。
举个例子:
输入:nums = [3,2,3] → 输出:3(出现2次,2 > 3/2=1.5);
输入:nums = [2,2,1,1,1,2,2] → 输出:2(出现4次,4 > 7/2=3.5)。
二、我的第一思路:哈希表计数(最直观的解法)
面对“找出现次数最多的元素”这类问题,哈希表是最直观的工具——用键存储元素,用值存储元素出现的次数,遍历数组统计完成后,再找出出现次数最多的元素即可。而这道题中,“多数元素”本身就是出现次数最多的元素(因为它出现次数超过一半,其他元素加起来都没它多),所以用哈希表完全可行。
1. 哈希表解法实现代码
function majorityElement_1(nums: number[]): number {
// 思路:存储元素出现的次数,查找出现最多的元素
const numsLen = nums.length;
const map = new Map();
// 第一步:遍历数组,统计每个元素的出现次数
for (let i = 0; i < numsLen; i++) {
if (map.has(nums[i])) {
// 元素已存在,次数+1
map.set(nums[i], map.get(nums[i]) + 1);
} else {
// 元素首次出现,次数初始化为1
map.set(nums[i], 1);
}
}
// 第二步:遍历数组,找到出现次数最多的元素(即多数元素)
let res = nums[0];
for (let i = 0; i < numsLen; i++) {
if (map.get(nums[i]) > map.get(res)) {
res = nums[i];
}
}
return res;
};
2. 核心逻辑拆解
整个过程分为两步,逻辑非常清晰:
-
统计次数:遍历数组,用Map记录每个元素出现的次数。比如处理nums = [2,2,1,1,1,2,2]时,最终Map会是 {2:4, 1:3};
-
查找最大值:再次遍历数组,对比每个元素在Map中的次数,找到次数最大的元素(即2)。
3. 解法优劣分析
✅ 优点:逻辑直观、容易想到,实现简单,几乎没有边界陷阱(题目已保证有多数元素);
❌ 缺点:空间复杂度O(n)(需要用Map存储所有元素的次数),不是最优解法。
三、进阶思路:排序法(利用多数元素的特性)
在写完哈希表解法后,我开始思考:题目中“多数元素出现次数>⌊n/2⌋”这个条件,有没有什么可以利用的特性?比如排序后,多数元素会出现在什么位置?
仔细一想,还真有!因为多数元素出现次数超过数组长度的一半,所以无论数组是升序还是降序排列,数组的中间位置(索引为⌊n/2⌋)一定是多数元素。比如:
-
nums = [3,2,3] → 排序后 [2,3,3],⌊3/2⌋=1,索引1的元素是3(多数元素);
-
nums = [2,2,1,1,1,2,2] → 排序后 [1,1,1,2,2,2,2],⌊7/2⌋=3,索引3的元素是2(多数元素);
-
nums = [1] → 排序后 [1],⌊1/2⌋=0,索引0的元素是1(多数元素)。
这个特性是排序法的核心,也是它比哈希表更优的关键。
1. 排序法实现代码
function majorityElement_2(nums: number[]): number {
// 思路:多数元素出现次数>⌊n/2⌋,排序后一定在最中间
nums.sort();
return nums[Math.floor(nums.length / 2)];
};
2. 核心逻辑拆解
代码非常简洁,只有两步:
-
对数组进行排序(默认升序);
-
直接返回数组中间位置的元素(索引为Math.floor(nums.length/2))。
3. 解法优劣分析
✅ 优点:代码极简、空间复杂度优化到了O(1)(如果忽略排序算法的空间消耗,题目通常认为排序法的空间复杂度是O(1)或O(logn),取决于排序算法);
❌ 缺点:时间复杂度O(nlogn)(排序算法的时间消耗),比哈希表的O(n)略差。
四、最优解法:摩尔投票法(想不到但超高效)
在看题解之前,我完全想不到还有“摩尔投票法”这种解法——它能做到时间复杂度O(n)、空间复杂度O(1),是这道题的最优解。核心思路非常巧妙,利用“多数元素出现次数超过一半”的特性,通过“抵消”的方式筛选出多数元素。
1. 摩尔投票法核心思想
把多数元素想象成“好人”,其他元素想象成“坏人”,好人的数量比所有坏人加起来还多。我们进行一轮“投票”:
-
初始化一个计数器count=0,和一个候选者res(初始值任意);
-
遍历数组,遇到当前元素:
-
如果count=0,说明之前的“好人”和“坏人”已经全部抵消,把当前元素设为新的候选者res;
-
如果当前元素等于res(是“好人”),count+1(投票支持);
-
如果当前元素不等于res(是“坏人”),count-1(投票反对,抵消一次);
-
-
遍历结束后,res一定是多数元素(因为好人数量比坏人多,永远无法被完全抵消)。
2. 摩尔投票法实现代码
function majorityElement_3(nums: number[]): number {
// 思路:摩尔投票法
let count = 0;
let res = nums[0];
for (let num of nums) {
// 之前的候选者已被抵消,更新候选者为当前元素
if (count === 0) {
res = num;
}
// 相同则计数+1,不同则计数-1(抵消)
if (num !== res) {
count--;
} else {
count++;
}
}
return res;
};
3. 代码执行过程演示(直观理解抵消逻辑)
以nums = [2,2,1,1,1,2,2]为例,一步步看摩尔投票法如何工作:
-
初始状态:count=0,res=2(nums[0]);
-
num=2:count=0 → res=2;num===res → count=1;
-
num=2:num===res → count=2;
-
num=1:num!==res → count=1;
-
num=1:num!==res → count=0;
-
num=1:count=0 → res=1;num===res → count=1;
-
num=2:num!==res → count=0;
-
num=2:count=0 → res=2;num===res → count=1;
-
遍历结束,res=2(正确的多数元素)。
从过程能看出,前4个元素(2,2,1,1)相互抵消,count回到0;后面的元素继续筛选,最终留下的res就是多数元素。
4. 解法优劣分析
✅ 优点:时间复杂度O(n)(仅遍历数组一次),空间复杂度O(1)(无需额外存储),是这道题的最优解;
❌ 缺点:思路不直观、新手很难想到,需要理解“抵消”的核心逻辑,且仅适用于“存在多数元素”的场景(本题已保证,所以适用)。
五、三种解法对比总结
为了帮大家清晰区分三种解法的适用场景和优劣,整理了以下对比表:
| 解法 | 核心思路 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 哈希表计数 | 统计每个元素出现次数,找次数最多的 | O(n) | O(n) | 逻辑直观、容易想到、实现简单 | 需要额外存储空间,不是最优 |
| 排序法 | 利用多数元素特性,排序后中间位置即结果 | O(nlogn) | O(1)(忽略排序空间) | 代码极简、空间优化好 | 排序耗时,时间复杂度略差 |
| 摩尔投票法 | 通过“抵消”逻辑筛选多数元素 | O(n) | O(1) | 时间、空间均最优 | 思路不直观、新手难想到、仅适用于存在多数元素的场景 |
六、新手学习建议
作为新手,我认为不需要一开始就追求“最优解法”,可以按以下步骤学习:
-
先掌握“哈希表计数”:理解“统计次数找最大值”的直观思路,确保能独立写出代码解决问题;
-
再学习“排序法”:培养“利用题目特性优化解法”的思维,比如本题中“多数元素在中间”的特性,这是算法优化的关键;
-
最后理解“摩尔投票法”:可以多模拟几遍执行过程,感受“抵消”的逻辑,记住这种解法的适用场景(存在多数元素),以后遇到类似题目就能举一反三。
算法学习的核心是“从会做到做好”,先保证能解决问题,再逐步优化思路,这样才能稳步提升。
七、总结
LeetCode169.多数元素这道题,三种解法对应了不同的思维层次:
-
哈希表:直观的“暴力统计”思路,适合新手入门;
-
排序法:利用题目特性的“优化思路”,培养特性挖掘能力;
-
摩尔投票法:巧妙的“抵消逻辑”,是最优解但需要深度理解。
对于新手来说,不用因为一开始想不到摩尔投票法而焦虑,重点是先掌握前两种直观解法,再逐步理解最优解的逻辑。通过这道题,我们可以学到:算法题的优化往往源于对题目条件的深度挖掘——“多数元素出现次数>⌊n/2⌋”这个条件,既是排序法的核心,也是摩尔投票法的基础。
最后,祝大家刷题顺利,在“会做”到“做好”的过程中,逐步提升自己的算法思维!