本题目为LeetCode第169题,仅分享我的做题经验,无商业用途!
题目描述
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3] 输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2] 输出:2
提示:
n == nums.length
1 <= n <= 5 * 104
-109 <= nums[i] <= 109
进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
解决这个问题有很多种思路,该题仅作为摩根投票法示范
一、问题引入:为什么需要摩根投票法?
常规思路的局限性
看到 “统计元素出现次数”,很多人第一反应是用哈希表计数:遍历数组记录每个元素的次数,再找到次数超过一半的元素,包括我第一次也是想用哈希表。这种方法时间复杂度是 O (n),但空间复杂度是 O (n)—— 如果数组规模达到 10^5 甚至更大,额外的空间开销会成为负担(多为实际应用时)。这时候我们也许会思考,有没有什么更优的方法来解决这个问题——这时候可以引进摩根投票法
二、摩根投票法:原理剖析
摩根投票法的核心,是利用多数元素的 **“数量绝对优势”**—— 它的出现次数比数组中所有其他元素的出现次数之和还要多。这种优势决定了:无论其他元素如何 “抵消”,多数元素最终一定会 “存活”。
1. 核心逻辑:抵消与候选
算法通过两个变量实现:
candidate
:当前候选的 “多数元素”;count
:候选元素的 “当前计数”,用于记录候选元素与其他元素的抵消状态。
具体步骤如下:
- 初始化:将
candidate
设为数组第一个元素,count
设为 1; - 遍历数组:
- 若当前元素与
candidate
相同:count++
(同元素,增强候选优势); - 若当前元素与
candidate
不同:count--
(不同元素,相互抵消); - 若
count
减至 0:说明当前候选已被完全抵消,将candidate
更新为当前元素,重置count = 1
;
- 若当前元素与
- 结果:遍历结束后,
candidate
就是多数元素。
2. 实例演示:直观理解抵消过程
以数组 nums = [2,2,1,1,1,2,2]
为例,一步步看算法如何工作:
遍历元素 | candidate | count | 操作说明 |
---|---|---|---|
初始 | 2 | 1 | 从第一个元素开始初始化 |
2 | 2 | 2 | 同候选,count+1 |
1 | 2 | 1 | 不同,count-1(抵消 1 次) |
1 | 2 | 0 | 不同,count-1(候选被抵消完) |
1 | 1 | 1 | count=0,更换候选为 1 |
2 | 1 | 0 | 不同,count-1(新候选被抵消) |
2 | 2 | 1 | count=0,更换候选为 2 |
最终 candidate = 2
,正是数组的多数元素,与预期结果一致。
三、代码实现:
基于上述逻辑,C++ 代码实现仅需十几行,且空间复杂度严格 O (1):
#include <vector>
using namespace std;
class Solution {
public:
int majorityElement(vector<int>& nums) {
// 初始化候选元素和计数
int candidate = nums[0];
int count = 1;
// 遍历数组,动态调整候选和计数
for (int i = 1; i < nums.size(); i++) {
if (nums[i] == candidate) {
count++;
} else {
count--;
// 候选被抵消完,更换候选
if (count == 0) {
candidate = nums[i];
count = 1;
}
}
}
return candidate;
}
};
代码解析
- 无需额外容器存储计数,仅用
candidate
和count
两个变量; - 一次遍历即可完成,时间复杂度 O (n),适配 10^4 甚至 10^5 规模的数组;
- 逻辑鲁棒:即使数组前半段非多数元素占优,后半段多数元素仍能凭借数量优势 “翻盘”。 (重点)
四、实际应用场景
摩根投票法在实际工程中也有广泛应用,尤其适合 “数据量大、空间敏感” 的场景:
1. 数据统计与分析
- 用户投票统计:快速定位得票超过一半的候选人(无需存储所有候选人的票数);
- 日志异常检测:从海量服务器日志中,找出出现次数超过一半的错误代码(实时处理,节省内存);
- 传感器数据处理:识别数据流中占比超过 50% 的异常值(如温度传感器的故障读数)。
2. 分布式系统 Leader 选举
在分布式集群(如 MySQL 主从、ZooKeeper)中,Leader 节点需要获得过半数节点的支持。摩根投票法的思想可用于简化选举过程:无需记录所有节点的投票,通过动态抵消和候选更换,快速选出过半数支持的 Leader,降低通信和存储成本。
3. 数据校验与去重
- 问卷数据校验:验证是否有超过一半的受访者选择同一选项;
- 重复数据清洗:在大量重复日志中,快速定位占比超过一半的 “核心数据”(如某用户 ID 出现次数过半,可直接作为主标识)。
五、注意事项:适用边界
摩根投票法虽高效,但并非万能,它有一个严格的适用前提:数组中必须存在 “多数元素”(出现次数 > ⌊n/2⌋)。如果问题中没有这种 “绝对多数” 元素(如找出现次数最多的元素,不一定过半数),则需要改用哈希表等其他方法。
例如:若数组为 [1,2,3,2,2,3]
,没有元素出现超过 3 次(n=6,⌊n/2⌋=3),摩根投票法会返回 3,但 3 并非 “多数元素”—— 因此使用前需确认场景是否符合前提。
六、总结:
摩根投票法的精妙之处,在于它没有 “死磕”“统计次数” 的常规思路,而是从 “多数元素的本质特性” 出发,用 “抵消” 的思想简化问题。这种 “抓核心矛盾” 的思维,在算法设计中尤为重要:
- 遇到问题时,先思考 “问题的核心特性是什么”(如多数元素的数量优势)
- 再思考 “如何利用特性减少资源消耗”(如用抵消代替计数,降低空间复杂度)
当然,其实在刷题的过程中做出来题目才是最重要的,在用该方法解决问题之前先试试用自己所掌握的知识来解答这个问题,然后再尝试优化,或者用更优的方法解决这个问题。不是解决难题才算成功,一天一个简单的题目也可以增强成就感以及编程的乐趣哦!
到此,本文就结束了,祝阅读愉快^_^
本文仅为个人拙见,不一定全部正确,甚至可能会有讲错的地方。
欢迎评论区分享!!!