【算法】数组中出现次数超过一半的数字

面试题39:数组中超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1, 2, 3, 2, 2, 2, 5, 4, 2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。

解法1

数字超出一半,随机选中该数字的概率就很大。可以随机选一个数字,然后用快排的Partition划分一下,小的放左边,大的放右边,如果当前在中间位置,就说明已经找到了排序后中间位置的那个数(中位数),就一定是要找的超过一半的数字。否则就比较一下位置,向左找向右找,递归这个过程。

Partition划分

作者写的这个划分,其实是把小的放左边,大于等于的放右边。

#include <stdlib.h>
#include "Array.h"
#include <exception>

//在min和max之间随机找一个数 
int RandomInRange(int min, int max) {
	//即随机数对距离取整,再加上最小数使其落在这个范围 
	int random = rand() % (max - min + 1) + min;
	return random;
}

//交换两指针所对应的值 
void Swap(int* num1, int* num2) {
	int temp = *num1;
	*num1 = *num2;
	*num2 = temp;
}

//在start和end中随机找一个数,在长为length的data数组中以此下标为界
//划分整个数组,使比它(所对应的数)小的数都在其左边,>=它的在右边 
int Partition(int data[], int length, int start, int end) {
	//输入合法性检查 
	if(data == nullptr || length <= 0 || start < 0 || end >= length)
		throw new std::exception();
	//随机取该范围内的一个划分点 
	int index = RandomInRange(start, end);
	//将其和最后一个位置的数进行交换(因为从边界的位置开始划分比较方便)
	Swap(&data[index], &data[end]);
	
	/*接下来作者的操作需要画图理解!*/

	int small = start - 1;//这只是方面后面在循环里都做++ 
	//从前到后遍历除最后位置之外的所有位置 
	for(index = start; index < end; ++ index) {
		//如果发现比分隔数小的数字 
		if(data[index] < data[end]) {
			++ small;//这时small才+1
			//注意index随着循环总是+1,但是small遇到一些>=分隔数的数字时
			//就会暂且落后一个身位,在>=分隔数的数字前面
			//当下一次遇到比分隔数小的数字时,就会执行++small踩上来 
			if(small != index)//这里去除无用操作,相等时即指向同一个比分隔数小的数字 
				//交换两个数,使得让small指针受阻的第一个>=分隔数的数字换过来 
				Swap(&data[index], &data[small]);//取而代之的是一个比分隔数小的数字 
			//每次交换完成之后,相当于为small指针破开了一个向前走的阻碍
			//这个阻碍就是>=分隔数的数字
		}
		//因为这个遍历会遍历完整个(除最后一个分隔数之外)数组
		//相当于去拿后面的小数字去砸阻碍small的大石头,能砸掉多少是多少
		//整个遍历完成后,small前面仍然会有(也可以没有)大石头(指>=分隔数的数字)
		//但是这些石头里不会再夹杂任何比分隔数小的数字了 
	}
	
	//所以,至此,数组里的情况是[小..小(small)][石..石(index)][分隔数(end)] 
	//(当然也可以没有小于分隔数的数字,或者没有>=分隔数的数字,这些情况都能通过) 
	//现在,small指向最后一个比分割数字小的数字,index指向分隔数前一个数字 

	++ small;//small向前走一步踩在接下来第一个石头上 
	Swap(&data[small], &data[end]);//将其和最后的分隔数交换 
	
	//现在数组情况:[小..小][分隔数(small)][石..石(index)石(end)]

	return small;//返回分隔数所在位置下标 
}

这没什么问题,但其实这样做以后,并不能达到作者在书上说的"小的放左边,大的放右边"这种效果,因为超过一半的数字很多,不止一个,使用上面的划分函数,那么与基准数相等的会被放到右边。所以即使找到了一堆超过一半的数字,还是会落在最左边的一个上,它往往不是中位数。

所以这个解法其实真正找的过程比较累,我在代码里加了一些输出看一下。

寻找超过一半的数字
#include<bits/stdc++.h>
#include "../Utilities/Array.h"
using namespace std;

//全局变量,用于指示是否出错
bool g_bInputInvalid = false;

//检查数组是否合法
bool CheckInvalidArray(int* numbers, int length) {
	g_bInputInvalid = false;
	if(numbers == nullptr && length <= 0)//数组不合法时
		g_bInputInvalid = true;//同样在这个全局变量上做出指示

	return g_bInputInvalid;
}

//确认number在长为length的numbers数组中是否超过一半
bool CheckMoreThanHalf(int* numbers, int length, int number) {
	//统计number在数组中出现的次数
	int times = 0;
	for(int i = 0; i < length; ++i) {
		if(numbers[i] == number)
			times++;
	}

	bool isMoreThanHalf = true;
	//如果没有超过一半
	if(times * 2 <= length) {
		g_bInputInvalid = true;//在全局变量上做出指示
		isMoreThanHalf = false;
	}

	return isMoreThanHalf;
}

//把上面两部分单独写到一个函数里可以和方法解耦,多个方法不用重复代码 

//====================方法1====================
int MoreThanHalfNum_Solution1(int* numbers, int length) {
	if(CheckInvalidArray(numbers, length))
		return 0;
	//长度的一半,数组中位数出现的位置 
	int middle = length >> 1;
	int start = 0;
	int end = length - 1;
	//随机划分一下,返回划分后的坐标位置 
	int index = Partition(numbers, length, start, end);
	cout<<"划分位置是"<<index<<",值是"<<numbers[index]<<endl;
	//只要划分后不在中位数位置,就一直循环
	//注意:这个Partition每次划分将>=它的都放在右边
	//所以右边的数往往会很多,特别是在这个题的这种大量一样数字的输入情况下 
	//并且在后续的递归中,这种情况也完全不会变好
	//不妨输出一下每次划分的位置看一下这种有点蠢的查找方式,, 
	while(index != middle) {
		if(index > middle) {//如果比中位数大 
			end = index - 1;//就继续在左边找 
			index = Partition(numbers, length, start, end);
			cout<<"划分位置是"<<index<<",值是"<<numbers[index]<<endl;
		} else {//如果比中位数小 
			start = index + 1;//就继续在右边找 
			index = Partition(numbers, length, start, end);
			cout<<"划分位置是"<<index<<",值是"<<numbers[index]<<endl;
		}
	}

	int result = numbers[middle];//最终结果就是划分在中间位置时(中位数)
	//最后要检查一下是不是确实超过一半,花O(n)时间不增加总时间复杂度 
	if(!CheckMoreThanHalf(numbers, length, result))
		result = 0;
	return result;
}

int main() {
	int numbers[]={1,2,3,2,2,2,2,2,2,2,2,2,5,4,2};
	MoreThanHalfNum_Solution1(numbers,15);
	return 0;
}

最终的运行结果:
这里写图片描述
最后面连续一串2,当数组很大的时候,这种情况更严重。

解法2

超过半数以上,说明它自己就比其它数字加起来还多,所以以其一可以敌全部,可以打擂台赛。

从第一个数字开始上擂台,遍历整个数组,挑战者数字和它相同,就把它分数+1,挑战者数字和它不同,就把它分数-1,减到0就换擂主,最终的赢家就是要找的数字。

//====================方法2====================
int MoreThanHalfNum_Solution2(int* numbers, int length) {
	if(CheckInvalidArray(numbers, length))//检查数组合法性 
		return 0;

	int result = numbers[0];//擂主一开始是第一个数字 
	int times = 1;//分数是1
	//遍历剩下的所有挑战者 
	for(int i = 1; i < length; ++i) {
		if(times == 0) {//如果分数掉到0了
			result = numbers[i];//有新的挑战者上台直接当擂主
			times = 1;//刚上台分数肯定是1 
		} else if(numbers[i] == result)//如果和擂主同族(数字一样) 
			times++;//分数+1,相当于给擂主加HP 
		else//如果不同族(数字不一样) 
			times--;//掉一分,相当于擂主花1HP打掉1HP的挑战者 
	}

	//最终检查一下找到的数字是不是确实超过一半 
	if(!CheckMoreThanHalf(numbers, length, result))
		result = 0;

	return result;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值