摩尔投票

摩尔投票算法 (Boyer-Moore Voting Algorithm)

目录


1. 算法概述

1.1 核心思想

摩尔投票算法是一个巧妙且高效的算法,专门用于在 O(n) 时间复杂度和 O(1) 空间复杂度内,找到数组中出现次数超过一定比例的众数。

1.2 适用场景

  • 寻找数组中出现次数超过一半的绝对众数(Majority Element)
  • 寻找数组中出现次数超过n/k的所有众数
  • 海量数据处理,尤其是流式数据场景
  • 内存资源有限的环境

1.3 复杂度分析

特性 复杂度
时间复杂度 O(n)
空间复杂度 O(1)(寻找n/2众数)或 O(k)(寻找n/k众数)

2. 算法原理

2.1 核心直觉:阵营抵消

将算法想象成一场"阵营大乱斗":

  • 每个不同的数字代表一个不同的阵营
  • 如果两个不同阵营的人相遇,他们就同归于尽(一换一)
  • 如果两个相同阵营的人相遇,他们就暂时抱团
  • 最终剩下的那个人所属的阵营,才有可能是占比超过一半的众数

2.2 算法的两个阶段

2.2.1 阶段一:寻找候选人

遍历数组,维护两个关键变量:

  • 候选人 (candidate):当前可能成为众数的数字
  • 计数器 (count):当前候选人领先的"兵力"

遍历规则:

  1. 如果计数器为0:将当前数字设为新候选人,计数器设为1
  2. 如果当前数字等于候选人:计数器加1
  3. 如果当前数字不等于候选人:计数器减1

2.2.2 阶段二:二次验证

原因:算法只能保证"如果有众数,它一定剩下",但不能保证"剩下的一定是众数"。

做法:再次遍历数组,统计候选人的实际出现次数,确认是否严格大于n/k。

2.3 算法示例

假设数组是 [2, 2, 1, 1, 2]

  1. 遇到第一个 2count 是 0,候选人变为 2count 变为 1
  2. 遇到第二个 2:和候选人相同,count 变为 2
  3. 遇到第一个 1:和候选人不同,抵消,count 变为 1
  4. 遇到第二个 1:再次不同,抵消,count 变为 0
  5. 遇到第三个 2count 是 0,重新选候选人为 2count 变为 1

遍历结束,最后的候选人是 2。通过二次验证,确认2出现次数为3,超过数组长度5的一半,因此2是众数。


3. 基本实现:寻找超过n/2的众数

3.1 C++代码实现

#include <vector>

int findMajorityElement(std::vector<int>& nums) {
    int candidate = 0;
    int count = 0;

    // 阶段一:寻找候选人
    for (int num : nums) {
        if (count == 0) {
            candidate = num;
            count = 1;
        } else if (num == candidate) {
            count++;
        } else {
            count--;
        }
    }

    // 阶段二:二次验证
    int actualCount = 0;
    for (int num : nums) {
        if (num == candidate) {
            actualCount++;
        }
    }

    if (actualCount > nums.size() / 2) {
        return candidate;
    } else {
        return -1; // 或抛出异常,表示没有找到符合条件的众数
    }
}

3.2 代码解析

  • 阶段一:通过遍历数组,利用"抵消"逻辑找到潜在的众数候选人
  • 阶段二:再次遍历数组,验证候选人是否真的是众数
  • 返回值:如果找到众数则返回众数,否则返回-1或抛出异常

4. 扩展实现:寻找超过n/k的众数

4.1 扩展原理

  • 规律:寻找超过n/k的众数,最多有 k-1 个结果(因为k-1个各占1/k+ε,总和超过1)
  • 抵消机制:需要准备 k-1 个候选人和 k-1 个计数器,当遇到第k个不同的数字时,所有候选人计数器减1
目标门槛 候选人数量 抵消所需的不同数字个数
超过n/2 1个 2个
超过n/3 2个 3个
超过n/k k-1个 k个

4.2 C++代码实现 (n/3版本)

#include <vector>
#include <unordered_set>

std::vector<int> findMajorityElementN3(std::vector<int>& nums) {
    int cand1 = 0, count1 = 0;
    int cand2 = 0, count2 = 0;

    // 阶段一:寻找最多两个候选人
    for (int num : nums) {
        if (num == cand1) {
            count1++;
        } else if (num == cand2) {a
            count2++;
        } else if (count1 == 0) {
            cand1 = num;
            count1 = 1;
        } else if (count2 == 0) {
            cand2 = num;
            count2 = 1;
        } else {
            // 三方抵消:三个不同数字各抵消一个
            count1--;
            count2--;
        }
    }

    // 阶段二:二次验证
    int actualCount1 = 0, actualCount2 = 0;
    for (int num : nums) {
        if (num == cand1) actualCount1++;
        if (num == cand2) actualCount2++;
    }

    std::unordered_set<int> resultSet;
    if (actualCount1 > nums.size() / 3) resultSet.insert(cand1);
    if (actualCount2 > nums.size() / 3) resultSet.insert(cand2);

    return std::vector<int>(resultSet.begin(), resultSet.end());
}

4.3 通用实现:使用std::unordered_map

4.3.1 核心逻辑

  1. 匹配阶段:如果当前数字已经在map中,直接将其计数器+1
  2. 占位阶段:如果map的大小还没达到k-1,就把当前数字存入map,计数器设为1
  3. 全员抵消:如果map已满且当前数字不在其中,让map中所有的候选人计数器都-1,并移除计数器归零的项

4.3.2 完整代码

#include <vector>
#include <unordered_map>

std::vector<int> findMajorityK(std::vector<int>& nums, int k) {
    std::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,移除计数器为0的候选人
            for (auto it = candidates.begin(); it != candidates.end(); ) {
                if (--(it->second) == 0) {
                    it = candidates.erase(it);
                } else {
                    ++it;
                }
            }
        }
    }

    // 阶段二:二次验证
    std::vector<int> result;
    std::unordered_map<int, int> actualCounts;
    
    for (int num : nums) {
        if (candidates.count(num)) {
            actualCounts[num]++;
        }
    }

    for (auto const& [num, count] : actualCounts) {
        if (count > nums.size() / k) {
            result.push_back(num);
        }
    }

    return result;
}

4.3.3 优化的二次验证

可以复用第一阶段的candidates字典进行二次验证,节省空间:

// 优化的阶段二:原地验证
// 1. 将现有的候选人计数器全部重置为0
for (auto& pair : candidates) {
    pair.second = 0;
}

// 2. 再次遍历原数组,只统计这些候选人的出现次数
for (int num : nums) {
    if (candidates.count(num)) {
        candidates[num]++;
    }
}

// 3. 收集真正达标的候选人
std::vector<int> result;
for (auto const& [num, count] : candidates) {
    if (count > nums.size() / k) {
        result.push_back(num);
    }
}

5. 算法对比

5.1 与排序法对比

特性 排序法 摩尔投票法
时间复杂度 O(nlogn) O(n)
空间复杂度 O(1) 或 O(n)(取决于算法) O(1) 或 O(k)
对原数组影响 会改变数组顺序(或需额外空间) 完全不改变原数组
处理海量数据 很难处理无法一次放入内存的数据 非常适合处理"流"数据

5.2 与哈希表计数法对比

特性 哈希表计数法 摩尔投票法
时间复杂度 O(n) O(n)
空间复杂度 O(n)(最坏情况) O(1) 或 O(k)
内存占用 随不同元素增加而线性增长 始终恒定,极省内存
数据读取 通常需要将数据存入内存 流式处理,看一个扔一个
适用场景 适合需要统计所有元素出现次数的场景 适合仅需寻找众数的场景

6. 面试要点

6.1 面试官为什么看重摩尔投票法?

  1. 空间效率的极致追求:体现对资源极限优化的理解
  2. 对"流式数据"的处理能力:展示对大数据/实时处理场景的适应能力
  3. 洞察问题的本质:体现根据问题特殊性设计定制化方案的能力
  4. 算法设计的巧妙性:展示对算法本质的深刻理解

6.2 常见面试问题

  1. 为什么摩尔投票法需要二次验证?

    • 答:算法只能保证"如果有众数,它一定剩下",但不能保证"剩下的一定是众数"。
  2. 寻找超过n/3的众数,最多有多少个结果?

    • 答:最多2个,因为3个各占1/3+ε,总和超过1。
  3. 摩尔投票法的空间复杂度为什么是O(1)或O(k)?

    • 答:寻找超过n/2的众数时,只需要1个候选人和1个计数器,空间复杂度O(1);寻找超过n/k的众数时,需要k-1个候选人和k-1个计数器,空间复杂度O(k)。

6.3 避坑指南

  1. 不要忘记二次验证:即使算法返回了候选人,也必须验证其是否真的是众数
  2. 注意边界情况:数组为空、k值大于数组长度等情况需要特殊处理
  3. 扩展实现时注意顺序:必须先检查是否匹配现有候选人,再检查是否有空位
  4. 二次验证可以优化:可以复用第一阶段的候选人字典,节省空间

7. 完整代码实现

以下是包含基本实现和扩展实现的完整C++代码:

#include <iostream>
#include <vector>
#include <unordered_map>
#include <unordered_set>

// 寻找超过n/2的众数
int findMajorityElement(std::vector<int>& nums) {
    int candidate = 0;
    int count = 0;

    // 阶段一:寻找候选人
    for (int num : nums) {
        if (count == 0) {
            candidate = num;
            count = 1;
        } else if (num == candidate) {
            count++;
        } else {
            count--;
        }
    }

    // 阶段二:二次验证
    int actualCount = 0;
    for (int num : nums) {
        if (num == candidate) {
            actualCount++;
        }
    }

    if (actualCount > nums.size() / 2) {
        return candidate;
    } else {
        return -1; // 或抛出异常
    }
}

// 寻找超过n/3的众数
std::vector<int> findMajorityElementN3(std::vector<int>& nums) {
    int cand1 = 0, count1 = 0;
    int cand2 = 0, count2 = 0;

    // 阶段一:寻找最多两个候选人
    for (int num : nums) {
        if (num == cand1) {
            count1++;
        } else if (num == cand2) {
            count2++;
        } else if (count1 == 0) {
            cand1 = num;
            count1 = 1;
        } else if (count2 == 0) {
            cand2 = num;
            count2 = 1;
        } else {
            count1--;
            count2--;
        }
    }

    // 阶段二:二次验证
    int actualCount1 = 0, actualCount2 = 0;
    for (int num : nums) {
        if (num == cand1) actualCount1++;
        if (num == cand2) actualCount2++;
    }

    std::unordered_set<int> resultSet;
    if (actualCount1 > nums.size() / 3) resultSet.insert(cand1);
    if (actualCount2 > nums.size() / 3) resultSet.insert(cand2);

    return std::vector<int>(resultSet.begin(), resultSet.end());
}

// 寻找超过n/k的众数(通用实现)
std::vector<int> findMajorityK(std::vector<int>& nums, int k) {
    std::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 {
            // 全员抵消
            for (auto it = candidates.begin(); it != candidates.end(); ) {
                if (--(it->second) == 0) {
                    it = candidates.erase(it);
                } else {
                    ++it;
                }
            }
        }
    }

    // 阶段二:二次验证(优化版)
    for (auto& pair : candidates) {
        pair.second = 0;
    }

    for (int num : nums) {
        if (candidates.count(num)) {
            candidates[num]++;
        }
    }

    std::vector<int> result;
    for (auto const& [num, count] : candidates) {
        if (count > nums.size() / k) {
            result.push_back(num);
        }
    }

    return result;
}

int main() {
    // 测试示例
    std::vector<int> nums1 = {2, 2, 1, 1, 1, 2, 2};
    std::cout << "超过n/2的众数:" << findMajorityElement(nums1) << std::endl;

    std::vector<int> nums2 = {1, 1, 1, 3, 3, 2, 2, 2};
    std::vector<int> result2 = findMajorityElementN3(nums2);
    std::cout << "超过n/3的众数:";
    for (int num : result2) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::vector<int> nums3 = {1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 4};
    std::vector<int> result3 = findMajorityK(nums3, 4);
    std::cout << "超过n/4的众数:";
    for (int num : result3) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

8. 总结

摩尔投票算法是一种高效、巧妙的众数查找算法,具有以下特点:

  • 时间复杂度:O(n),仅需遍历数组1-2次
  • 空间复杂度:O(1)或O(k),极省内存
  • 适用场景:寻找超过n/k的众数,尤其适合海量数据和流式数据场景
  • 核心思想:利用"抵消"机制,将不同元素两两抵消,最终剩下的元素即为潜在的众数
  • 必要步骤:必须进行二次验证,确保候选人真的是众数

掌握摩尔投票算法,不仅可以解决众数查找问题,更能培养对算法本质的深刻理解和对资源优化的极致追求,是算法学习和面试中的重要知识点。

posted @ 2026-01-02 10:58  belief73  阅读(6)  评论(0)    收藏  举报