摩尔投票算法:杀敌一千自损八百~

前言

今日在刷leetcode时遇见了一道很有意思的题:

这一题的解法很多,我也尝试了两种方式进行解答:Map存储记录统计个数方案, 先排序后统计个数方案。

虽然能够满足解题,但是无法满足进阶的要求:

  • 进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

一开始,我尝试使用快慢指针来解决问题。代码如下:

Java 复制代码
private static int majorityElementV2(int[] nums) {
    int left = 0, right = 0, count = 0;
    while (left < nums.length) {
        if (right >= nums.length) {
            right = ++left;
            count = 0;
            continue;
        }
        if (nums[left] == nums[right++] && ++count > nums.length / 2) {
            return nums[left];
        }
    }
    return 0;
}

验证通过,但是leetcode耗时达到了2177ms。虽然能够满足空间复杂度O(1),但时间复杂度仍保持在O(n)O(n^2),不满足进阶的时间复杂度。

在翻看题解时,很多题目带有"摩尔投票"、"多数投票"、"打擂台"字样,我很好奇~

学习后,引出本篇,摩尔投票算法~!

一、摩尔投票算法-杀敌一千自损八百

博耶-摩尔多数投票算法(英语:Boyer--Moore majority vote algorithm),中文又称摩尔投票算法、多数投票算法,是一种用于寻找序列中多数元素的算法,其中多数元素需要占序列个数的一半以上。1981年由罗伯特·S·博耶和J·斯特罗瑟·摩尔发表,当下常用于处理数据流问题。

基本思想是:设计一个候选者和计数器,在一次遍历中,通过候选者之间的抵消,最终得到一个多数元素。

核心思想是:

  1. 设计候选者和计数器,计数器初始化为0

  2. 遍历序列:

    1. 当候选者不存在时,当前元素上位,计数器设为1;
    2. 当前元素与候选者相同时,计数器加1;
    3. 当前元素与候选者不同时,计数器减1。若计数器归0,则清除候选者。
  3. 遍历结束,当前候选者则为序列中个数超过一半的多数元素。

我们简单点理解:

  • 诸侯争霸,百军对冲。假设我军士兵占战场总人数的一半以上,且当前进行大决战,全军出击。若每个士兵与对方一对一同归于尽,则战场上最后活下来的必定是我军。

了解之后,我们再看代码就十分清晰明了:

Java 复制代码
public static int mooreVoting(int[] nums) {
    // init
    int count = 0, major = -1;
    // loop
    for (int num : nums) {
        if (count == 0) {
            major = num;
            count++;
        } else if (major == num) {
            count++;
        } else {
            count--;
        }
    }
    // result
    return major;
}

从代码入手,我们能够分析出一下优缺点:

  • 优点:

    • 时间复杂度、空间复杂度低:只进行一次遍历,且不需要额外的空间存储数据。
    • 代码简单,易理解。
  • 缺点:

    • 只适用于多数元素在序列占比超过一半的情况。

二、若多数元素不超过一半呢?

在Leetcode上还有一道同类型的题目:Leetcode《229.多数元素 Ⅱ》

多数元素超过的次数是⌊n/3⌋,即多数元素占比超过1/3,意味着至多出现两个多数元素。

候选者个数增加,意味着只有当无候选者空位时,才会减少候选者个数。同时,上下位情况会变得频繁,而每次上下位也属于元素抵消。因此多数元素仍会在候选者中,但候选者是否全部合法,是无法确定的。所以,我们需要新增校验当前候选者是否合法。

当下策略就变为:

  • 增加候选者为2个。
  • 增加校验逻辑。

代码如下:

Java 复制代码
public static List<Integer> mooreVotingOneThird(int[] nums) {
    // 1.check
    if (nums == null || nums.length == 0) {
        return Collections.emptyList();
    }
    if (nums.length == 1) {
        return Collections.singletonList(nums[0]);
    }
    // 2.init
    Integer major1 = null, major2 = null;
    int count1 = 0, count2 = 0;
    // 3.选举
    for (int num : nums) {
        // major之间没有高低之分。因此,先判断是否和候选者相同,再判断是否有空位,最后判断是否需要减少统计个数
        if (Objects.equals(num, major1) && count1 > 0) {
            count1++;
        } else if (Objects.equals(num, major2) && count2 > 0) {
            count2++;
        } else if (Objects.isNull(major1) || count1 <= 0) {
            // 作为候选者上位
            major1 = num;
            count1 = 1;
        } else if (Objects.isNull(major2) || count2 <= 0) {
            // 作为候选者上位
            major2 = num;
            count2 = 1;
        } else {
            // 不相等,且无空位,所有候选者统计个数-1
            if (--count1 == 0) {
                major1 = null;
            }
            if (--count2 == 0) {
                major2 = null;
            }
        }
    }
    // 4.校验:统计候选者个数在序列是否大于1/3,大于则加入结果集
    int majorNum = nums.length / 3;
    int checkCount1 = 0, checkCount2 = 0;
    for (int num : nums) {
        if (Objects.equals(num, major1)) {
            checkCount1++;
        } else if (Objects.equals(num, major2)) {
            checkCount2++;
        }
    }
    List<Integer> result = new ArrayList<>();
    if (checkCount1 > majorNum) {
        result.add(major1);
    }
    if (checkCount2 > majorNum) {
        result.add(major2);
    }
    return result;
}

可能会有惯性思维,认为候选者是有优先级的,其实不然。正如上文所说,候选者的竞争会十分激烈,上下位的情况会十分频繁,所以二者在选举过程中属于是平等状态。

分析后可知,当下时间复杂度为O(n),空间复杂度为O(1)

三、泛化摩尔投票算法

如果我们将第二部分进行泛化思考,就可以得知,摩尔投票也同样适合用于

  • 查找出现次数占比超过1/k的元素。即,元素个数超过n/k,n为总量个数。

思路与上文一致,增加候选者数量,以及校验是否合法。需注意的是,当前多数元素在总量的占比为变量,候选者个数也是动态的,即n/k。无法在代码中写死变量,因此采用Map来存储候选者及其个数。

代码如下:

Java 复制代码
public static List<Integer> mooreVotingGeneralize(int[] nums, int k) {
    // 1.check
    if (nums == null || nums.length == 0) {
        return Collections.emptyList();
    }
    // 2.init
    int majorNum = nums.length / k; // 候选者个数
    Map<Integer, Integer> majorMap = new HashMap<>(majorNum - 1); // key:候选者名单,value:候选者个数
    // 3.选举
    for (int num : nums) {
        if (majorMap.isEmpty()) {
            // 无候选者
            majorMap.put(num, 1);
        } else if (majorMap.containsKey(num)) {
            // 与候选者相同
            majorMap.put(num, majorMap.get(num) + 1);
        } else if (majorMap.size() < majorNum) {
            // 与候选者不同,且有空位,则将当前元素上位候选者
            majorMap.put(num, 1);
        } else {
            // 与候选者不同,且无空位,则所有元素统计个数-1
            majorMap.entrySet().forEach(entry -> {
                if (entry.getValue() <= 0) {
                    majorMap.remove(entry.getKey());
                } else {
                    entry.setValue(entry.getValue() - 1);
                }
            });
        }
    }
    // 4.统计
    // 比较value的值,选出value大于等于majorNum的key。PS:可能为多个key。
    return majorMap.entrySet().stream()
            .filter(entry -> entry.getValue() >= majorNum)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
}

代码逻辑与之前相似,只不过在候选者下位时,需要删除在Map中的存储。

相关推荐
little redcap1 小时前
第十九次CCF计算机软件能力认证-乔乔和牛牛逛超市
数据结构·c++·算法
muyierfly2 小时前
34.贪心算法1
算法·贪心算法
luthane4 小时前
python 实现average mean平均数算法
开发语言·python·算法
静心问道4 小时前
WGAN算法
深度学习·算法·机器学习
杰九5 小时前
【算法题】46. 全排列-力扣(LeetCode)
算法·leetcode·深度优先·剪枝
manba_5 小时前
leetcode-560. 和为 K 的子数组
数据结构·算法·leetcode
liuyang-neu5 小时前
力扣 11.盛最多水的容器
算法·leetcode·职场和发展
忍界英雄5 小时前
LeetCode:2398. 预算内的最多机器人数目 双指针+单调队列,时间复杂度O(n)
算法·leetcode·机器人
Kenneth風车5 小时前
【机器学习(五)】分类和回归任务-AdaBoost算法-Sentosa_DSML社区版
人工智能·算法·低代码·机器学习·数据分析
C7211BA5 小时前
使用knn算法对iris数据集进行分类
算法·分类·数据挖掘