【C++】寻找数组中出现次数超过一半的数字:三种解法深度剖析

一、问题描述

题目要求

给定一个长度为 n 的整数数组 numbers,请找出数组中出现的次数超过数组长度一半的数字。如果不存在则返回 0。

题目约束

  • 1 ≤ n ≤ 10000

  • 数组中的值 0 ≤ val ≤ 10000

  • 要求时间复杂度 O(n),空间复杂度 O(1)

示例

text

复制代码
输入: [1,2,3,2,2,2,5,4,2]
输出: 2

输入: [1,2,3,4,5]
输出: 0

二、三种解法对比分析

方法 时间复杂度 空间复杂度 优点 缺点 适用场景
暴力统计法 O(n²) O(1) 简单直观 效率极低 仅学习用
排序法 O(nlogn) O(1)或O(n) 实现简单 不符合要求,修改原数组 简单场景
摩尔投票法 O(n) O(1) 最优解 需要理解原理 面试推荐

三、解法一:暴力统计法

代码实现

cpp

复制代码
class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int>& numbers) {
        for(size_t i = 0; i < numbers.size(); i++) {
            int count = 1;
            int it = numbers[i];
            for(size_t j = 0; j < numbers.size(); j++) {
                if(j != i && numbers[j] == it) {
                    ++count;
                }
            }
            if(count > numbers.size() / 2) {
                return it;
            }
        }
        return 0;
    }
};

算法分析

1. 核心思路
  • 遍历数组中的每个元素

  • 对于每个元素,再次遍历整个数组统计其出现次数

  • 如果某个元素的出现次数超过数组长度一半,返回该元素

  • 如果遍历完没有找到,返回 0

2. 时间复杂度
  • 外层循环: n 次

  • 内层循环: n 次

  • 总复杂度: O(n²)

3. 示例演示

text

复制代码
输入: [1,2,2,2,3]
数组长度: 5, 需要超过 2.5,即至少 3 次

i=0, num=1, count=1 → 不满足
i=1, num=2, count=3 → 满足,返回2
4. 存在的问题

cpp

复制代码
// 问题1:重复统计
// 对于相同的元素会重复统计多次
// 例如有两个相同的数字2,每个2都会被单独统计

// 问题2:效率极低
// 10000个元素的数组需要约1亿次比较

// 问题3:边界情况处理不完善
// 没有考虑空数组的情况

5. 优化建议

cpp

复制代码
// 可以添加一些优化,但时间复杂度仍然是O(n²)

class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int>& numbers) {
        if (numbers.empty()) return 0;
        
        // 使用哈希集合记录已统计过的元素
        unordered_set<int> visited;
        
        for (size_t i = 0; i < numbers.size(); i++) {
            // 如果当前元素已经统计过,跳过
            if (visited.find(numbers[i]) != visited.end()) {
                continue;
            }
            
            int count = 1;
            for (size_t j = i + 1; j < numbers.size(); j++) {
                if (numbers[j] == numbers[i]) {
                    ++count;
                }
            }
            
            visited.insert(numbers[i]);  // 标记为已统计
            
            if (count > numbers.size() / 2) {
                return numbers[i];
            }
        }
        
        return 0;
    }
};

四、解法二:排序法

代码实现

cpp

复制代码
class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int>& numbers) {
        sort(numbers.begin(), numbers.end());
        return numbers[numbers.size() / 2];
    }
};

算法分析

1. 核心思想
  • 将数组排序

  • 如果存在出现次数超过一半的数字,那么排序后这个数字一定在中间位置

  • 检查中间位置的数字是否真的出现超过一半

2. 完整实现(需要验证)

cpp

复制代码
class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int>& numbers) {
        if (numbers.empty()) return 0;
        
        sort(numbers.begin(), numbers.end());
        
        int candidate = numbers[numbers.size() / 2];
        int count = 0;
        
        // 验证候选数字是否真的超过一半
        for (int num : numbers) {
            if (num == candidate) {
                ++count;
            }
        }
        
        return count > numbers.size() / 2 ? candidate : 0;
    }
};
3. 时间复杂度
  • 排序: O(nlogn)

  • 验证: O(n)

  • 总复杂度: O(nlogn)

4. 数学原理

假设数组中存在一个数字 x 出现超过一半:

  • 排序后,数组被分为三部分:小于 x 的部分,等于 x 的部分,大于 x 的部分

  • 因为 x 的数量超过一半,所以等于 x 的部分至少占据数组中间位置

text

复制代码
示例: [1,2,2,2,3,4,2] → 排序后: [1,2,2,2,2,3,4]
长度7,中间位置是索引3,值为2
5. 优缺点分析

优点:

  • 代码极其简洁

  • 容易理解和实现

缺点:

  • 改变了原数组(可能需要备份)

  • 时间复杂度不是最优的

  • 需要额外验证步骤(不能直接返回中间值)

五、解法三:摩尔投票法(最优解)

代码实现

cpp

复制代码
class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int>& numbers) {
        if (numbers.empty()) return 0;
        
        int candidate = 0;  // 候选数字
        int votes = 0;      // 票数
        
        // 第一轮:找出候选数字
        for (int num : numbers) {
            if (votes == 0) {
                candidate = num;
                votes = 1;
            } else if (num == candidate) {
                ++votes;
            } else {
                --votes;
            }
        }
        
        // 第二轮:验证候选数字是否超过一半
        int count = 0;
        for (int num : numbers) {
            if (num == candidate) {
                ++count;
            }
        }
        
        return count > numbers.size() / 2 ? candidate : 0;
    }
};

算法分析

1. 核心思想
  • 将数组中的数字看作不同候选人的选票

  • 遍历数组,维护一个候选数字和票数

  • 遇到相同数字则票数加1,遇到不同数字则票数减1

  • 票数为0时更换候选数字

2. 详细过程

text

复制代码
示例: [1,2,3,2,2,2,5,4,2]

初始化: candidate = 0, votes = 0

步骤1: num=1, votes=0 → candidate=1, votes=1
步骤2: num=2, votes=1, num≠candidate → votes=0
步骤3: num=3, votes=0 → candidate=3, votes=1
步骤4: num=2, votes=1, num≠candidate → votes=0
步骤5: num=2, votes=0 → candidate=2, votes=1
步骤6: num=2, votes=1, num=candidate → votes=2
步骤7: num=5, votes=2, num≠candidate → votes=1
步骤8: num=4, votes=1, num≠candidate → votes=0
步骤9: num=2, votes=0 → candidate=2, votes=1

候选数字: 2
验证: 2出现5次 > 9/2=4.5 → 返回2
3. 数学证明

原理:

  • 如果存在一个数字出现次数超过一半

  • 那么它与其他数字一一抵消后,最后剩下的必然是它

证明 :

设数组长度为 n,目标数字 x 出现次数为 m > n/2

  • 每次抵消消除一个 x 和一个非 x

  • 最多可以消除 n/2 对(因为 m > n/2,所以 x 不会被完全消除)

  • 最终剩下的必然是 x

4. 时间和空间复杂度
  • 第一轮遍历: O(n)

  • 第二轮验证: O(n)

  • 总时间复杂度: O(n)

  • 空间复杂度: O(1)(只使用了常数个变量)

六、算法可视化对比

性能对比图表

text

复制代码
n = 10000 时的操作次数对比:

暴力法: n² = 100,000,000 次操作
排序法: n log n ≈ 10000 × 13.3 ≈ 133,000 次操作
摩尔投票法: 2n = 20,000 次操作

效率提升: 暴力法 : 排序法 : 摩尔投票法 ≈ 5000 : 6.7 : 1

内存使用对比

cpp

复制代码
// 暴力法: 无额外内存
int count;  // 4字节

// 排序法: 
// 如果使用快速排序,递归调用栈 O(logn)
// 如果使用堆排序,额外空间 O(1)

// 摩尔投票法:
int candidate;  // 4字节
int votes;      // 4字节
int count;      // 4字节

七、边界情况与错误处理

1. 空数组处理

cpp

复制代码
if (numbers.empty()) return 0;

2. 只有一个元素的数组

cpp

复制代码
// 直接返回该元素
if (numbers.size() == 1) return numbers[0];

3. 不存在超过一半的数字

cpp

复制代码
// 必须进行第二轮验证
// 摩尔投票法找到的候选数字不一定真的超过一半
int count = 0;
for (int num : numbers) {
    if (num == candidate) ++count;
}
return count > numbers.size() / 2 ? candidate : 0;

4. 大数测试

cpp

复制代码
// 测试用例
vector<int> test1(10000, 5);  // 全是5,应该返回5
vector<int> test2;             // 空数组,应该返回0
vector<int> test3 = {1,2,3};   // 没有超过一半的,应该返回0
vector<int> test4 = {1,2,2};   // 2出现2次,3个元素需要>1.5,返回2

八、实际应用场景

1. 选举系统

  • 快速找出得票过半的候选人

  • 实时票数统计

2. 数据分析

  • 寻找主要影响因素

  • 异常值检测

3. 系统监控

  • 识别频繁出现的错误类型

  • 故障根因分析

4. 网络协议

  • 数据包验证

  • 一致性检查

九、扩展思考

1. 如果要求返回所有出现次数超过 n/k 的元素?

cpp

复制代码
// 扩展摩尔投票法,维护 k-1 个候选
vector<int> majorityElement(vector<int>& nums, int k) {
    unordered_map<int, int> candidates;
    
    for (int num : nums) {
        if (candidates.count(num)) {
            ++candidates[num];
        } else if (candidates.size() < k - 1) {
            candidates[num] = 1;
        } else {
            // 所有候选者票数减1
            for (auto it = candidates.begin(); it != candidates.end(); ) {
                if (--(it->second) == 0) {
                    it = candidates.erase(it);
                } else {
                    ++it;
                }
            }
        }
    }
    
    // 验证阶段
    vector<int> result;
    for (auto& p : candidates) {
        int count = 0;
        for (int num : nums) {
            if (num == p.first) ++count;
        }
        if (count > nums.size() / k) {
            result.push_back(p.first);
        }
    }
    
    return result;
}

2. 如果数组特别大,无法一次性加载到内存?

cpp

复制代码
// 使用分治法或流式处理
// 只能使用摩尔投票法,因为它只需要常数空间
int streamMajorityElement(istream& data_stream) {
    int candidate = 0, votes = 0;
    int num;
    
    while (data_stream >> num) {
        if (votes == 0) {
            candidate = num;
            votes = 1;
        } else if (num == candidate) {
            ++votes;
        } else {
            --votes;
        }
    }
    
    return candidate;  // 需要再次遍历验证
}

3. 并行计算版本

cpp

复制代码
// 使用MapReduce思想
// Map阶段:每个线程统计局部候选
// Reduce阶段:合并局部候选

十、面试技巧

1. 如何向面试官解释摩尔投票法?

text

复制代码
"可以把这个算法想象成选举:
- 每个数字就是一个候选人
- 我们遍历数组进行投票
- 遇到相同候选人就加一票
- 遇到不同候选人就被抵消一票
- 因为目标候选人票数超过一半,所以最后剩下的必然是他"

2. 解题步骤建议

  1. 首先提出暴力解法(体现思考过程)

  2. 分析其缺点(时间复杂度高)

  3. 提出排序解法(简单但不符合要求)

  4. 最后给出摩尔投票法(最优解)

  5. 分析时间空间复杂度

  6. 讨论边界情况和验证步骤

3. 常见面试问题

  • 为什么需要第二轮验证?

  • 如果没有超过一半的数字会怎样?

  • 算法的时间复杂度是多少?

  • 能否进一步优化?

十一、总结

关键点回顾

  1. 暴力统计法:简单但效率低,仅用于理解问题

  2. 排序法:实现简单,但改变了原数组且时间复杂度非最优

  3. 摩尔投票法:O(n)时间,O(1)空间,是最优解法

选择建议

  • 学习时:理解三种方法的演进过程

  • 面试时:直接给出摩尔投票法,并解释原理

  • 工程实践:使用摩尔投票法,注意添加验证步骤

算法思想扩展

摩尔投票法体现了"抵消"的思想,这种思想可以应用于:

  • 寻找主要元素

  • 数据压缩

  • 异常检测

  • 负载均衡

掌握这种算法思想,能够帮助我们解决更多类似的问题,培养高效的问题解决能力。

记住:优秀的算法不仅在于解决问题,更在于以最优的方式解决问题。摩尔投票法正是这样一个既优美又高效的算法典范。

相关推荐
研究点啥好呢1 小时前
Momenta算法工程师面试题精选:10道高频考题+答案解析
人工智能·算法·求职招聘·面试笔试
Resistance丶未来1 小时前
DeepSeek-V4 新手快速上手指南
数据结构·python·gpt·算法·机器学习·claude·claude 4.6
同勉共进1 小时前
并发编程系列(二)—— store, load 与 RMW
c++·arm·并发编程·x86·store·load·rmw
山甫aa1 小时前
多叉树定义与遍历-----从零开始的数据结构
开发语言·c++·二叉树·多叉树
永远睡不够的入1 小时前
C++11新特性(2):深入 C++ 参数传递黑盒:从引用折叠到完美转发,再到可变参数模板
开发语言·c++
深邃-2 小时前
【Web安全】-Kali,Linux配置(1):Kali网络配置,LinuxEnvConfig配置脚本,APT源的讲解,Kali设置中文
linux·运维·开发语言·网络·安全·web安全·网络安全
Hello World . .2 小时前
Linux驱动编程:内核同步的艺术-从互斥到底半部
linux·开发语言·数据库
江山与紫云2 小时前
告别重复造轮子:Codex写脚本
开发语言·python
comli_cn2 小时前
HMM算法
线性代数·算法