【算法】摩尔投票算法

目录

  • 1.概述
  • 2.算法思想
  • 3.代码实现
    • 3.1.t = ⌊n / 2⌋
    • 3.2.t = ⌊n / 3⌋
    • 3.3.t = ⌊n / (m + 1)⌋
  • 4.应用

参考:LeetCode_多数元素 II 题解

1.概述

(1)摩尔投票法 (Boyer–Moore Majority Vote Algorithm) 是一种用来寻找一组元素中多数元素的常量级空间复杂度算法。一般来说,摩尔投票法常用于求众数,求众数这个问题本身比较简单,但是想要使用常量级空间复杂度来实现却不是那么简单。而摩尔投票法正是这样一种算法。

(2)众数 (Mode) 是指在统计分布上具有明显集中趋势点的数值,代表数据的一般水平。 也是一组数据中出现次数最多的数值,有时众数在一组数中有好几个。用 M 表示。上面提到的多数元素与众数的含义差不多,只不过在摩尔投票法算法中,多数元素是指在数组(长度为 n)中出现次数大于某个阈值 (t) 的元素,并且存在如下结论:

  • 如果 t = ⌊n / 2⌋,那么多数元素最多只有 1 个;
  • 如果 t = ⌊n / 3⌋,那么多数元素最多只有 2 个;
  • 如果 t = ⌊n / (m + 1)⌋,那么多数元素最多只有 m 个;

2.算法思想

(1)摩尔投票法的核心思想为对拼消耗。首先我们考虑最基本的摩尔投票问题,比如找出一组数字序列中出现次数大于 ⌊n / 2⌋ 的数字(并且假设这个数字一定存在)。我们可以直接利用反证法证明这样的数字只可能有一个:

  • 假设我们当前数组中存在次数大于总数一半的元素为 x,数组的总长度为 n,则我们可以把数组分为两部分:
    • 一部分为相同的 k 个元素 x;
    • 另一部分为 n − k 2 \frac{n - k}{2} 2nk 对个不同的元素配对,
  • 此时我们假设还存在另外一个次数大于总数一半的元素 y,则此时 y 因该满足 y > n 2 \frac{n}{2} 2n ,但是按照我们之前的推理 y 应当满足 y ≤ n − k 2 \frac{n - k}{2} 2nk ,二者自相矛盾。

因此,对于 t = ⌊n / 2⌋,摩尔投票算法的核心思想是基于这个事实:每次从序列里选择两个不相同的数字删除掉(或称为抵消),最后剩下一个数字或几个相同的数字,就是出现次数大于总数一半的那个元素。

(2)对于 t = ⌊n / 3⌋,我们可以利用反证法推断出满足这样条件的元素最多只有两个,同理,我们可以利用摩尔投票法的核心思想,每次选择三个互不相同的元素进行删除(或称为「抵消」),推导思路如下:

  • 假设数组中一定只存在一个次数大于 ⌊ n 3 \frac{n}{3} 3n⌋ 的元素 x,则此时我们可以把数组分成两部分:一部分相同的 k 个元素 x,另一部分为 n − k 3 \frac{n - k}{3} 3nk 组三个不同的元素,我们知道三个不同的元素会被抵消,因此最终只会剩下一个元素为 x。
  • 如果只存在 2 个次数大于 ⌊ n 3 \frac{n}{3} 3n⌋ 的元素时,假设这两个不同的元素分别为 x 和 y,则此时我们一定可以把数组分成三部分:第一部分相同的 m 个元素 x,第二部分相同的 k 个元素 y,第三部分为 n − m − k 3 \frac{n - m - k}{3} 3nmk 组三个互不同的元素,我们知道三个互不同的元素会被抵消,因此最终只会剩下两个元素为 x 和 y。

具体思路如下:

  • 我们每次检测当前元素是否为第一个选中的元素或者第二个选中的元素;
  • 每次我们发现当前元素与已经选中的两个元素都不相同,则进行抵消一次;
  • 如果存在最终选票大于 0 的元素,我们还需要再次统计已选中元素的次数,检查元素的次数是否大于 ⌊ n 3 \frac{n}{3} 3n⌋。

(3)对于 t = ⌊n / (m + 1)⌋,我们可以利用同样的方法推断出满足这样条件的元素最多只有 m 个。但是需要注意的是,此时算法时间复杂度为 O(n * m),空间复杂度为 O(m)

3.代码实现

3.1.t = ⌊n / 2⌋

class Solution {
    public int majorityElement(int[] nums) {
        //设 candidate 为出现次数大于 ⌊n / 2⌋ 的元素
        int candidate = 0;
        int cnt = 0;
        //遍历数组 nums
        for (int num : nums) {
            if (cnt == 0) {
                //重新确定选举人
                candidate = num;
            }
            if (num == candidate) {
                //candidate 的票数加一
                cnt++;
            } else {
                //对拼消耗,即相当于 candidate 的票数减一
                cnt--;
            }
        }
        return candidate;
    }
}

① 上述算法的时间复杂度为 O(n),空间复杂度为 O(1)。
② 有关 t = ⌊n / 2⌋ 的摩尔投票算法的具体细节,可以参考本题官方题解。

3.2.t = ⌊n / 3⌋

class Solution {
	public List<Integer> majorityElement(int[] nums) {
	    //满足题目条件的元素最多只有两个,我们设为 ele1 和 ele2
	    int ele1 = 0;
	    int ele2 = 0;
	    //设 vote1 和 vote2 分别为 ele1 和 ele2 的赞成票数
	    int vote1 = 0;
	    int vote2 = 0;
	    for (int num : nums) {
	        if (vote1 > 0 && num == ele1) {
	            //num 为第一个元素,则票数加 1
	            vote1++;
	        } else if (vote2 > 0 && num == ele2) {
	            //num 为第二个元素,则票数加 1
	            vote2++;
	        } else if (vote1 == 0) {
	            //选择第一个元素
	            ele1 = num;
	            vote1++;
	        } else if (vote2 == 0) {
	            //选择第二个元素
	            ele2 = num;
	            vote2++;
	        } else {
	            //三个元素 ele1、ele3、num 互不相同,则其票数减 1,即对拼消耗
	            vote1--;
	            vote2--;
	        }
	    }
	    //cnt1 和 cnt2 分别记录元素 ele1 和 ele2 出现的次数
	    int cnt1 = 0;
	    int cnt2 = 0;
	    for (int num : nums) {
	        if (vote1 > 0 && num == ele1) {
	            cnt1++;
	        }
	        if (vote2 > 0 && num == ele2) {
	            cnt2++;
	        }
	    }
	    //检查元素出现的次数是否满足要求
	    List<Integer> res = new ArrayList<>();
	    if (vote1 > 0 && cnt1 > nums.length / 3) {
	        res.add(ele1);
	    } 
	    if (vote2 > 0 && cnt2 > nums.length / 3) {
	        res.add(ele2);
	    }
	    return res;
	}
}

上述算法的时间复杂度为 O(n),空间复杂度为 O(1)。

3.3.t = ⌊n / (m + 1)⌋

class Solution {
	public static List<Integer> majorityElements(int[] nums, int m) {
        //存储多数元素的集合
        List<Integer> res = new ArrayList<>();
        //候选元素数组
        int[] candidates = new int[m];
        //对应候选元素的计数器数组
        int[] counts = new int[m];
        for (int num : nums) {
            //如果 num 存在于候选元素数组中,则将对应计数器加 1
            for (int i = 0; i < m; i++) {
                if (candidates[i] == num) {
                    counts[i]++;
                    break;
                }
                //如果 num 不在候选元素数组中且有空位置(计数器为 0),则将 num 加入候选元素数组
                if (candidates[i] == 0 && counts[i] == 0) {
                    candidates[i] = num;
                    counts[i]++;
                    break;
                }
            }
            //如果候选元素数组已满,则将所有计数器减 1
            if (counts[m - 1] != 0) {
                for (int i = 0; i < m; i++) {
                    counts[i]--;
                }
            }
        }

        //验证候选元素的计数器是否大于阈值
        for (int i = 0; i < m; i++) {
            int count = 0;
            for (int num : nums) {
                if (num == candidates[i]) {
                    count++;
                }
            }
            if (count > nums.length / (m + 1)) {
                res.add(candidates[i]);
            }
        }
        return res;
    }
}

4.应用

大家可以去 LeetCode 上找相关的 Boyer-Moore 投票算法的题目来练习,或者也可以直接查看 LeetCode 算法刷题目录 (Java) 这篇文章中的 Boyer-Moore 投票算法章节。如果大家发现文章中的错误之处,可在评论区中指出。

你可能感兴趣的:(数据结构与算法,摩尔投票算法,算法,众数)