优质博文:IT-BLOG-CN
给定一个大小为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
-109 <= nums[i] <= 109
进阶: 尝试设计时间复杂度为O(n)
、空间复杂度为O(1)
的算法解决此问题。
【1】哈希映射(HashMap): 来存储每个元素以及出现的次数。对于哈希映射中的每个键值对,键表示一个元素,值表示该元素出现的次数。我们用一个循环遍历数组nums
并将数组中的每个元素加入哈希映射中。在这之后,我们遍历哈希映射中的所有键值对,返回值最大的键。我们同样也可以在遍历数组nums
时候使用打擂台的方法,维护最大的值,这样省去了最后对哈希映射的遍历。
class Solution {
public int majorityElement(int[] nums) {
// 思想: 通过 hashMap 存放 value 和 count , 如果 count > n/2 直接返回
if (nums.length == 0) {
return 0;
}
int mid = nums.length/2;
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
if (map.getOrDefault(nums[i], 0) > mid) {
return nums[i];
}
}
return 0;
}
}
时间复杂度: O(n)
其中n
是数组nums
的长度。我们遍历数组nums
一次,对于nums
中的每一个元素,将其插入哈希表都只需要常数时间。如果在遍历时没有维护最大值,在遍历结束后还需要对哈希表进行遍历,因为哈希表中占用的空间为O(n)
(可参考下文的空间复杂度分析),那么遍历的时间不会超过O(n)
。因此总时间复杂度为O(n)
。
空间复杂度: O(n)
哈希表最多包含n−⌊n/2⌋
个键值对,所以占用的空间为O(n)
。这是因为任意一个长度为n
的数组最多只能包含n
个不同的值,但题中保证nums
一定有一个众数,会占用(最少)⌊n/2⌋+1
个数字。因此最多有n−(⌊n/2⌋+1)
个不同的其他数字,所以最多有n−⌊n/2⌋
个不同的元素。
【2】排序: 如果将数组nums
中的所有元素按照单调递增或单调递减的顺序排序,那么下标为⌊n/2⌋
的元素(下标从0
开始)一定是众数。
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return (nums[nums.length/2]);
}
}
时间复杂度: O(nlogn)
将数组排序的时间复杂度为O(nlogn)
。
空间复杂度: O(logn)
如果使用语言自带的排序算法,需要使用O(logn)
的栈空间。如果自己编写堆排序,则只需要使用O(1)
的额外空间。
【3】Boyer-Moore 投票算法: 如果我们把众数记为+1
,把其他数记为−1
,将它们全部加起来,显然和大于0
,从结果本身我们可以看出众数比其他数多。
oyer-Moore
算法的本质和分治十分类似。我们首先给出Boyer-Moore
算法的详细步骤:
【1】我们维护一个候选众数candidate
和它出现的次数count
。初始时candidate
可以为任意值,count
为0
;
【1】我们遍历数组nums
中的所有元素,对于每个元素x
,在判断x
之前,如果count
的值为0
,我们先将x
的值赋予candidate
,随后我们判断x
:如果x
与candidate
相等,那么计数器count
的值增加1
;如果x
与candidate
不等,那么计数器count
的值减少1
。在遍历完成后,candidate
即为整个数组的众数。
我们举一个具体的例子,例如下面的这个数组:
[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
在遍历到数组中的第一个元素以及每个在 | 之后的元素时,candidate
都会因为count
的值变为0
而发生改变。最后一次candidate
的值从5
变为7
,也就是这个数组中的众数。
Boyer-Moore
算法的正确性较难证明,这里给出一种较为详细的用例子辅助证明的思路,供读者参考:首先我们根据算法步骤中对count
的定义,可以发现:在对整个数组进行遍历的过程中,count
的值一定非负。这是因为如果count
的值为0
,那么在这一轮遍历的开始时刻,我们会将x
的值赋予candidate
并在接下来的一步中将count
的值增加1
。因此count
的值在遍历的过程中一直保持非负。
那么count
本身除了计数器之外,还有什么更深层次的意义呢?我们还是以数组
[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
作为例子,首先写下它在每一步遍历时candidate
和count
的值:
nums: [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
candidate: 7 7 7 7 7 7 5 5 5 5 5 5 7 7 7 7
count: 1 2 1 2 1 0 1 0 1 2 1 0 1 2 3 4
我们再定义一个变量value
,它和真正的众数maj
绑定。在每一步遍历时,如果当前的数x
和maj
相等,那么value
的值加1
,否则减1
。value
的实际意义即为:到当前的这一步遍历为止,众数出现的次数比非众数多出了多少次。我们将value
的值也写在下方:
nums: [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
value: 1 2 1 2 1 0 -1 0 -1 -2 -1 0 1 2 3 4
有没有发现什么?我们将count
和value
放在一起:
nums: [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
count: 1 2 1 2 1 0 1 0 1 2 1 0 1 2 3 4
value: 1 2 1 2 1 0 -1 0 -1 -2 -1 0 1 2 3 4
发现在每一步遍历中,count
和value
要么相等,要么互为相反数!并且在候选众数candidate
就是maj
时,它们相等,candidate
是其它的数时,它们互为相反数!
为什么会有这么奇妙的性质呢?这并不难证明:我们将候选众数candidate
保持不变的连续的遍历称为「一段」。在同一段中,count
的值是根据candidate == x
的判断进行加减的。那么如果candidate
恰好为maj
,那么在这一段中,count
和value
的变化是同步的;如果candidate
不为maj
,那么在这一段中count
和value
的变化是相反的。因此就有了这样一个奇妙的性质。
这样以来,由于:我们证明了count
的值一直为非负,在最后一步遍历结束后也是如此;由于value
的值与真正的众数maj
绑定,并且它表示「众数出现的次数比非众数多出了多少次」,那么在最后一步遍历结束后,value
的值为正数;
在最后一步遍历结束后,count
非负,value
为正数,所以它们不可能互为相反数,只可能相等,即count == value
。因此在最后「一段」中,count
的value
的变化是同步的,也就是说,candidate
中存储的候选众数就是真正的众数maj
。
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
算法只对数组进行了一次遍历。
空间复杂度: O(1)
,Boyer-Moore
算法只需要常数级别的额外空间。