[数学 求众数] 169. 多数元素 229. 求众数 II (哈希表、摩尔投票法)
169. 求众数 I(寻找一个出现次数 > n/2的元素)
题目链接:https://leetcode-cn.com/problems/majority-element/
分类:
- 数学(众数、抽屉原理、摩尔投票法)
- 哈希表(key = 元素值,value = 出现次数)
思路1:数组排序 + 中位数=众数 (O(NlogN), O(logN))
Arrays.sort()的时间复杂度为O(NlogN),空间复杂度为O(logN).
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length/2];
}
}
思路2:哈希表 O(N),O(N)
创建一个哈希表,key=数字,value=出现的次数,遍历完整个数组,完成map的填充之后,再遍历一次map,找出value最大的key,即为众数。
- 时间复杂度O(N)
空间复杂度O(N)
思路3:分治法 O(NlogN),O(logN)
一个数如果是数组的众数,则把数组分成大小相同的两部分后,该众数在至少其中一个部分里仍然是众数。
可以用反证法证明:
设一个众数x既不是左半部分的众数也不是右半部分的众数,设左半部分的大小为left,右半部分的大小为right,该数字x的数量 < left/2,也 < right/2,则数字 x 的数量 < (left+right)/2,则数字x不是众数,与假设的前提矛盾,所以这个结论是正确的。
根据这个结论,对数组进行二分,分别找出划分的两部分里的众数,如果得到的两个众数相等,则该数字就是最终的众数;如果两个众数不相等,则两者之中必有一个是众数,所以对这两个数做众数判断,找出其中的众数。
其中,对一个数做众数判断就是遍历整个数组,判断它出现的次数是否 > 数组大小的一半。
-
时间复杂度:O(NlogN),二分的时间复杂度为O(logN),众数判断需要遍历整个数组,统计目标数字出现的次数,所以时间复杂度为O(N),整合起来整体时间复杂度为O(NlogN))
空间复杂度:O(logN),递归用到的系统栈,也可以改用迭代,可以把空间复杂度降为O(1).
思路4:摩尔投票法 O(n),O(1)
分析:遍历数组,设数组的第一个数字num,创建一个计数器times表示num的出现次数,置初值times=1;
在遍历过程中,每遇到值相同的元素,times+1,遇到值不同的数字,times-1,当times=0时,num更换为当前的数字,times重新置1,继续遍历。
在遍历结束后,num就是要求的数字。
上面的算法使用到了一个结论:每次从序列里选择两个不相同的数字删除掉(或称为“抵消”),最后剩下一个数字或几个相同的数字,就是出现次数大于总数一半的那个。
证明:
首先,可以证明最终不会一个数字都不剩。
原因: 假设两两抵消之后,最终一个数字都不剩。那么就是说一共有偶数个数字,假设有n个,那么n = 2k,k是整数。所以是进行了k次两两抵消。又因为一定存在众数 (数量超过⌊n/2⌋ = k的数字 ),所以该众数出现次数至少为k+1。由抽屉原理,这就会导致前面两两抵消的某一对数字是一样的。这是矛盾的。所以这就证明了最终不会一个数字都不剩,至少剩下一个。
假设最终剩下的那一种数字是a,假设前面进行了k次两两抵消。要证明a是欲求的众数,即证明其他数字不可能是众数。由于抽屉原理,在前面抵消的数字中,同一种数字最多出现k次,即是除了a之外的数字最多出现k次。而且最终至少剩下一个数字,所以数字的总数量大于等于2k+1。那么除了a之外的数字出现的频率<= k/(2k+1) < k/2k = 1/2,所以证明了除了a之外的数字均不会是众数。那么就是说最终剩下的那种数字a是所求众数。
链接:https://www.zhihu.com/question/49973163/answer/477886752
class Solution {
public int majorityElement(int[] nums) {
//特殊用例
if(nums.length == 1) return nums[0];
//变量创建和初始化
int num = nums[0], times = 1;
//遍历数组
for(int i = 1; i < nums.length; i++){
if(nums[i] == num)
times++;
else{
times--;
if(times == 0){
num = nums[i];
times = 1;
}
}
}
return num;
}
}
229. 求众数 II (寻找所有出现次数 > n/3的元素)
题目链接:https://leetcode-cn.com/problems/majority-element-ii/
分类:
- 数学(众数问题的变形、摩尔投票法的一般形式)
- 哈希表(key = 元素值,value = 出现次数)
思路1:哈希表
使用一个哈希表,key=元素值,value=元素出现的次数,每有一个元素对应的value>n/3,就将其加入最终的解集。
class Solution {
public List<Integer> majorityElement(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
List<Integer> res = new ArrayList<>();
for(int num : nums){
int value = map.getOrDefault(num, 0);
if(value + 1 == nums.length / 3 + 1) res.add(num);//取等号避免重复加入
map.put(num, value + 1);
}
return res;
}
}
- 时间复杂度:O(N)
- 空间复杂度:O(N)
思路2:摩尔投票法
首先,我们可以预先得出数组中满足条件的众数数量:
假设数组存在x个出现次数>n/3的元素,如果x==3,则这三个元素的总出现次数 > n,不可能,所以x < 3.所以 出现次数 >n/3 的元素个数 <= 2.
我们可以使用两个变量p1,p2来存放可能的众数,count1,count2来记录这两个众数的出现次数,初始值count1=count2=0。
实现上需要对169题的摩尔投票法做变形:
例如:[A, B, C, A, A, B, C]
最开始拿前三个元素A,B,C比较,因为三个元素都不相等,所以互相抵消;
拿A,A,B比较,因为存在两个元素相等,所以令p1=A,count1=2,p2=b,count2=1;
在p1,p2被赋值后,后面每次只拿一个元素来比较,取C和p1,p2比较,都不相等,所以count1-1=1,count2-1=0。
数组遍历结束,p1对应的count1>0,p2对应的count2==0,但还不能确定p1就一定是众数,因为可能存在如:[A,B,C,D,E,F,G,G,F],最后剩下p1=G,count1=2,p2=F,count2=1,但这两个变量都不是满足条件的众数,所以需要再遍历一次数组做众数校验,用最朴素的方法检验可能的众数是否真的是众数,检验部分的时间复杂度为O(N),投票部分的时间复杂度为O(N),整体时间复杂度为O(2N)=O(N)。
为了代码更容易实现,我们在初始时就令p1=nums[0],p2=nums[0],然后从数组的第0个元素开始遍历,每次拿nums[i]和p1,p2比较,这样每次只需要从数组中提取一个元素和p1,p2比较:
- 如果该元素和p1或p2相等,则对应的count+1;
- 如果都不相等,则先判断当前count的值:
- 如果存在count == 0,就拿该元素覆盖对应的p,再重新置count = 1;
- 如果两个count > 0,则两个count都-1。
需要注意的是,上述四个分支都是互斥的,一次只能进入一个分支。
class Solution {
public List<Integer> majorityElement(int[] nums) {
List<Integer> res = new ArrayList<>();
if(nums == null || nums.length == 0) return res;
int p1 = nums[0], p2 = nums[0], count1 = 0, count2 = 0;
for(int num : nums){
if(p1 == num){
count1++;
continue;
}
if(p2 == num){
count2++;
continue;
}
if(count1 == 0){
p1 = num;
count1++;
continue;
}
if(count2 == 0){
p2 = num;
count2++;
continue;
}
count1--;
count2--;
}
//最后检验p1,p2是不是众数
count1 = 0;
count2 = 0;
for(int num : nums){
if(num == p1) count1++;
else if(num == p2) count2++;
}
if(count1 > nums.length / 3) res.add(p1);
if(count2 > nums.length / 3) res.add(p2);
return res;
}
}