重温数据结构与算法之摩尔投票法

前言

在统计学中,众数是一种重要的集中趋势指标,它表示一组数据中出现次数最多的那个值。此外在一个集合中,如果一个元素的出现次数比其他所有元素的出现次数之和还多,那么就称它为这个集合的绝对众数(等价地说,绝对众数的出现次数大于总元素数的一半)。例如,在{1,2,3,3,4}这组数据中,3就是众数,因为它出现了两次,而其他值都只出现了一次,但是没有绝对众数

寻找众数可以帮助我们了解数据的主要特征和分布情况,在某些场合下也可以作为数据代表或近似值。例如,

  • 在民意调查中,我们可能会关注哪个候选人得到了最多人的支持;
  • 在商品评价中,我们可能会关注哪种评分占据了大多数;
  • 在图像处理中,我们可能会关注哪种颜色或灰度值出现频率最高。

寻找众数有不同的方法和算法,在本文中我们将介绍两种常见的方法:暴力法和摩尔投票法。

  • 暴力法是通过遍历或哈希统计每个元素出现的次数,然后找出最大值对应的元素。这种方法简单直观,但需要额外的空间存储每个元素及其次数,时间或空间复杂度较高。
  • 摩尔投票法是通过维护一个候选元素和一个计数器,遍历数组时更新候选元素和计数器,最后检验候选元素是否为众数。这种方法巧妙地利用了数组中存在一个占比超过一半(或其他阈值)的元素这一条件,并且不需要额外空间,并且时间复杂度较低,但是这种算法不能求一般上的众数,能够求绝对众数

本文将分别介绍暴力法和摩尔投票法的原理、实现、拓展、局限等方面,并给出 LeetCode上相关题目及解答。

一、暴力法

暴力法是一种简单直观的寻找众数的方法,它的基本思路是遍历数组中的每个元素,统计该元素出现的次数,然后找出最大值对应的元素。这种方法可以用两种方式实现:一种是嵌套循环,另一种是哈希表。

1.1 嵌套循环

嵌套循环的方法是在外层循环中遍历数组中的每个元素,在内层循环中统计该元素出现的次数,并与当前最大值进行比较。如果该元素出现次数超过当前最大值,则更新最大值和众数。这种方法不需要额外空间,但时间复杂度较高,为O(n^2)。

以下是 Java 代码示例:

// 嵌套循环方法
public static int majorityElement1(int[] nums) {
    
    
    // 初始化最大值和众数
    int maxCount = 0;
    Integer majority = null;
    // 外层循环遍历数组
    for (int i = 0; i < nums.length; i++) {
    
    
        // 初始化当前元素出现次数为0
        int count = 0;
        // 内层循环统计当前元素出现次数
        for (int j = 0; j < nums.length; j++) {
    
    
            if (nums[j] == nums[i]) {
    
    
                count++;
            }
        }
        // 如果当前元素出现次数超过最大值,则更新最大值和众数
        if (count > maxCount) {
    
    
            maxCount = count;
            majority = nums[i];
        }
    }
    return majority;
}

1.2 哈希表

哈希表(或字典)的方法是利用一个数据结构来存储每个元素及其出现次数。遍历数组时,如果该元素在哈希表中不存在,则将其加入并初始化其次数为1;如果存在,则将其次数加1。同时维护一个最大值和一个众数,在更新哈希表时也更新它们。这种方法需要额外空间O(n),但时间复杂度较低,为O(n)。

以下是 Java 代码示例:

// 哈希表方法
public static int majorityElement2(int[] nums) {
    
    
    // 初始化哈希表、最大值和众数
    HashMap<Integer, Integer> counter = new HashMap<>();
    int maxCount = 0;
    Integer majority = null;
    // 遍历数组
    for (int num : nums) {
    
    
        // 如果该元素在哈希表中不存在,则将其加入并初始化其次数为1;如果存在,则将其次数加1。
        counter.put(num, counter.getOrDefault(num, 0) + 1);
        // 如果该元素出现次数超过最大值,则更新最大值和众数。
        if (counter.get(num) > maxCount) {
    
    
            maxCount = counter.get(num);
            majority = num;
        }
    }
    return majority;
}

二、摩尔投票法

2.1 原理

摩尔投票法(Boyer-Moore Majority Vote Algorithm)是一种高效且节省空间的寻找绝对众数的方法,它由 Robert S. Boyer 和 J Strother Moore 在1981年提出。

摩尔投票法的过程非常简单,让我们把找到绝对众数的过程想象成一次选举。我们维护一个m,表示当前的候选人,然后维护一个cnt。对于每一张新的选票,如果它投给了当前的候选人,就把cnt加1,否则就把cnt减1(也许你可以想象成,B的一个狂热支持者去把A的一个支持者揍了一顿,然后两个人都没法投票)。特别地,计票时如果cnt=0,我们可以认为目前谁都没有优势,所以新的选票投给谁,谁就成为新的候选人。

例如,5张选票分别投给1,3,3,2,3,则:

选票 候选人 计数cnt
1 1 1
3 3 0
3 3 1
2 2 0
3 3 1

最后剩下的候选人就是3,也就是绝对众数。

摩尔投票法的时间复杂度是O(n),因为只需要遍历一次数组。空间复杂度是O(1),因为只需要两个变量来记录候选人和计数。

2.2 扩展

摩尔投票法可以扩展到寻找出现次数超过 n/k 的元素( n 为元素个数)。

  • 首先,n/k 的众数最多只有 k-1 个,因为众数的定义是指出现的次数大于 n/k ,如果众数有k个那么众数的所有元素加起来肯定是大于n 的,不符。因此,我们可以用一个哈希表来记录 k-1 个候选人和它们对应的计数。
  • 对于每一个新元素
    • 如果它已经在哈希表中,则增加其计数;
    • 如果它不在哈希表中,并且哈希表还没有满,则将其加入哈希表并初始化计数为 1 ;
    • 如果它不在哈希表中,并且哈希表已经满,则将所有候选人的计数减 1 ,并删除那些计数为0的候选人。
  • 最后剩下的候选人就是可能超过 n/k 次出现的元素,但还需要再遍历一次数组来验证它们是否真的满足条件。

Java 示例代码如下:

// 输入:数组arr,整数k
// 输出:一个列表,包含所有出现次数超过n/k的元素
public List<Integer> mooreVoting(int[] arr, int k) {
    
    
  // 初始化一个哈希表,用来存储候选人和计数
  HashMap<Integer, Integer> candidates = new HashMap<>();
  // 遍历数组中的每个元素
  for (int x : arr) {
    
    
    // 如果x已经是候选人,则增加其计数
    if (candidates.containsKey(x)) {
    
    
      candidates.put(x, candidates.get(x) + 1);
    }
    // 如果x不是候选人,并且候选人还没有满k-1个,则将x加入候选人并初始化计数为1
    else if (candidates.size() < k - 1) {
    
    
      candidates.put(x, 1);
    }
    // 如果x不是候选人,并且候选人已经满了k-1个,则将所有候选人的计数减1,并删除那些计数为0的候选人
    else {
    
    
      for (Integer y : new ArrayList<>(candidates.keySet())) {
    
    
        candidates.put(y, candidates.get(y) - 1);
        if (candidates.get(y) == 0) {
    
    
          candidates.remove(y);
        }
      }
    }
  }

  // 初始化一个列表,用来存储最终结果
  List<Integer> result = new ArrayList<>();
  // 遍历剩下的候选人,验证它们是否真的超过n/k次出现
  for (Integer x : candidates.keySet()) {
    
    
    int count = 0;
    for (int y : arr) {
    
    
      if (x == y) {
    
    
        count++;
      }
    }
    if (count > arr.length / k) {
    
    
      result.add(x);
    }
  }

  // 返回结果列表
  return result;
}

2.3 缺点和局限

  • 摩尔投票法只能找到出现次数超过一定比例的元素,而不能找到出现次数最多的元素。如果没有一个元素满足这个比例条件,那么摩尔投票法可能返回空列表或错误的元素。例如在{1,1,2,3}这组数据中,出现次数最多的元素是1,摩尔投票法只能找到比例大于1/2的数,而不能找到比例 <=1/2 的数。
  • 摩尔投票法的另一个局限性是,它需要有一个明确的占比阈值来确定候选人的数量。如果没有这个阈值,或者这个阈值不合理,那么摩尔投票法可能无法找到正确的结果。

三、LeetCode实战

3.1 多数元素

169. 多数元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

public int majorityElement(int[] nums) {
    
    
    int m = nums[0], cnt = 1; // 初始化候选元素m和计数器cnt
    for (int i = 1; i < nums.length; i++) {
    
     // 遍历数组中的每个元素
        if (cnt == 0) m = nums[i]; // 如果计数器为零,更新候选元素为当前元素
        if (nums[i] == m) {
    
     // 如果当前元素等于候选元素
            cnt++; // 增加计数器
        } else {
    
     // 否则
            cnt--; // 减少计数器
        }
    }
    return m; // 返回最终的候选元素,它就是多数元素
}

3.2 多数元素 II

229. 多数元素 II

给定一个大小为 n 的整数数组,找出其中所有出现超过 ⌊ n/3 ⌋ 次的元素。

public List<Integer> majorityElement(int[] nums) {
    
    
    // 初始化一个哈希表,用来存储候选人和计数
    HashMap<Integer, Integer> candidates = new HashMap<>();
    int k = 3;
    // 遍历数组中的每个元素
    for (int x : nums) {
    
    
        // 如果x已经是候选人,则增加其计数
        if (candidates.containsKey(x)) {
    
    
            candidates.put(x, candidates.get(x) + 1);
        }
        // 如果x不是候选人,并且候选人还没有满k-1个,则将x加入候选人并初始化计数为1
        else if (candidates.size() < k - 1) {
    
    
            candidates.put(x, 1);
        }
        // 如果x不是候选人,并且候选人已经满了k-1个,则将所有候选人的计数减1,并删除那些计数为0的候选人
        else {
    
    
            for (Integer y : new ArrayList<>(candidates.keySet())) {
    
    
                candidates.put(y, candidates.get(y) - 1);
                if (candidates.get(y) == 0) {
    
    
                    candidates.remove(y);
                }
            }
        }
    }

    // 初始化一个列表,用来存储最终结果
    List<Integer> result = new ArrayList<>();
    // 遍历剩下的候选人,验证它们是否真的超过n/k次出现
    for (Integer x : candidates.keySet()) {
    
    
        int count = 0;
        for (int y : nums) {
    
    
            if (x == y) {
    
    
                count++;
            }
        }
        if (count > nums.length / k) {
    
    
            result.add(x);
        }
    }

    // 返回结果列表
    return result;
}

参考

  1. 算法学习笔记(78): 摩尔投票

猜你喜欢

转载自blog.csdn.net/qq_23091073/article/details/129641989