超级水王问题
描述:
在一个数组中,如果存在一个数,在该数组中出现的次数大于数组长度的一半,那么这个数就是超级水王数。给定一个数组,要求在O(N)的时间复杂度下找出超级水王数,如果不存在,则返回-1.
暴力求解
乍一看,超级水王数???很高端的词诶,但是在看完描述之后那种害怕的感觉直接就消散而去,这不就简简单单的计数问题吗?的确,最容易也是最直观的想法是用一个哈希表记录各个数值以及对应出现过的次数,如果在遍历过程中发现某数已经出现次数超过数组长度的一半了,就可以直接返回答案了。
贴上代码:
int getSuperWaterKingNumber(vector<int>& nums) {
//标准做法: 使用哈希表记录每个元素对应的出现次数
int bound = nums.size() >> 1;//边界数: 长度 / 2
unordered_map<int, int> hash_map;
for (auto& num : nums) {
if (++hash_map[num] > bound)
return num;
}
return -1;
}
优雅Accept
如上的哈希求解方式虽然满足了题目中的时间复杂度O(N)的要求,那要是此时将限制修改为: 时间复杂度O(N), 空间复杂度为O(1)。
此时该如何求解此问题。。。。
这里分享一个特别精妙的思路:
求解数组中出现次数超过一半的数字,超过一半的意思是:如果长度为5,那么需要大于 5 / 2 = 2,也就是该数最少出现三次,如果数组长度为6,同理需要大于 6 / 2 = 3,该数需要最少出现四次),可以有一种操作手段是:
对数组中的数字,每次寻找两个数:
- 如果两个数不相等,那么将这两个数同时去除
- 如果两个数相等,那么不做操作。
这样做留下来的数字可能就是水王数,如果数组中存在水王数,那么我们这样做完后剩下的数就一定是水王数,为什么可以这么判断?
原因很简单,如果数组中存在水王数,也就是说一个数出现次数超过了 N / 2,假设在极端情况下,其余的都不相等,并且每一次都与水王数匹配上(也就是跟水王数一起被删除),那么最多可以删掉的水王数个数为N/2, 而水王数的个数为 >N/2,所以很明显有剩下来的。如果不是极端情况,那么很可能其余的数之间也存在“自相残杀”的现象。
情况分析
如果存在水王数,如 numbers = [1, 2, 3, 1, 1, 1]
- 第一二个数字不相等,同时删除, numbers = [2, 3, 1, 1, 1]
- 第二三个数字不相等,同时删除, numbers = [1, 1, 1]
- 剩下的数字全部都相等,没法删除,算法终止。
此时得到的1就是最终的水王数。
为什么在之前说根据删除的思路最后留下来的数不一定是水王数呢?
看个例子:
数组numbers = [1, 2, 3, 4, 5]
按照顺序进行删除:
- 第一二个数字不相等,同时删除,numbers = [3, 4, 5]
- 第二三个数字不相等,同时删除,numbers = [5]
- 此时只剩一个元素,算法终止。
很显然,如果就轻易判断5为水王数那就错了,因为它只出现了一次,并不符合条件,因此,需要在得到这个剩余数之后再对原数组进行遍历计数,才能最终判断该数是否为水王数。
算法实现
思路有了,也验证完了可行性,那么接下来如何实现用程序这个算法?这是个问题
如何高效且简洁地达到上述效果?
算法只需要两个变量即可完成,一个变量candidate表示候选,另一个变量blood表示当前候选的血量。
当血量为0时,表示当前没有候选,需要立一个候选数
当血量不为0时
- 如果出现了与candidate候选数不等的数,那么就将candidate的血量减一,如果血量变为0,则表示需要另立一个candidate。
- 出现了与candidate相等的数,那么血量blood加一。
贴代码:
int getSuperWaterKingNumber(vector<int>& nums) {
int candidate = 0, blood = 0;
for (int& num : nums) {
if (blood == 0) {
candidate = num;
blood = 1;
}
else if (num == candidate) {
++blood;
}
else {
--blood;
}
}
int count = 0;
for (int& num : nums) {
if (num == candidate)
++count;
}
return count > (nums.size() >> 1) ? candidate : -1;
}
- 时间复杂度O(N)
- 空间复杂度O(1)