我在刷题系列--Majority Number(LintCode)

本文介绍了一个有效算法,用于在整数数组中找到出现次数超过一半的MajorityNumber,该算法仅需一次遍历且空间复杂度为O(1)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

         本文作者:Chunk

         题目链接

         这道题目来自于LintCode,是一个Easy级别的题目,题目大意是在一个给定的整数数组中,找到出现次数超过数组大小一半的那个整数,这个整数就叫做Majority Number。看到题目以后我的第一想法就是遍历数组,然后通过一个map统计出现的整数的频次,最后遍历map找到出现次数最多的整数。相信这也是大多数人的第一反应。但是当我看到本题目的Challenge中要求O(n) time and O(1) space后,我自觉的发现把题目想的过于简单了。上面提到的方法在需要2次遍历,时间复杂度可以保证为O(n),但是空间复杂度却是O(n),与Challenge要求的O(1)差了不少。

         既然Challenge的空间复杂度可以达到O(1),那么就一定有窍门来降低空间复杂度。经过思考,发现这个题目确实是一个很有意思的问题,给定一个数组[1, 1, 1, 1, 2, 2, 2],人眼一下就可以看出Majority Number 是1,可如果一个超大的数组交给人眼来看怎么找呢?

         我的想法是,两两消除:在这样一个数组中,每次消除两个不同的数,最后剩下的数,一定就是Majority Number,如果把这个数组写在纸上,工作就是随机找到两个不同的数,然后划掉,最后看一个剩下的数就是Majority Number。

         但是编程实现可不能照此方法,编程中我们能做的只有遍历数组,然后存储下某些特征属性,在遍历结束后返回符合要求的数。继续分析,上面提到的两两消除的确是一种方法,但是由于我们对于数组的操作只有遍历,所以首先要克服的问题是:如果两个比较的数相同怎么办

         想到这个问题,突然发现为什么不把相同的数字出现的频次存起来呢?可是这样好像有回到了第一中方案中了。解决空间复杂度为O(1)的关键就在这里,其实,我们只要知道当前出现次数较多的数和它出现的次数就好,当出现不同的数时,我们将这个数的频次-1,当这个数再一次出现时就把频次+1,如果频次降为0,说明前面出现的数中正好实现了两两消除,这时把之后出现的数当做临时Majority Number记录就好,直到最后,得到整个数组的Majority Number。

         下面上代码:(需要指出的是,由于LintCode提交一直处在Pending状态,下面的代码其实没有通过LintCode的测试,只在本机上做了单元测试,但本文后边的论证基本可以保证算法的正确性)

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

public class Solution {
    /**
     * @param nums: a list of integers
     * @return: find a  majority number
     */
    public int majorityNumber(ArrayList<Integer> nums) {
        int sum = 0;
    	int mn = 0;
    	for (int i : nums) {
    		if (sum == 0) {
    			mn = i;
    			sum++;
    			continue;
    		} else {
    			if (i == mn) {
        			sum++;
        		} else {
        			sum--;
        		}
    		}
    	}
		return mn;
    }
}

         在之后的讨论中,一个题友提问我如何证明这种想法是正确的呢?经过讨论,我们给出了以下的解释:

         事实上,在上述算法运行的过程中,整个数组被分成了若干个的段落,每个段落的特征值就是mn,开始条件就是sum值从0变为1,终止条件就是sum值回归为0。在每个段落中,出现频次一半的数就是mn,另外一半都是与mn不相等的数。这里就是两两消去的关键,这样的一个段落被从整个数组中消除,是不会影响整个数组中的Majority Number的。理由如下:

         (1)如果mn是Majority Number,那么消除这个段落,在剩余的数组中,mn,即Majority Number仍然是Majority Number;

         (2)如果mn不是Majority Number,那么消除这个段落,无疑会使剩余数组中的Majority Number所占的比例大于其在整个数组中的比例;

         因此,这个方法得到了证明。此方法只需要对数组进行1次遍历,时间复杂度为O(n),空间复杂度为O(1)。

         另外还是需要注意一点,此题的条件限制,Majority Number出现的次数超过了一半,而不是等于一半。当等于一半时,会出现这样一个问题:在最后一个段落中,mn的sum值最终也会变为0,这是可以判定mn是一个出现频次为数组长度一半的数,但是,无法判断是否存在另外一个数,其出现频次也是数组长度的一半。如果题目被如此限定,就需要记录最后一个段落的位置,在整个整个数组遍历之后,再次遍历最后一个段落,判断除去确定的Majority Number之外,是否存在另一个Majority Number,即在遍历过程中如果出现第三个数,则判定只有一个Majority Number,否则存在2个Majority Number,这样仍能保证Challenge的时间复杂度和空间复杂度。当然,在最坏的情况下,整个数组会被遍历2次

         搞定这道题目以后,第一个感受就是,千万不要把Easy题目Easy做,在复杂度上能降一级是一级。其次,一定要交流或者在网上找类似的解法来比较,一方面验证自己思路的正确性,另一方面,触类旁通。最后也是最重要的,不论题目如何简单,一定要多思考,一个朴素的想法有时可能并不能经得起推敲,最好能够证明想法的正确性,得到理论上的提升,这其实也是算法的本质。

         PS:本人第一次写blog,如果上述内容哪里说得不清楚甚至存在错误,欢迎大家指正~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值