问题描述
给定一个大小为 n 的数组 nums,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
1 | 输入:[3,2,3] |
示例 2:
1 | 输入:[2,2,1,1,1,2,2] |
约束条件:
n == nums.length
1 <= n <= 5 * 10^4
-10^9 <= nums[i] <= 10^9
- 题目数据保证
nums
中存在多数元素
解题思路
针对多数元素问题,我们有多种解决方案。下面我们将详细介绍四种常用的解法:
方法一:哈希表计数法
核心思想:使用哈希表记录每个元素出现的次数,找出出现次数大于 n/2 的元素。
这是一种直观的解法,通过两次遍历实现:
- 第一次遍历:统计每个元素的出现次数
- 第二次遍历:找出出现次数大于 n/2 的元素
方法二:排序法
核心思想:如果将数组排序,由于多数元素出现次数大于 n/2,所以排序后中间位置的元素一定是多数元素。
这是一种简单但效率不高的解法:
- 对数组进行排序
- 返回中间位置的元素(下标为 n/2)
方法三:分治法
核心思想:将数组分为左右两部分,分别找出左右两部分的多数元素,然后确定整个数组的多数元素。
分治法的具体步骤:
- 将数组分成左右两部分
- 递归地找出左半部分和右半部分的多数元素
- 比较这两个多数元素在整个数组中的出现次数,返回出现次数较多的那个
方法四:摩尔投票法
核心思想:利用"不同元素相互抵消"的思想,找出最终留下的元素。
想象一下有两支军队在打仗:
- 红军(代表多数元素)
- 蓝军(代表所有其他元素)
由于多数元素出现次数超过总数的一半,所以红军人数必然多于蓝军。即使红蓝双方一对一厮杀(相互抵消),最后战场上剩下的一定是红军!
这就是摩尔投票算法的核心思想:不同的数字相互抵消,最后剩下的一定是出现次数最多的那个数字。
算法步骤(超简单版)
- 找一个"候选人",先认为第一个数字是候选人
- 开始计数:遇到和候选人一样的数字就+1,遇到不一样的就-1
- 如果计数变成0了,就换一个候选人(选当前遇到的那个数字)
- 继续上面的过程直到结束
- 最后的候选人就是我们要找的多数元素
实现细节与代码
方法一:哈希表计数法
1 | func majorityElement(nums []int) int { |
示例演示:以 [2,2,1,1,1,2,2]
为例
- 初始化
countMap = {}
- 遍历数组并统计:
- 遇到 2:
countMap[2] = 1
- 遇到 2:
countMap[2] = 2
- 遇到 1:
countMap[1] = 1
- 遇到 1:
countMap[1] = 2
- 遇到 1:
countMap[1] = 3
- 遇到 2:
countMap[2] = 3
- 遇到 2:
countMap[2] = 4
- 遇到 2:
- 最终
countMap = {1: 3, 2: 4}
,阈值threshold = 7/2 = 3
- 检查
countMap
中元素,发现 2 的出现次数为 4,大于阈值 3 - 返回 2 作为多数元素
方法二:排序法
1 | import "sort" |
示例演示:以 [2,2,1,1,1,2,2]
为例
- 排序后数组变为
[1,1,1,2,2,2,2]
- 中间位置是
7/2 = 3
,对应的元素是nums[3] = 2
- 返回 2 作为多数元素
方法三:分治法
1 | func majorityElement(nums []int) int { |
示例演示:对于复杂的分治算法,我们以一个简化的例子 [2,2,1,1,1,2,2]
展示部分步骤:
- 将数组分为两部分:
[2,2,1]
和[1,1,2,2]
- 递归地处理左半部分:进一步分为
[2]
和[2,1]
,最终得到左半部分的多数元素 2 - 递归地处理右半部分:进一步分为
[1,1]
和[2,2]
,最终得到右半部分的多数元素没有(因为 1 和 2 的出现次数相同) - 比较左右两部分的多数元素在整个数组中的出现次数:2 出现了 4 次,1 出现了 3 次
- 返回 2 作为整个数组的多数元素
方法四:摩尔投票法
1 | func majorityElement(nums []int) int { |
示例演示:让我们用数组 [2,2,1,1,1,2,2]
来一步步看看上面的算法是怎么工作的:
- 初始状态:
- 候选人 = 2(第一个元素)
- 票数 = 1
- 遍历第2个元素(也是2):
- 发现和候选人相同
- 票数 +1 = 2
- 遍历第3个元素(是1):
- 发现和候选人不同
- 票数 -1 = 1
- 遍历第4个元素(是1):
- 发现和候选人不同
- 票数 -1 = 0
- 遍历第5个元素(是1):
- 票数为0,所以换新候选人
- 候选人 = 1
- 票数 = 1
- 遍历第6个元素(是2):
- 发现和候选人不同
- 票数 -1 = 0
- 遍历第7个元素(是2):
- 票数为0,所以换新候选人
- 候选人 = 2
- 票数 = 1
- 遍历结束,最终候选人是2,这就是我们要的答案!
用表格表示更清晰:
当前位置 | 当前数字 | 候选人 | 票数 | 操作说明 |
---|---|---|---|---|
开始 | - | 2 | 1 | 初始状态,选第一个数为候选人 |
位置1 | 2 | 2 | 2 | 遇到相同数字,票数+1 |
位置2 | 1 | 2 | 1 | 遇到不同数字,票数-1 |
位置3 | 1 | 2 | 0 | 遇到不同数字,票数-1 |
位置4 | 1 | 1 | 1 | 票数变为0,换新候选人 |
位置5 | 2 | 1 | 0 | 遇到不同数字,票数-1 |
位置6 | 2 | 2 | 1 | 票数变为0,换新候选人 |
为什么这个算法有效?
因为多数元素出现次数超过了数组长度的一半,所以即使它和其他所有元素一一"对抗"(相互抵消),最后剩下的也必定是它。
就像两支军队打仗:
- 红军(多数元素)有100人
- 蓝军(其他所有元素)合起来最多只有99人
- 即使红蓝双方一对一厮杀,最终也会剩下至少1个红军
无论如何变换候选人,最终留下的一定是出现次数最多的那个元素。这就是摩尔投票算法的巧妙之处!
方法比较
方面 | 哈希表计数法 | 排序法 | 分治法 | 摩尔投票法 |
---|---|---|---|---|
时间复杂度 | O(n) | O(n log n) | O(n log n) | O(n) |
空间复杂度 | O(n) | O(1) 或 O(n) | O(log n) | O(1) |
优点 | 直观易懂 | 实现简单 | 可以扩展到其他问题 | 最高效 |
缺点 | 需要额外空间 | 速度较慢 | 实现复杂 | 不直观 |
推荐度 | ★★★★☆ | ★★★☆☆ | ★★★☆☆ | ★★★★★ |
复杂度分析
哈希表计数法
- 时间复杂度:$O(n)$,需要遍历数组两次
- 空间复杂度:$O(n)$,最坏情况下哈希表需要存储 $n/2+1$ 个不同的元素
排序法
- 时间复杂度:$O(n \log n)$,主要是排序的时间复杂度
- 空间复杂度:$O(1)$ 或 $O(n)$,取决于使用的排序算法(原地排序或非原地排序)
分治法
- 时间复杂度:$O(n \log n)$
递归树的深度为 $\log n$,每层的总操作次数为 $O(n)$,因此总时间复杂度为 $O(n \log n)$ - 空间复杂度:$O(\log n)$,递归调用栈的深度
摩尔投票法
- 时间复杂度:$O(n)$,只需要遍历数组一次
- 空间复杂度:$O(1)$,只使用了常数个变量
关键收获
-
多角度解决问题:同一个问题可以有多种解法,每种解法都有其优缺点
-
算法效率的权衡:
- 哈希表方法直观但需要额外空间
- 排序方法简单但效率不高
- 分治法思想优雅但实现复杂
- 摩尔投票法高效但不够直观
-
摩尔投票算法的巧妙之处:
- 利用"不同元素相互抵消"的思想解决问题
- 不需要额外空间,一次遍历即可得到结果
- 是解决"多数元素"问题的最佳算法
-
问题的扩展思考:
- 如果不保证存在多数元素,需要增加验证步骤
- 可以扩展到寻找出现次数大于 n/3 的元素(LeetCode 229)
- 可以用于解决其他计数相关的问题
相关题目
- LeetCode 229: 求众数 II(找出所有出现次数超过 n/3 的元素)
- LeetCode 1150: 检查一个数是否在数组中占绝大多数