Easy | LeetCode 169 | 剑指 Offer 39. 数组中出现次数超过一半的数字
剑指 Offer 39. 数组中出现次数超过一半的数字
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
方法一:Hash计数
先遍历一遍数组, 将每个数字出现的次数存到HashMap当中, 然后再遍历HashMap, 找是否有超过一半的数字, 如果有, 立即返回true。否则返回false;
方法二:多数投票算法(Boyer-Moore Algorithm)
基本算法:两趟扫描, 第一趟扫描, 找到可能成为这个超过一半数字的候选, 第二趟扫描, 看这个候选出现的次数是否超过一半。
找候选的过程如下:
维持一个计数器, 初始值为0。
当计数器为0时, 设置当前值为候选值。并且将计数器自增一次。
当计数器不为0时, 如果当前值与候选值相同, 则计数器自增, 如果不相同, 则计数器自减。
当遍历结束时, 如果计数器大于0, 则可能存在一个超过一半的数字。如果真的有, 那就是候选值。
public int majorityElement(int[] nums) {
int candidate = nums[0];
int count = 1;
for(int i = 1; i < nums.length; i++) {
if (count == 0) {
candidate = nums[i];
count = 1;
} else if (nums[i] == candidate){
count += 1;
} else {
count -= 1;
}
}
return candidate;
}
为什么这种策略能够成功?
(1) 我们先分析数组中只有1,0的情况, 我们看这两个数字, 谁比谁多, 多的那个一定是多数元素(超过一半数量)。比如[1,1,0,0,0,1,0]。对于只有1,0的情况, 最简单的想法是我直接设两个计数器统计个数进行比较。一个更好的想法是:我设置一个计数器, 当遇到1时, 自增, 当遇到0时, 自减。这样我遍历完成时, 如果计数器大于0, 则说明1多, 自减意味着0的个数和1的个数相互抵消了, 计数器的最终值就是1比0多出来的个数。
(2) 上面0,1的应该是非常好理解的, 我现在在0,1数字引入其他数字来分析。比如[1,1,0,1,2,0,3]。在这种情况下, 候选值和计数器值如下:
1 | 1 | 0 | 1 | 2 | 2 | 3 | |
---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
candidate | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
count | 1 | 2 | 1 | 2 | 1 | 0 | 1 |
当下标为[0,5]时, 候选值是1, count值代表为1比其他数字多出来的数字, 这个是比较容易理解的。当count变为0时, 代表当前候选值1被其他数字抵消完了, 1不可能成为多数元素。这个也是比较好理解的。有人可能要问了, 为什么就把count为0的哪个值(这里是3), 作为候选值, 在下标[0,5]中间, 不还是有其他数吗?他们为什么不能作为候选值呢?比如例子里有2个2, 他们为什么不能当做候选值?
其实思路很简单,候选值1在下标[0,5]区间内, 计数器有1开始, 最后变为0, 这个区间内的所有数字, 都是不可能成为多数元素的(至少遍历到下标5时, 这么看是没有问题的, 至于后面某个数字会不会一直增加超过一半, 对当前的判断无关紧要)。因为,当count = 0时, 1的数字, 目前是出现了一半, 而对于不是1的其他数字, 他们的出现的次数, 一定小于或等于当前1的次数, 所以, 这中间出现的元素一定不是多数元素。要理解, 这种策略, 本身是一种“减法策略”。每次count = 0时, 他就把不可能成为多数元素的值给“减掉”, 最后会留下一个值, 它可能成为多数元素。