文章

一个有趣的算法:Boyer-Moore投票算法

一个有趣的算法:Boyer-Moore投票算法

一、背景

摩尔投票算法(Boyer-Moore)是一种用于在数组中寻找多数元素的高效算法。多数元素是指在数组中出现次数超过一半的元素。该算法由Robert S. Boyer和J Strother Moore于1981年提出,具有线性时间复杂度和常数空间复杂度。

二、算法设计

2.1问题定义

Boyer-Moore投票算法要解决什么问题?

  1. 在一个数组中,找到出现次数超过一半的元素(多数元素)。
  2. 如果不存在多数元素,算法应返回一个指示不存在的结果。

2.2算法思路

Boyer-Moore投票算法的核心思想是通过“抵消”不同元素的出现次数来找到多数元素。具体步骤如下:

  1. 初始化两个变量:候选元素candidate和计数器count,初始值分别为None和0。
  2. 遍历数组中的每个元素num:
    • 如果count为0,将candidate设置为当前元素num,并将count设为1。
    • 如果num等于candidate,增加count的值(count++)。
    • 如果num不等于candidate,减少count的值(count–)。
  3. 遍历结束后,candidate即为可能的多数元素。
  4. 为了确认candidate是否为多数元素,需要进行第二次遍历,计算candidate在数组中的出现次数,并与数组长度的一半进行比较。(如果题目明确指出数组中一定存在多数元素,则可以省略这一步。)
    • 如果candidate的出现次数超过数组长度的一半,那么candidate即为多数元素。
    • 否则,数组中不存在多数元素。

2.3伪代码

1
2
3
4
5
6
7
8
9
count ← 0
for each a in A:
    if count == 0:
        candidate ← a
        count ← 1
    else if a == candidate:
        count ← count + 1
    else:
        count ← count - 1

为什么有效?

  • Boyer-Moore投票算法之所以有效,是因为它利用了多数元素的定义:如果一个元素在数组中出现的次数超过一半,那么即使与其他所有不同元素的出现次数相抵消,最终该元素仍然会留下来。
  • 通过这种“抵消”机制,算法能够在一次遍历中找到候选元素,并在第二次遍历中验证其有效性。

三、代码实现(Java)

3.1Java实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MajorityElement {
    
    /**
     * 使用 Boyer-Moore 投票算法寻找多数元素
     * 时间复杂度: O(n)
     * 空间复杂度: O(1)
     */
    public static int majorityElement(int[] nums) {
        int candidate = nums[0];  // 初始化候选元素
        int count = 1;            // 初始化计数器
        
        // 遍历数组(从第二个元素开始)
        for (int i = 1; i < nums.length; i++) {
            if (count == 0) {
                // 如果计数器为0,更新候选元素
                candidate = nums[i];
                count = 1;
            } else if (nums[i] == candidate) {
                // 如果当前元素等于候选元素,计数器加1
                count++;
            } else {
                // 如果当前元素不等于候选元素,计数器减1
                count--;
            }
        }
        
        return candidate;
    }
}

四、算法延伸

如果要在一个数组中,找到出现次数超过 ⌊ n/3 ⌋ 次的元素怎么办?

  1. 思路

    与上面的实现思路基本相似,我们考虑出现次数超过 ⌊ n/3 ⌋ 次的元素最多只有 2 个,所以可以设置两个候选位,进行计数和消除。 算法可行性的证明也与上面的原理是一样的。

  2. 算法实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
     class Solution {
         public List<Integer> majorityElement(int[] nums) {
             List<Integer> result = new ArrayList<>();
                
             //初始化两个候选者和计数器
             int candidate1=0,count1=0;
             int candidate2=0,count2=0;
    
             //遍历数组挑选候选者
             for(int num : nums){
                 if(candidate1==num){
                     count1++;
                 }else if(candidate2==num){
                     count2++;
                 }else if(count1==0){
                     candidate1=num;
                     count1=1;
                 }else if(count2==0){
                     candidate2=num;
                     count2=1;
                 }else{
                     count1--;
                     count2--;
                 }
             }
             //验证候选者是否达到了要求(防止这个问题没有答案)
             count1=0;
             count2=0;
             for(int num: nums){
                 if(num==candidate1) count1++;
                 else if(num==candidate2) count2++;
             }
             int n=nums.length;
             if(count1>n/3) result.add(candidate1);
             if(count2>n/3) result.add(candidate2);
             return result;
         }
     }
    
本文由作者按照 CC BY 4.0 进行授权