文章目录
- 169.多数元素(找频率>n/2,且多数元素一定存在)
- 思路
- 完整版
- 补充:
- 注意点
- 面试题 17.10. 主要元素(找频率>n/2,但多数元素不一定存在)
- 思路
- 完整版
- 229.多数元素Ⅱ(找频率>n/3)
- 思路
- 最开始的写法
- 修改完整版
- debug测试:解答错误
- 总结:找频率>n/3元素与找>n/2元素的区别
看这篇总结:【算法】摩尔投票法 找 多数元素_小威W的博客-CSDN博客
169.多数元素(找频率>n/2,且多数元素一定存在)
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
提示:
- n ==
nums.length
- 1 <= n <= 5 * 104
- -10^9 <=
nums[i]
<= 10^9
思路
这道题目是找出现频率高于n/2的元素,是投票法。
投票法(Boyer-Moore Voting Algorithm)是一种用于在数组中查找主要元素的算法,主要元素定义为一个元素出现次数超过数组长度的一半。它并不一定能找到频率最高的元素,例如在数组 [1, 2, 2, 3, 3, 3]
中,频率最高的元素是 3
,但没有元素出现次数超过数组长度的一半,因此投票法不会返回任何元素。如果数组 [1, 1, 2, 2, 3, 3, 3, 3]
中,频率最高的元素也是主要元素,这时投票法会返回元素 3
。
如果一个元素是数组的多数元素(出现次数超过数组长度的一半),那么即使我们把它和其他每个不同的元素一一抵消(每次都从数组中删除两个不同的元素),最后剩下的一定是这个多数元素。
- 基本的投票法是找**数组中出现频率超过半数(必须是超过不能是等于)**的元素。
- 数组中出现频率超过半数的元素,一定只有一个!
完整版
class Solution {
public:
int majorityElement(vector<int>& nums) {
int ans=-1,count=0;
for(int num:nums){
if(count==0) ans = num;
//这样写的话,else会和直接相邻的上一个if组成if-else
if(num==ans) count++;
else count--;
}
return ans;//注意ans就是多数元素!
}
};
补充:
在C++中,else
语句总是与最近的一个未配对的if
语句进行配对。因此,上面写法中,else
语句是与第二个if
语句配对的,形成了一个if-else
结构。
上面是比较直观的写法,也可以换成另一种逻辑更清晰的:
class Solution {
public:
int majorityElement(vector<int>& nums) {
int ans=-1,count=0;
for(int num:nums){
if(count==0) ans = num;
count += (num==ans)?1:-1;
}
return ans;//注意ans就是多数元素!
}
};
注意点
- ans就是多数元素,因为只要count没有抵消到0,ans就会存放当前累积的频率最大的(还没有被抵消掉的)候选结果,只有count抵消到0了,才会重新开始寻找下一个相同的元素。
- 可以举出用例{2,2,2,2,3,2,3,3,3,3}来进行尝试,各占一半是会被抵消掉的。
- 上面这个写法,基于的大前提是数组中一定存在多数元素。否则,类似{2,2,2,2,3,2,3,3,3,3,4}这样的用例,得到的多数元素就会是4(因为前面2和3都抵消了),但是这个用例实际上是没有多数元素的。
面试题 17.10. 主要元素(找频率>n/2,但多数元素不一定存在)
数组中占比超过一半的元素称之为主要元素。给你一个 整数 数组,找出其中的主要元素。若没有,返回 -1
。请设计时间复杂度为 O(N)
、空间复杂度为 O(1)
的解决方案。
示例 1:
输入:[1,2,5,9,5,9,5,5,5]
输出:5
示例 2:
输入:[3,2]
输出:-1
示例 3:
输入:[2,2,1,1,1,2,2]
输出:2
思路
本题是多数元素不一定存在的情况,也就是说,我们根据投票法找到元素之后,需要进行二阶段的判断,判断这个元素出现次数是不是真的>n/2!
对抗阶段的代码与上一题 169.多数元素 相同,但是本题需要增加计数阶段,也就是验证元素出现次数。
完整版
- 如果count=0,先赋初值
class Solution {
public:
int majorityElement(vector<int>& nums) {
//对抗阶段,先用投票法抵消找出一个备选值
int ans=0,count=0;
for(int num:nums){
if(count==0) ans=num;//ans不需要赋特别大的初值因为会直接覆盖
count+= (num==ans)?1:-1;
}
//计数阶段,验证这个备选值是不是真的出现次数高于n/2
int cnt=0;
for(int num:nums){
if(num==ans) cnt++;
}
//判断次数是不是超过n/2
if(cnt>nums.size()/2){
return ans;
}
return -1;
}
};
229.多数元素Ⅱ(找频率>n/3)
给定一个大小为 n 的整数数组,找出其中所有出现超过 ⌊ n/3 ⌋
次的元素。
示例 1:
输入:nums = [3,2,3]
输出:[3]
示例 2:
输入:nums = [1]
输出:[1]
示例 3:
输入:nums = [1,2]
输出:[1,2]
提示:
1 <= nums.length <= 5 * 104
-10^9 <= nums[i] <= 10^9
思路
这道题的问题是:在一个数组中找到所有出现次数大于n/3的元素。
大于n/3的元素,且不包含相等,也就是说一个数组里最多只有两个这样的元素。和基础的投票法思路相同,我们可以先预设两个备选值,再看后面的元素经过count的抵消,能不能继续保留这两个备选值。
算法分为两个阶段:
- 对抗阶段:遍历数组,找出两个候选的多数元素
a1
和a2
。对于遍历到的每个元素num
,如果num
等于a1
或a2
,则增加a1
或a2
的计数;否则,如果a1
或a2
的计数为0,将num
设置为a1
或a2
,并将对应的计数设置为1;如果num
、a1
和a2
都不相等,并且a1
和a2
的计数都不为0,则num
、a1
和a2
互相抵消,a1
和a2
的计数都减1。 - 计数阶段:再次遍历数组,统计
a1
和a2
的出现次数。如果a1
或a2
的出现次数大于n/3,则将其添加到结果列表。
最开始的写法
- 本题没有说是不是真的存在多数元素,因此也需要计数阶段进行验证。
- 这种写法是错误的,因为三目运算符不能一次性执行多行语句,而在if-else中,我们需要同时更新候选元素num1和count1,再同时更新num2和count2。如果想用三目运算符,需要运用pair数据结构来确保一次性更新两个元素。
class Solution {
public:
vector<int> majorityElement(vector<int>& nums) {
//对抗阶段,写法和上面的主要元素是一样的
int num1=0,count1=0;
int num2=0,count2=0;
for(int num:nums){
//先赋初值
if(count1==0) num1=num;
else if(count2==0) num2=num;
//累积频率,这里count2的更新是有问题的,count2最开始没有赋值是不能-1的
count1 += (num1==num)?1:-1;
count2 += (num2==num)?1:-1;
}
//计数阶段,验证是不是真的>n/3
int cnt1=0,cnt2=0;
for(int num:nums){
c1+=(num=nums1)?1:0;
c2+=(num=nums2)?1:0;
}
//存储结果
vector<int>result;
if(c1>num.size()/3) result.push_back(nums1);
if(c2>num.size()/3) result.push_back(nums2);
return result;
}
};
修改完整版
- 注意,不能先判断count=0,因为本题有两个候选元素,防止影响两个候选元素的累加!
- 元素num不等于任何一个候选,且不需要替代任何一个候选的时候,才进行抵消
class Solution {
public:
vector<int> majorityElement(vector<int>& nums) {
//对抗阶段,因为题目提示中的最大值是10^9,因此设置初始值为1e9+1
int num1 = 1e9+1,cnt1=0;
int num2 = 1e9+1,cnt2=0;
for(int num:nums){//这种写法num就是nums[i]的数值,没有下标概念
if(num==num1) cnt1++;
else if(num==num2) cnt2++;
else if(cnt1==0){
num1=num;
cnt1++;
}
else if(cnt2==0){
num2=num;
cnt2++;
}
else{//出现新元素,且不等于任何一个候选,且不需要替代任何一个候选,就进行抵消
cnt1--;
cnt2--;
}
}
//对抗阶段结束后,num1就是第一个数字,num2就是第二个数字
cnt1=0;
cnt2=0;
for(int num:nums){
cnt1+=(num==num1)?1:0;
cnt2+=(num==num2)?1:0;
}
vector<int>result;
if(cnt1>nums.size()/3) result.push_back(num1);
if(cnt2>nums.size()/3) result.push_back(num2);
return result;
}
};
debug测试:解答错误
这个错误是因为最开始,对抗阶段的逻辑出现了错误。对抗阶段逻辑最开始的写法:
//对抗阶段,因为题目提示中的最大值是10^9,因此设置初始值为1e9+1
int num1 = 1e9+1,cnt1=0;
int num2 = 1e9+1,cnt2=0;
for(int num:nums){//这种写法num就是nums[i]的数值,没有下标概念
//这里的写法是错误的,有两个候选元素都需要累积!
if(cnt1==0){
num1=num;
cnt1++;
}
else if(cnt2==0){
num2=num;
cnt2++;
}
else if(num==num1) cnt1++;
else if(num==num2) cnt2++;
else{
cnt1--;
cnt2--;
}
}
这里的问题在于,本题我们有两个候选元素,我们必须先判断这两个候选元素自身能不能进行累积,才能判断count是否==0!
否则例如上图中{2,2}这种用例,只有一个候选元素在累积,那么另一个元素count=0的判定如果放在前面,就会导致原有的==num1的元素没有被累积到。
修改为先判定num==num1/num==num2
,再判断count==0的情况,即可
总结:找频率>n/3元素与找>n/2元素的区别
这两个问题之间的主要区别在于正在寻找的元素的数量。
在查找主要元素的问题中,出现次数超过了n/2的只可能有一个元素。在这种情况下,当count变为0时,可以安全地开始考虑一个新的候选元素,因为不可能有两个元素都出现次数超过n/2。所以当count为0时,可以肯定之前的元素不可能是主要元素,所以重新设置一个候选主要元素是合理的。
然而,当寻找的是出现次数超过n/3的元素时,一个数组有可能存在两个这样的元素。
所以,不能直接在count为0时就更改候选元素,必须先判断新元素是不是==num1/==num2!如果直接在count=0的时候更改候选元素,可能会导致另一个候选元素无法累积,例如{2,2}这样的情况。
也就是说,n/3的情况,我们需要同时追踪两个候选元素(num1和num2)并维护它们的count。出现一个新元素。首先进行两个候选元素的累积判断,才能再判断这两个候选元素的count是否为0,为0的时候进行替换。