寻找超过半数出现的数字题解 | 豆包MarsCode AI刷题

58 阅读5分钟

学习记录:寻找超过半数出现的数字

在解决编程问题时,不仅需要设计出高效的算法,还需要深刻理解问题的本质。在这篇学习笔记中,我将详细解析一个算法问题:在数组中找到出现次数超过一半的数字。问题表面简单,但其背后隐藏了数据结构与算法设计的巧妙思想。


一、题目解析

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. 暴力解法

最直观的方式是遍历数组,对每个元素统计出现次数,然后判断是否超过数组长度的一半。这种方法虽然容易实现,但时间复杂度为O(n2)O(n^2),在大规模数据场景下表现较差。

2. 排序解法

将数组排序后,多数元素一定会出现在数组的中间位置。因此,可以直接返回排序后的中位数,再验证是否符合条件。这种方法的时间复杂度为O(nlogn)O(n \log n),优于暴力解法,但仍然不是最优解。

3. 摩尔投票算法

最优解使用 摩尔投票算法(Boyer-Moore Voting Algorithm),其核心思想是通过计数器记录候选元素,利用“抵消”的机制减少无用计算:

  • 初始化一个候选元素 candidate 和计数器 count

  • 遍历数组:

    • 如果 count == 0,将当前数字设为候选元素;
    • 如果当前数字等于候选元素,count 增加;
    • 如果当前数字不等于候选元素,count 减少;
  • 遍历结束后,候选元素即为多数元素。

摩尔投票算法的时间复杂度为 O(n)O(n),空间复杂度为 O(1)O(1),是本问题的最佳解法。


三、代码详解

以下是摩尔投票算法的实现:

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. 第一次遍历

通过维护 candidatecount 找到潜在的多数元素:

  • count 为零时,更新候选元素;
  • 遇到相同元素时增加计数,遇到不同元素时减少计数。

2. 第二次遍历

验证候选元素是否确实是多数元素:

  • 再次统计候选元素的出现次数;
  • 判断是否超过数组长度的一半。

3. 返回结果

如果候选元素满足条件,返回它;否则返回 None。但由于题目保证多数元素存在,实际上不会返回 None


四、算法图解

通过图解更直观地理解摩尔投票算法:

假设输入数组为 [1, 3, 8, 2, 3, 1, 3, 3, 3]

  1. 遍历数组:

    • 初始状态:candidate = Nonecount = 0
    • 遇到 1candidate = 1count = 1
    • 遇到 3count = 0
    • 遇到 8candidate = 8count = 1
    • … 继续遍历,最终 candidate = 3
  2. 验证候选元素:

    • 再次遍历数组,统计 3 的出现次数,确认其超过数组长度的一半。

以示例数组 [1, 3, 8, 2, 3, 1, 3, 3, 3] 为例,图解摩尔投票算法的执行过程:

第一次遍历(确定候选元素):
元素candidatecount说明
111候选元素初始化为 1
3103 ≠ 1,计数器减 1
881计数器为 0,更新候选元素为 8
2802 ≠ 8,计数器减 1
331计数器为 0,更新候选元素为 3
1301 ≠ 3,计数器减 1
3313 = 3,计数器加 1
3323 = 3,计数器加 1
3333 = 3,计数器加 1

最终,候选元素为 3

第二次遍历(验证候选元素):

统计 3 在数组中的出现次数,发现它出现了 5 次,超过数组长度的一半,验证成功。

五、心得总结

  1. 算法思想 摩尔投票算法的核心是“抵消”思想,通过对非多数元素进行相互抵消,最终保留多数元素。它展示了如何巧妙利用数学性质优化算法。
  2. 工程应用 多数元素问题的解决思路不仅适用于竞赛题目,也可以推广到实际场景,例如统计频率最高的日志错误码、分析用户行为数据等。
  3. 深入反思 摩尔投票算法的局限性在于只能处理保证多数元素存在的情况。如果题目未作保证,还需添加额外的验证步骤。

通过本次学习,我更加深刻地体会到,优秀的算法设计源于对问题本质的透彻理解,以及对时间与空间复杂度的精妙平衡。