本文作者: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,如果上述内容哪里说得不清楚甚至存在错误,欢迎大家指正~