给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在众数。
- 示例 1:
输入: [3,2,3]
输出: 3 - 示例 2:
输入: [2,2,1,1,1,2,2]
输出: 2
我的方案
思路
题目要求找到众数,即为数组中重复次数最多的值。那么可以利用哈希表键唯一的特性,将重复次数作为键存储。
算法
建立哈希表,将出现的唯一数值作为键,其键重复次数作为值存储。之后就可以对建立的哈希表进行遍历,找到出现次数最多的值。
代码
class Solution {
public int majorityElement(int[] nums) {
Map map = new HashMap<Integer,Integer>();
int maxNum=0;
for(int i =0;i<nums.length;i++){
if(!map.containsKey(nums[i])){
map.put(nums[i],1);
}else{
int value = (int)map.get(nums[i]);
map.put(nums[i],++value);
}
}
Set<Integer> set = map.keySet();
for(Iterator<Integer>it = set.iterator();it.hasNext();){
Integer key = it.next();
int value = (int)map.get(key);
if(value>nums.length/2)
maxNum = key;
}//因为只会有一个众数,所以只要大于数组一半的即为值。
return maxNum;
}
官方方案一 排序法
思路
如果所有数字被单调递增或者单调递减的顺序排了序,那么众数的下标为
n/2(当 n 是偶数时,下标为(n/2)+1)。前提条件是,数组中一定有一个重复数大于数组大小的一半,那么排序数组的中间数一定是众数。
算法
对于这种算法,我们先将 nums 数组排序,然后返回上面所说的数字。下面解释了为什么这种策略是有效的。考虑下图(上面的例子是一个可能的奇数的情况,下面的例子是一个可能的偶数的情况):
对于每种情况,数组下面的线表示如果众数是数组中最小值的情况下覆盖的下标。数组上面的线是数组中最大值的情况。其他情况,这条线会在这两种极端情况的中间。但我们看到即使是这两种极端情况,它们也会在下标为 n/2 的地方有重叠。因此,无论众数是多少,返回 n/2下标对应的值都是正确的。
代码
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length/2];
}
}
复杂度分析
-
时间复杂度:O(nlgn)
数组排序开销为 O(nlgn) ,它占据了运行的主要时间。 -
空间复杂度:O(1) 或者 O(n)O(n)
我们将 nums 就地排序,如果不能就低排序,我们必须使用线性空间将 nums 数组拷贝,然后再排序。
官方方案二 随机化
思路
因为超过 【n/2】的数组下标被众数占据了,一个随机的下标很有可能存有众数。
算法
由于一个给定的下标对应的数字很有可能是众数,我们随机挑选一个下标,检查它的值是否是众数,如果是就返回,否则继续随机挑选。
class Solution {
//随机数函数
private int randRange(Random rand, int min, int max) {
return rand.nextInt(max - min) + min;
}
//总计数量函数
private int countOccurences(int[] nums, int num) {
int count = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == num) {
count++;
}
}
return count;
}
public int majorityElement(int[] nums) {
Random rand = new Random();
int majorityCount = nums.length/2;
while (true) {
int candidate = nums[randRange(rand, 0, nums.length)];
if (countOccurences(nums, candidate) > majorityCount) {
return candidate;
}
}
}
}
复杂度分析
-
时间复杂度:O(∞)
理论上这个算法有可能跑无穷次(如果我们一直无法随机到众数),所以最坏时间复杂度是没有上限的。然而,运行的期望时间远小于无限次的 - 线性时间即可。为了更简单地分析,先说服你自己:由于众数占据 超过 数组一半的位置,期望的迭代次数会小于众数占据数组恰好一半的情况。因此,我们可以计算迭代的期望次数(下标为 prob 为原问题, mod 为众数恰好占据数组一半数目的问题):
因为级数会收敛,修改后问题的迭代期望次数是个常数。所以修改后问题的运行时间为线性的。因此,原问题期望运行时间也是线性的。 -
空间复杂度:O(1)
就像暴力解,随机方法只需要常数级别的额外空间。
官方方案三 分治法
思路
如果我们知道数组左边一半和右边一半的众数,我们就可以用线性时间知道全局的众数是哪个。
算法
这里我们使用经典的分治算法递归求解,直到所有的子问题都是长度为 1 的数组。由于传输子数组需要额外的时间和空间,所以我们实际上只传输子区间的左右指针 lo 和 hi 表示相应区间的左右下标。长度为 1 的子数组中唯一的数显然是众数,直接返回即可。如果回溯后某区间的长度大于 1 ,我们必须将左右子区间的值合并。如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。否则,我们需要比较两个众数在整个区间内出现的次数来决定该区间的众数。原问题的答案就是下标为 0 和 n 之间的众数这一子问题。
class Solution {
//范围内记数函数
private int countInRange(int[] nums, int num, int lo, int hi) {
int count = 0;
for (int i = lo; i <= hi; i++) {
if (nums[i] == num) {
count++;
}
}
return count;
}
private int majorityElement(int[] nums, int lo, int hi) {
//数组中剩下的唯一元素就是众数
if (lo == hi) {
return nums[lo];
}
//在左边和右边的部分递归
int mid = (hi-lo)/2 + lo;
int left = majorityElement(nums, lo, mid);
int right = majorityElement(nums, mid+1, hi);
// 如果两部分的众数相等,则返回
if (left == right) {
return left;
}
// 否则,返回两部分中众数总数更大的那一个。
int leftCount = countInRange(nums, left, lo, hi);
int rightCount = countInRange(nums, right, lo, hi);
return leftCount > rightCount ? left : right;
}
public int majorityElement(int[] nums) {
return majorityElemenet(nums, 0, nums.length-1);
}
}
复杂度分析
-
时间复杂度:O(nlgn)
函数 majority_element_rec 会求解 2 个长度为n/2 的子问题,并做两遍长度为 n 的线性扫描。因此,分治算法的时间复杂度可以表示为:
根据 主定理,本题满足第二种情况,所以时间复杂度可以表示为:
-
空间复杂度:O(lgn)
尽管分治算法没有直接分配额外的数组空间,但因为递归的过程,在栈中使用了一些非常数的额外空间。因为算法每次将数组从每一层的中间断开,所以数组长度变为 1 之前只有 O(lgn) 次切断。由于递归树是平衡的,所以从根到每个叶子节点的长度都是 O(lgn) 。由于递归树是深度优先的,空间复杂度等于最长的一条路径,也就是 O(lgn) 。
官方方案四 Boyer-Moore 投票算法
思路
如果我们把众数记为 +1 ,把其他数记为 -1 ,将它们全部加起来,显然和大于 0 ,从结果本身我们可以看出众数比其他数多。
算法
本质上, Boyer-Moore 算法就是找 nums 的一个后缀 suf ,其中 suf[0] 就是后缀中的众数。我们维护一个计数器,如果遇到一个我们目前的候选众数,就将计数器加一,否则减一。只要计数器等于 0 ,我们就将 nums 中之前访问的数字全部 忘记 ,并把下一个数字当做候选的众数。直观上这个算法不是特别明显为何是对的,我们先看下面这个例子(竖线用来划分每次计数器归零的情况)
[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
首先,下标为 0 的 7 被当做众数的第一个候选。在下标为 5 处,计数器会变回0 。所以下标为 6 的 5 是下一个众数的候选者。由于这个例子中 7 是真正的众数,所以通过忽略掉前面的数字,我们忽略掉了同样多数目的众数和非众数。因此, 7 仍然是剩下数字中的众数。
[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 5, 5, 5, 5]
现在,众数是 5 (在计数器归零的时候我们把候选从 7 变成了 5)。此时,我们的候选者并不是真正的众数,但是我们在 遗忘 前面的数字的时候,要去掉相同数目的众数和非众数(如果遗忘更多的非众数,会导致计数器变成负数)。
因此,上面的过程说明了我们可以放心地遗忘前面的数字,并继续求解剩下数字中的众数。最后,总有一个后缀满足计数器是大于 0 的,此时这个后缀的众数就是整个数组的众数。
我的总结:由于众数性质,其数量超过总数一半,投票法就是通过不断拟定候选人,不断抵消同样多的众数与非众数,最后剩下的总是众数。
class Solution {
public int majorityElement(int[] nums) {
int count = 0;
Integer candidate = null;
for (int num : nums) {
if (count == 0) {
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
return candidate;
}
}
-
复杂度分析
时间复杂度:O(n)
Boyer-Moore 算法严格执行了 n 次循环,所以时间复杂度是线性时间的。 -
空间复杂度:O(1)
Boyer-Moore 只需要常数级别的额外空间。