超级水王问题
问题描述
在一个数组中,某一项出现的次数大于数组长度的一半,那么这个数就称为水王数。
当评论区一般以上的评论都来自某个用户,则表示这个人在灌水。
要求
- 时间复杂度为 O ( n ) O(n) O(n)
- 空间复杂度为 O ( 1 ) O(1) O(1)
解题思路
我们先抛开要求不谈,最容易想到的就是遍历一遍数组,用Map这种数据结构记录下每个数出现的次数,然后遍历一遍Map集合,如果有值大于数组长度一般的键,则说明存在水王数,并且该键就是对应水王数。
//普通解法,通过一个Map集合实现,时间复杂度和空间复杂度都为O(n)
public static int water(int arr[]){
//如果存在水王数,则返回对应的数字,否则返回-1
//如果传入的数组为空数组,则返回-1
if (arr == null || arr.length == 0)
return -1;
//建立一个HashMap
HashMap<Integer,Integer> map = new HashMap();
for (int num : arr) {
if (map.containsKey(num)){
map.put(num, (int)map.get(num) + 1);
}else {
map.put(num, 1);
}
}
Set<Map.Entry<Integer, Integer>> entrySet = map.entrySet();
for (Map.Entry<Integer, Integer> record : entrySet) {
if (record.getKey() > (arr.length >> 1))
return record.getKey();
}
return -1;
}
那这种解题方式,它的时间复杂度和空间复杂度是多少呢?很明显,时间复杂度和空间复杂度都为 O ( n ) O(n) O(n),也就是说空间复杂度不符合要求,那么有没有更加优雅的解法呢?
优化解法
当然,既然这个问题被提出来,相对应的就必然有其对应的解法。
注:作者对该解法理解源自左程云老师在B站发布的视频内容,有兴趣的小伙伴可以去查看相关课程。
如果一个数组存在水王数,那么可以肯定的就是水王数在数组中的个数一定比其他全部数加起来的个数都多(由定义可知),根据这一点,我们是不是可以遍历一遍数组,如果遍历到两个不相同的数,就把这两个数移除数组,这样剩下的数不就一定是水王数吗(前提是数组中存在水王数)。
如果数组中不存在水王数,例如1,2,3,4,5
,经过上面描述的操作,最后数组只剩下数字5,很显然5并不是水王数。基于这种情况的存在,我们在得到最后一个数的时候,需要再遍历一遍数组,计算该数在数组中出现的次数,如果次数大于长度的一半,那么毫无疑问该数是水王数,反之则不是。
注:如果数组经过移除操作,最后没有剩下数如[1,2,3,4,5,6]
,那么该数组必然就不存在水王数。
//优雅的水王问题解法(候选值和血量)
public static int waterKing(int[] arr){
//判断输入的数组是否为空
if (arr == null || arr.length == 0){
return -1;
}
//初始化候选人和血量
int candidate = 0, HP = 0;
for (int cur : arr) {
if (HP == 0){
candidate = cur;
HP = 1;
}else if (candidate == cur) {
HP++;
} else {
HP--;
}
}
//遍历一遍之后,判断血量情况,如果血量为0则没有水王数
if (HP == 0)
return -1;
//此时可能存在水王数,遍历数组,查看当前数在数组中出现的次数
int count = 0;
for (int num : arr) {
if (num == candidate)
count++;
}
return count > (arr.length >> 2)?candidate:-1;
}
注:在代码中,解题思路的移除替换为了更高效的血量表示,当遇到相同的数,血量+1,反之血量-1,继续遍历后面的数,如果血量为0,则下一个数标记为候选人,并给初始血量1(优雅)。