【力扣】摩尔投票(多数元素)系列
Leetcode 0169 多数元素
题目描述:Leetcode 0169 多数元素
分析
-
本题的考点:摩尔投票法。
-
本题存在很多做法
(1)做法1:排个序,中间第 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n⌋个数就是答案,时间复杂度是 O ( n × l o g ( n ) ) O(n \times log(n)) O(n×log(n))的。
(2)做法2:使用哈希表统计次数,空间复杂度是 O ( n ) O(n) O(n)的。
-
上述做法都不是最优的,下面提供最优做法,即摩尔投票法。设置两个变量
r、c
,分别记录当前数据以及当前数据的个数。 -
从前向后一次考察数组中每个数据
x
,如果c==0
,则让r=x, c=1
,表示现在有c
个库存r
,如果下一个元素值还是x
,则让c++
,即库存增加一个;如果不是x
的话,则让c--
,表示库存减少一个。则最后r
就一定是答案。可以使用反证法证明。 -
假设最后
r
不是答案,根据题目,这个多数元素出现次数比其余其他元素个数之和还多(即若有3个元素,多数元素至少有2;即若有4个元素,多数元素至少有3),其他元素不可能将多数元素消耗完,因此最后r
一定是答案。 -
与本题相关的题目有Leetcode 0229 求众数 II,这个题是让求解出现次数严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor ⌊3n⌋的元素,另外LC229还分析了如何求出现次数严格大于 ⌊ n k ⌋ \lfloor \frac{n}{k} \rfloor ⌊kn⌋的元素。
代码
- C++
class Solution {
public:
int majorityElement(vector<int>& nums) {
int r = 0, c = 0; // r: 当前存的数; c: 当前数的数量
for (int x : nums) {
if (!c) r = x, c = 1;
else if (x == r) c++;
else c--;
}
return r;
}
};
- Java
class Solution {
public int majorityElement(int[] nums) {
int r = 0, c = 0;
for (int x : nums) {
if (c == 0) {
r = x;
c = 1;
} else if (r == x) c++;
else c--;
}
return r;
}
}
时空复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),
n
为数组长度。 -
空间复杂度: O ( 1 ) O(1) O(1)。
Leetcode 0229 求众数 II
题目描述:Leetcode 0229 求众数 II
分析
-
本题的考点:摩尔投票法。
-
与本题相关的题目有Leetcode 0169 多数元素,这个题是让求解出现次数严格大于 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n⌋的元素。这是一类题目。
-
首先考虑如何解决本题,即找出出现次数严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor ⌊3n⌋的元素,之后再推广到 ⌊ n k ⌋ \lfloor \frac{n}{k} \rfloor ⌊kn⌋。类似于LC169,本题需要开辟两个仓库
r1、r2
以及记录仓库对应元素出现次数的变量c1、c2
。然后从左到右遍历整个数组,假设此时遍历到x
,步骤如下:(1)如果仓库1有数据并且等于
x
,则让仓库1的数量c1++
;否则如果仓库2有数据并且等于x
,则让仓库2的数量c2++
;否则如果仓库1是空的,让r1=x, c1++
;否则如果仓库2是空的,让r2=x, c2++
;否则说明两个仓库都是非空的,并且其中的元素都不等于x
,则让c1--, c2--
(相当于消耗了两个不同元素)。(2)之后再次遍历数组,统计一下
r1, r2
出现的次数,如果严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor ⌊3n⌋,则放入结果中。 -
下面需要证明这种做法是正确的,即如果某个数据出现的次数严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor ⌊3n⌋,则其一定会出现在
r1、r2
中。分为三种情况讨论(因为最多只能有两个答案):(1)在遍历的过程中,如果
r1、r2
存储的元素不是答案,则如果x
是答案的话,会从两个仓库中消耗两个不是答案的数据;(2)在遍历的过程中,如果
r1、r2
中某个仓库存储的是答案x
,当遍历到一个不是答案的数据时,此时当前元素会被消耗掉,另外有个不是答案的仓库中的数据也会被消耗掉一个,一共消耗掉两个不是答案的数据。(3)在遍历的过程中,如果
r1、r2
中存储的都是答案,则两者出现次数都严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor ⌊3n⌋,剩余不是答案的数据不可能消耗完这两个答案对应的次数。 -
(3)显然成立,下面考虑(1)(2),这两种情况都会消耗掉两个数据。为了消耗掉我们的答案,我们需要两倍答案出现的次数的其他数据,这是不可能的,因为答案出现的次数严格大于 ⌊ n 3 ⌋ \lfloor \frac{n}{3} \rfloor ⌊3n⌋的。若是这样的话,我们总共需要的数据个数为 3 × ( ⌊ n 3 ⌋ + 1 ) 3 \times (\lfloor \frac{n}{3} \rfloor + 1) 3×(⌊3n⌋+1),可以分类讨论得出这个数一定是大于
n
的。 -
至此,证明了这种做法是正确的。
-
上述写法使用
C++
实现。 -
我们可以将这种做法推广到 ⌊ n k ⌋ \lfloor \frac{n}{k} \rfloor ⌊kn⌋,我们需要的仓库数为
k-1
个。时间复杂度为 O ( n × k ) O(n \times k) O(n×k)。用Java
实现这种写法。
代码
- C++
class Solution {
public:
vector<int> majorityElement(vector<int>& nums) {
int r1, r2, c1 = 0, c2 = 0; // r存储值, c存储值出现次数
for (auto x : nums) {
if (c1 && r1 == x) c1++;
else if (c2 && r2 == x) c2++;
else if (!c1) r1 = x, c1++;
else if (!c2) r2 = x, c2++;
else c1--, c2--; // 两个仓库都非空,并且存储的值都不等于x
}
c1 = 0, c2 = 0; // 使用c统计r出现的次数
for (auto x : nums) {
if (x == r1) c1++;
else if (x == r2) c2++;
}
vector<int> res;
int n = nums.size();
if (c1 > n / 3) res.push_back(r1);
if (c2 > n / 3) res.push_back(r2);
return res;
}
};
- Java
class Solution {
static final int K = 3; // 统计出现次数超过n/K下取整的数
public List<Integer> majorityElement(int[] nums) {
int[] r = new int[K - 1], c = new int[K - 1]; // r存储值, c存储值出现次数
Arrays.fill(c, 0);
for (int x : nums) {
boolean flag = false;
for (int i = 0; i < K - 1; i++)
if (c[i] != 0 && r[i] == x) {
c[i]++;
flag = true;
break;
}
if (flag) continue;
for (int i = 0; i < K - 1; i++)
if (c[i] == 0) {
r[i] = x; c[i]++;
flag = true;
break;
}
if (flag) continue;
for (int i = 0; i < K - 1; i++) c[i]--; // 仓库都非空,并且存储的值都不等于x
}
Arrays.fill(c, 0); // 使用c统计r出现的次数
for (int x : nums) {
for (int i = 0; i < K - 1; i++)
if (r[i] == x) {
c[i]++;
break;
}
}
List<Integer> res = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < K - 1; i++)
if (c[i] > n / K) res.add(r[i]);
return res;
}
}
时空复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),
n
为数组长度。 -
空间复杂度: O ( n ) O(n) O(n)。