剑指Offer🌞问题28:找出数组中出现超过一半的数字
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
问题描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如:输入如下所示的一个长度为9的数组 {1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
解题思路
思路一:首先对数组进行排序,在一个有序数组中,次数超过一半的必定是中位数,那么可以直接取出中位数,然后遍历数组,看中位数是否出现次数超过一半,这取决于排序的时间复杂度,最快为 O(nlogn)。
思路二:遍历数组,用 HashMap 保存每个数出现的次数,这样可以从 map 中直接判断是否有超过一半的数字,这种算法的时间复杂度为 O(n),但是这个性能提升是用 O(n) 的空间复杂度换来的。
思路三:(最优解法):根据数组特点得到时间复杂度为 O(n) 的算法。根据数组特点,数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数之和还要多。因此,我们可以在遍历数组的时候设置两个值:一个是数组中的数 result,另一个是出现次数 times 。当遍历到下一个数字的时候,如果与 result 相同,则次数加1,不同则次数减一,当次数变为 0 的时候说明该数字不可能为多数元素,将 result 设置为下一个数字,次数设为 1。这样,当遍历结束后,最后一次设置的 result 的值可能就是符合要求的值(如果有数字出现次数超过一半,则必为该元素,否则不存在),因此,判断该元素出现次数是否超过一半即可验证应该返回该元素还是返回 0。这种思路是对数组进行了两次遍历,复杂度为 O(n)。
求解
方法一:先使用快排将数组排序,然后统计中间数字出现的次数。
public static int MoreThanHalfNum1(int[] array){
// 判断数组是否为空
if (array == null || array.length == 0){
System.out.println("数组为空");
return 0;
}
// 对数组进行排序 快排
sort(array, 0, array.length - 1);
int times = 1;
for (int i = 0; i < array.length; i++){
if (array[i] == array[array.length / 2])
times++;
}
if (times > array.length / 2)
return array[array.length / 2];
else
return 0;
}
// 快排
public static void sort(int[] array, int low, int high) {
if (low < high) {
int part = partition(array, low, high);//获取中间索引,将区间一分为二,分为两个子区间
sort(array, low, part - 1);//对前面的子区间快速排序
sort(array, part + 1, high);//对后面的子区间快速排序
}
}
//一趟快速排序,返回值是本次基准的最终索引位置
private static int partition(int[] array, int low, int high) {
int benchmark = array[low];//初始化基准,不妨将低位索引值赋给基准
//从数组的两端向中间开始扫描,寻找基准元素位置
while (low < high) {
//高位指针开始向中间寻找比基准小的元素
while ((low < high) && array[high] >= benchmark) {
high--;
}
//比基准小的高位索引元素赋值到低位索引
if (low < high) {
array[low] = array[high];
}
//低位指针开始向中间寻找比基准大的元素
while ((low < high) && (array[low] <= benchmark)) {
low++;
}
//比基准大的低位索引元素赋值到高位索引
if (low < high) {
array[high] = array[low];
}
}
//将基准元素归位,基准元素的索引位置就是两个索引指针相遇的位置
array[low] = benchmark;
return low;//返回基准元素的最终索引
}
方法二:遍历数组,用 HashMap 保存每个数出现的次数
// 使用 HashMap 保存每个数出现的次数
public static int MoreThanHalfNum2(int[] array){
// 判断数组是否为空
if (array == null || array.length == 0){
System.out.println("数组为空");
return 0;
}
Map<Integer, Integer> result = new HashMap<>();
for (int i=0; i<array.length; i++){
// getOrDefault(Object key, V defaultValue): 当 Map 集合中有 key 时,使用 key 对应的 value 值,若没有,则使用默认值 defaultValue
result.put(array[i], result.getOrDefault(array[i], 0)+1);
if (result.get(array[i]) > array.length / 2){
return array[i];
}
}
return 0;
}
方法三:利用数组特性
// 根据数组特点得到时间复杂度为 O(n) 的算法
public static int MoreThanHalfNum3(int[] array){
// 判断数组是否为空
if (array == null || array.length == 0){
System.out.println("数组为空");
return 0;
}
int result = array[0];
int times = 1;
for (int i = 0; i < array.length; i++){
// 若 result 出现的次数为 0 时,则表明当前值一定不会超过一半,即考虑数组下一个数
if (times == 0){
result = array[i];
times = 1;
continue;
}
if (array[i] == result)
times++;
else
times--;
}
for (int i = 0; i < array.length; i++){
// 若 reslut 与当前值相等,则 times 加 1
if (array[i] == result)
times++;
// 若 reslut 出现的次数大于一般,则返回 reslut
if (times > array.length / 2)
return result;
}
return 0;
}
完整代码
// 根据数组特点得到时间复杂度为 O(n) 的算法
public static int MoreThanHalfNum(int[] array){
// 判断数组是否为空
if (array == null || array.length == 0){
System.out.println("数组为空");
return 0;
}
int result = array[0];
int times = 1;
for (int i = 0; i < array.length; i++){
// 若 result 出现的次数为 0 时,则表明当前值一定不会超过一半,即考虑数组下一个数
if (times == 0){
result = array[i];
times = 1;
continue;
}
if (array[i] == result)
times++;
else
times--;
}
for (int i = 0; i < array.length; i++){
// 若 reslut 与当前值相等,则 times 加 1
if (array[i] == result)
times++;
// 若 reslut 出现的次数大于一般,则返回 reslut
if (times > array.length / 2)
return result;
}
return 0;
}
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 2, 2, 2, 5, 4, 2};
//数组
for(int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
System.out.println(" ");
int result = MoreThanHalfNum(array);
if (result != 0)
System.out.println("数组中出现超过一半的数字为:" + result);
else
System.out.println("数组中没有出现超过一半的数字,输出" + result);
}