前言
用Java来求一个数组的众数,可使用HashMap、栈等数据结构完成。
一、例题、解答
1、例题
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
2、解答
A、HashMap
用HashMap快速解题,记录每个元素出现的个数,如果个数大于 len / 2 ,认为其就是众数。
public int majorityElement(int[] nums) {
//Hash Map快速解题
//用map记录num的长度,然后判断它是否大于len/2
Map<Integer, Integer> record = new HashMap<>();
int len = nums.length;
for (int i = 0; i < len; i++) {
int n = record.getOrDefault(nums[i], 0) + 1;
if (n > len / 2)
return nums[i];
record.put(nums[i], n);
}
return 0;
}
B、排序
对于数组,可对其排序,而众数一定在中间。
public int majorityElement2(int[] nums) {
//排序,值必然在中间
Arrays.sort(nums);
return nums[nums.length / 2];
}
C、栈
如果栈为空 或者 栈顶 == num,则将 num 进栈;
否则,不将 num 入栈且栈顶元素出栈;
这样最后栈里的元素都是众数。
public int majorityElement3(int[] nums) {
//用栈来进入所有元素,一旦碰到不同元素就出栈,由于个数大于数组的一半,所以最终剩下的元素都为返回值。
Deque<Integer> record = new LinkedList<>();
for (int i = 0; i < nums.length; i++) {
if (!record.isEmpty() || record.peek() == nums[i]) {
record.push(nums[i]);
continue;
}
record.pop();
}
return record.peek();
}
D、比武招亲
专业名称也叫摩尔选票,根据数组元素来记录当前值的票数,如果等于,则票数加1,否则-1。减为0时,重新记录新值。
将数组分为两组,一组为众数,一组为非众数。众数的个数 > 非众数的个数。所以最终会记录的数为众数。
public int majorityElement4(int[] nums) {
//比武招亲
int res = 0, count = 0;
for (int num : nums) {
if (count == 0) {
res = num;
count = 1;
continue;
}
count += num == res ? 1 : -1;
}
return res;
}
E、Random
将数组分为两组,一组为众数,一组为非众数。众数的个数 > 非众数的个数。所以随机选择一个数统计其次数,然后看它的次数是否 > len / 2,来大概率随机寻找众数。
//random
public int majorityElement5(int[] nums) {
//既然有个数它的个数这么多,我们随便挑选一个数统计它出现的次数,如果它的个数超过len/2,则返回
int res;
while (true) {
Random rand = new Random();
int index = getRandom(rand, 0, nums.length);
if (getCount(nums, nums[index]) > nums.length / 2)
return nums[index];
}
}
public int getRandom(Random rand, int min, int max) {
return rand.nextInt(max - min);
}
public int getCount(int[] nums, int n) {
int count = 0;
for (int num : nums) {
count += num == n ? 1 : 0;
}
return count;
}
F、分治
将数组对半砍,要么两组数组的众数一样,即为整个nums数组的众数;要么必有一半数组的众数为整个nums数组的众数,如果两边众数不一样,则统计各区间的众数个数。
//分治
public int majorityElement6(int[] nums) {
//将数组对半砍,要么两边的众数一样,要么必有一半有众数,如果两边众数不一样,则统计各区间的众数个数
return getResByRecursion(nums, 0, nums.length - 1);
}
public int getCountByLR(int[] nums, int n, int left, int right) {
int count = 0;
for (int i = left; i <= right; i++) {
count += n == nums[i] ? 1 : 0;
}
return count;
}
public int getResByRecursion(int[] nums, int left, int right) {
if (left == right)
return nums[left];
//注意mid的取法mid != right/2,而是(left+right)/ 2 或者(right -left) / 2 + left;
int mid = (left + right) / 2;
int leftN = getResByRecursion(nums, left, mid);
int rightN = getResByRecursion(nums, mid + 1, right);
if (leftN != rightN) {
return getCountByLR(nums, leftN, left, mid) > getCountByLR(nums, rightN, mid + 1, right) ? leftN : rightN;
}
return leftN;
}
总结
1)HashMap,快速存取。Time:O(n),Space:O(n)
2)排序,Time:O(nlog2n),Space:O(1)
3)栈,Time:O(n),Space:O(n)
4)摩尔选票,Time:O(n),Space:O(1)
5)Random,Time:O(n2),Space:O(1),虽然事件复杂度高,但这是最坏情况,而平均情况的实际运行时间没这么高,取到众数的概率 1 / 2.
6)分治,T(n) = 2T(n/2) + 2n,Time:O(nlog2n),Space:O(log2n)
参考文献
[1] Leetcode原题
[2] 官方详解