学习记录:寻找超过半数出现的数字
在解决编程问题时,不仅需要设计出高效的算法,还需要深刻理解问题的本质。在这篇学习笔记中,我将详细解析一个算法问题:在数组中找到出现次数超过一半的数字。问题表面简单,但其背后隐藏了数据结构与算法设计的巧妙思想。
一、题目解析
1. 问题描述
小R从班级中抽取了一些同学,每位同学都会给出一个数字。已知在这些数字中,某个数字的出现次数超过了数字总数的一半。现在需要你帮助小R找到这个数字。我们需要从一个数组中找到出现次数超过数组总数一半的元素。题目保证这样的元素一定存在。
2. 样例解析
通过具体的样例理解题目:
- 样例1
输入:[1, 3, 8, 2, 3, 1, 3, 3, 3]
输出:3
数字3出现了 5 次,超过数组长度9的一半。 - 样例2
输入:[5, 5, 5, 1, 2, 5, 5]
输出:5
数字5出现了 5 次,超过数组长度7的一半。 - 样例3
输入:[9, 9, 9, 9, 8, 9, 8, 8]
输出:9
数字9出现了 5 次,超过数组长度8的一半。
3. 问题核心
如果某个元素在数组中出现次数超过一半,我们称其为“多数元素”(Majority Element)。问题的核心是快速找到这个多数元素,并验证其确实符合条件。
二、解题思路
1. 暴力解法
最直观的方式是遍历数组,对每个元素统计出现次数,然后判断是否超过数组长度的一半。这种方法虽然容易实现,但时间复杂度为,在大规模数据场景下表现较差。
2. 排序解法
将数组排序后,多数元素一定会出现在数组的中间位置。因此,可以直接返回排序后的中位数,再验证是否符合条件。这种方法的时间复杂度为,优于暴力解法,但仍然不是最优解。
3. 摩尔投票算法
最优解使用 摩尔投票算法(Boyer-Moore Voting Algorithm),其核心思想是通过计数器记录候选元素,利用“抵消”的机制减少无用计算:
-
初始化一个候选元素
candidate和计数器count; -
遍历数组:
- 如果
count == 0,将当前数字设为候选元素; - 如果当前数字等于候选元素,
count增加; - 如果当前数字不等于候选元素,
count减少;
- 如果
-
遍历结束后,候选元素即为多数元素。
摩尔投票算法的时间复杂度为 ,空间复杂度为 ,是本问题的最佳解法。
三、代码详解
以下是摩尔投票算法的实现:
def solution(array):
# 初始化候选元素和计数器
candidate = None
count = 0
# 第一次遍历,找出候选元素
for num in array:
if count == 0:
candidate = num
count = 1
elif num == candidate:
count += 1
else:
count -= 1
# 验证候选元素的出现次数是否超过一半
count = 0
for num in array:
if num == candidate:
count += 1
if count > len(array) // 2:
return candidate
else:
return None # 理论上不会走到这一步,因为题目保证存在这样的元素
# 测试用例
if __name__ == "__main__":
print(solution([1, 3, 8, 2, 3, 1, 3, 3, 3]) == 3)
print(solution([5, 5, 5, 1, 2, 5, 5]) == 5)
print(solution([9, 9, 9, 9, 8, 9, 8, 8]) == 9)
1. 第一次遍历
通过维护 candidate 和 count 找到潜在的多数元素:
- 当
count为零时,更新候选元素; - 遇到相同元素时增加计数,遇到不同元素时减少计数。
2. 第二次遍历
验证候选元素是否确实是多数元素:
- 再次统计候选元素的出现次数;
- 判断是否超过数组长度的一半。
3. 返回结果
如果候选元素满足条件,返回它;否则返回 None。但由于题目保证多数元素存在,实际上不会返回 None。
四、算法图解
通过图解更直观地理解摩尔投票算法:
假设输入数组为 [1, 3, 8, 2, 3, 1, 3, 3, 3]:
-
遍历数组:
- 初始状态:
candidate = None,count = 0; - 遇到
1:candidate = 1,count = 1; - 遇到
3:count = 0; - 遇到
8:candidate = 8,count = 1; - … 继续遍历,最终
candidate = 3。
- 初始状态:
-
验证候选元素:
- 再次遍历数组,统计
3的出现次数,确认其超过数组长度的一半。
- 再次遍历数组,统计
以示例数组 [1, 3, 8, 2, 3, 1, 3, 3, 3] 为例,图解摩尔投票算法的执行过程:
第一次遍历(确定候选元素):
| 元素 | candidate | count | 说明 |
|---|---|---|---|
| 1 | 1 | 1 | 候选元素初始化为 1 |
| 3 | 1 | 0 | 3 ≠ 1,计数器减 1 |
| 8 | 8 | 1 | 计数器为 0,更新候选元素为 8 |
| 2 | 8 | 0 | 2 ≠ 8,计数器减 1 |
| 3 | 3 | 1 | 计数器为 0,更新候选元素为 3 |
| 1 | 3 | 0 | 1 ≠ 3,计数器减 1 |
| 3 | 3 | 1 | 3 = 3,计数器加 1 |
| 3 | 3 | 2 | 3 = 3,计数器加 1 |
| 3 | 3 | 3 | 3 = 3,计数器加 1 |
最终,候选元素为 3。
第二次遍历(验证候选元素):
统计 3 在数组中的出现次数,发现它出现了 5 次,超过数组长度的一半,验证成功。
五、心得总结
- 算法思想 摩尔投票算法的核心是“抵消”思想,通过对非多数元素进行相互抵消,最终保留多数元素。它展示了如何巧妙利用数学性质优化算法。
- 工程应用 多数元素问题的解决思路不仅适用于竞赛题目,也可以推广到实际场景,例如统计频率最高的日志错误码、分析用户行为数据等。
- 深入反思 摩尔投票算法的局限性在于只能处理保证多数元素存在的情况。如果题目未作保证,还需添加额外的验证步骤。
通过本次学习,我更加深刻地体会到,优秀的算法设计源于对问题本质的透彻理解,以及对时间与空间复杂度的精妙平衡。