【LeetCode】Majority Element 问题详解:从暴力到最优算法
在算法面试和LeetCode练习中,Majority Element(多数元素) 是一道经典题目。它的核心是寻找数组中出现次数超过 ⌊n/2⌋(n为数组长度)的元素。这个问题不仅考察对基础算法的掌握,更能体现你对时间/空间复杂度 trade-off 的理解。
比如,在投票系统中,我们需要快速找出获得超过半数选票的候选人;在数据挖掘中,多数元素可能代表最常见的异常值或趋势。掌握这个问题的解法,能帮你举一反三解决类似的“频率统计”问题。
目录#
- 问题描述
- 解法1:暴力枚举(Brute Force)
- 解法2:哈希表计数(Hash Map)
- 解法3:排序(Sorting)
- 解法4:博伊尔-摩尔多数投票算法(Boyer-Moore Majority Vote Algorithm)
- 解法对比与选择
- 常见陷阱与最佳实践
- 示例演练
- 结论
- 参考资料
问题描述#
LeetCode原题链接:169. Majority Element
题目要求:
给定一个大小为 n 的数组 nums,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊n/2⌋ 的元素。
说明:题目保证数组中一定存在多数元素。
解法1:暴力枚举(Brute Force)#
思路#
暴力枚举的核心是逐个检查每个元素的出现次数:
- 遍历数组中的每个元素
num; - 再次遍历数组,统计
num的出现次数; - 如果次数超过
n/2,直接返回num。
代码实现#
def majorityElement(nums):
n = len(nums)
half = n // 2 # 多数元素需要超过这个值
for num in nums:
count = 0
for elem in nums:
if elem == num:
count += 1
# 提前终止内层循环(优化)
if count > half:
break
if count > half:
return num复杂度分析#
- 时间复杂度:O(n²)。最坏情况下,每个元素都要遍历整个数组(比如数组是
[1,2,1,2,...,1],最后一个元素是多数)。 - 空间复杂度:O(1)。不需要额外空间。
优缺点#
- 优点:逻辑简单,无需额外空间。
- 缺点:效率极低,无法处理大规模数据(比如n=1e4时,需要1e8次运算)。
解法2:哈希表计数(Hash Map)#
思路#
哈希表的核心是用空间换时间:
- 用哈希表(字典)记录每个元素的出现次数;
- 遍历数组时更新哈希表,提前终止(当某元素的次数超过
n/2时直接返回)。
代码实现(两种方式)#
方式1:用 collections.Counter 简化代码#
from collections import Counter
def majorityElement(nums):
count = Counter(nums)
# 取出现次数最多的元素(题目保证存在)
return max(count, key=count.get)方式2:手动维护哈希表(支持提前退出)#
def majorityElement(nums):
count = {}
half = len(nums) // 2
for num in nums:
count[num] = count.get(num, 0) + 1
# 提前退出:一旦超过半数,直接返回
if count[num] > half:
return num复杂度分析#
- 时间复杂度:O(n)。最坏情况遍历整个数组(多数元素在最后一位),但平均情况更快(提前退出)。
- 空间复杂度:O(n)。哈希表最多存储所有不同的元素(极端情况如所有元素唯一,但题目保证存在多数,所以实际存储量远小于n)。
优缺点#
- 优点:代码简洁,时间效率高。
- 缺点:需要额外空间存储哈希表,对内存敏感的场景不友好。
解法3:排序(Sorting)#
思路#
排序的核心是利用多数元素的位置特性:
如果数组已排序,多数元素必然出现在 ⌊n/2⌋ 的位置(无论n是奇数还是偶数)。
比如:
- n=7(奇数):排序后第3位(0-based)是多数元素(需要至少4次出现);
- n=6(偶数):排序后第3位(0-based)是多数元素(需要至少4次出现)。
代码实现#
def majorityElement(nums):
nums.sort()
return nums[len(nums) // 2]复杂度分析#
- 时间复杂度:O(n log n)。排序的时间复杂度由排序算法决定(Python的
list.sort()是Timsort,平均O(n log n))。 - 空间复杂度:O(1)(如果允许原地排序)或 O(n)(如果需要额外空间存储排序后的数组)。
优缺点#
- 优点:代码极简,无需额外空间(原地排序)。
- 缺点:时间复杂度高于哈希表和Boyer-Moore算法。
解法4:博伊尔-摩尔多数投票算法(Boyer-Moore Majority Vote Algorithm)#
引言#
这是最优解法,由Robert S. Boyer和J Strother Moore在1981年提出。它的核心优势是O(n)时间 + O(1)空间,完美解决了时间和空间的权衡问题。
算法思路#
算法维护两个变量:
candidate:当前候选的多数元素(初始为None);count:候选元素的“支持度”(初始为0)。
遍历数组时,遵循以下规则:
- 如果
count == 0:将当前元素设为新的candidate; - 如果当前元素等于
candidate:count += 1(支持度增加); - 否则:
count -= 1(支持度减少)。
遍历结束后,candidate 就是多数元素。
关键直觉#
多数元素的出现次数超过所有其他元素的总和。因此,即使我们用“抵消”的方式(每遇到一个非候选元素就减少支持度),多数元素的支持度最终也会保持为正。
举个例子:nums = [2,2,1,1,1,2,2]
- 候选元素2的支持度会被1抵消,但由于2的数量更多,最终候选元素仍为2。
代码实现#
def majorityElement(nums):
candidate = None
count = 0
for num in nums:
if count == 0:
candidate = num # 更换候选
count += 1 if num == candidate else -1
return candidate复杂度分析#
- 时间复杂度:O(n)。仅需遍历一次数组。
- 空间复杂度:O(1)。仅用两个变量,完全不需要额外空间。
优缺点#
- 优点:最优的时间和空间复杂度,适用于所有场景(尤其是内存受限的情况)。
- 缺点:需要理解算法的核心逻辑(否则容易写错)。
解法对比与选择#
为了帮助你快速选择合适的解法,我们整理了各解法的关键指标:
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力枚举 | O(n²) | O(1) | 小数据量,无需额外空间 |
| 哈希表(Counter) | O(n) | O(n) | 数据量中等,追求代码简洁 |
| 哈希表(提前退出) | O(n) | O(n) | 数据量较大,多数元素出现早 |
| 排序 | O(n log n) | O(1) | 无需额外空间,允许O(n log n)时间 |
| Boyer-Moore算法 | O(n) | O(1) | 所有场景(最优) |
常见陷阱与最佳实践#
常见陷阱#
-
排序时的索引错误:
误用len(nums)//2 - 1代替len(nums)//2。例如,nums = [2,2,1,1,1,2,2](len=7),len//2=3是正确索引,而len//2-1=2会返回1(错误)。 -
忘记提前退出:
在哈希表解法中,没有提前终止循环,导致不必要的计算(比如多数元素在数组前半部分时,仍遍历整个数组)。 -
不理解Boyer-Moore的逻辑:
随意修改算法逻辑(比如将count的初始值设为1),导致错误。例如,nums = [1]时,初始count=1会正确返回1,但nums = [2,1,2]时会出错。
最佳实践#
-
优先选择Boyer-Moore算法:
它是最优解法,适用于所有场景,尤其是内存受限的情况(比如嵌入式系统)。 -
测试边缘情况:
- 单元素数组(
nums = [5]); - 所有元素相同(
nums = [7,7,7]); - 多数元素在数组末尾(
nums = [1,2,3,4,5,5,5]); - 交替元素(
nums = [2,1,2,1,2])。
- 单元素数组(
-
理解算法的本质:
不要死记硬背代码,要理解每种解法的逻辑。比如,Boyer-Moore的核心是“抵消”,哈希表的核心是“空间换时间”。
示例演练#
我们用 nums = [2,2,1,1,1,2,2] 演示所有解法的执行过程:
1. 暴力枚举#
- 遍历元素2:统计出现次数为4(>3.5),直接返回2。
2. 哈希表(提前退出)#
- 遍历2→count=1(未超过3.5);
- 遍历2→count=2(未超过);
- 遍历1→count=1(未超过);
- 遍历1→count=2(未超过);
- 遍历1→count=3(未超过);
- 遍历2→count=3(未超过);
- 遍历2→count=4(超过3.5),返回2。
3. 排序#
- 排序后的数组:
[1,1,1,2,2,2,2]; len//2=3,返回索引3的元素2。
4. Boyer-Moore算法#
| 步骤 | 当前元素 | candidate | count | 说明 |
|---|---|---|---|---|
| 1 | 2 | 2 | 1 | count=0,设为候选 |
| 2 | 2 | 2 | 2 | 等于候选,count+1 |
| 3 | 1 | 2 | 1 | 不等于,count-1 |
| 4 | 1 | 2 | 0 | 不等于,count-1 |
| 5 | 1 | 1 | 1 | count=0,更换候选为1 |
| 6 | 2 | 1 | 0 | 不等于,count-1 |
| 7 | 2 | 2 | 1 | count=0,更换候选为2 |
遍历结束,candidate=2(正确)。
结论#
Majority Element问题的解法覆盖了算法设计的常见思路:暴力枚举、空间换时间、利用排序特性、贪心算法(Boyer-Moore)。其中,Boyer-Moore算法是最优解法,它的时间复杂度O(n)和空间复杂度O(1)使其适用于所有场景。
在面试中,面试官不仅考察你能否写出正确的代码,更看重你对算法的理解。比如,当被问到“为什么Boyer-Moore算法有效?”时,你可以解释:“多数元素的出现次数超过所有其他元素的总和,因此抵消后仍会保留。”
最后,建议你多练习类似的问题(比如LeetCode 229. Majority Element II),加深对频率统计问题的理解。
参考资料#
- LeetCode原题:169. Majority Element
- Boyer-Moore算法维基百科:Boyer–Moore majority vote algorithm
- GeekforGeeks:Majority Element
- Python官方文档:collections.Counter