39 数组中出现次数超过一半的数字(时间效率)
题目描述:
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
测试用例:
功能测试(输入的数组中存在一个出现次数超过数组长度一半的数字;输入的数组中不存在一个出现次数超过数组长度一半的数字)
特殊输入测试(输入的数组中只有一个数字;输入nullptr指针)
解题思路:
1) 基于Partition函数的时间复杂度为O(n)的算法: 会修改原来的数组,如果要求不能修改,则不能选择该方法
数组的特性:数组中有一个数字出现的次数超过了数组长度的一半。如果把这个数组排序,那么排序之后位于数组中间的数字一定就是那个出现次数超过数组长度的一半。即长度为n的数组中第n/2大的数字。有成熟的时间复杂度为O(n)的算法得到数组中任意第K大的数字。
思路:
受快速排序的算法的启发。在随机快速排序算法中,我们先在数组中随机选择一个数字,然后调整数组中数字的顺序,使得比选中的数字小的数字都排在它的左边,比选中数字大的数字都排在它的右边。如果选中的数字下标刚好是n/2,那么这个数字就是数组的中位数;如果它的下标大于n/2,那么它的中位数应该位于它的左边,我们可以接着在它的左边部分数组中查找;如果它的下标小于n/2,那么中位数应该位于它的右边,我们可以接着在它的右边部分的数组中查找。
代码:
实现1:使用自己定义的Swap()
class Solution { public: int (vector<int> numbers) { //检查输入是否有效:输入为空时 if(numbers.empty()) return 0; //输入不为空 //int mid = (numbers.size()+1)>>1; int end = numbers.size()-1; int begin = 0; int mid = (end-begin+1)>>1; int index = Partition(numbers, begin, end); while(index!=mid){ //不是中位数的时候 if(index>mid){//中位数在(begin,index)之间 end = index-1; index = Partition(numbers, begin, end); //begin end 范围不断缩小 //index = Partition(numbers, begin, index-1); //error: end没有被更新,下一次迭代执行line22时,会遍历到最后。 }else{ //不能用两个if,只能执行一个 //if(index<mid) begin = index+1; index = Partition(numbers, begin, end); } } //判断是否出现多次 int res = numbers[mid]; int count =0; for(index=0;index<numbers.size();index++) if(numbers[index]==res) count++; //if(count>mid) //不用加等号 if(count*2<=numbers.size()) return 0; //return res; return res; } // int Partition(vector<int> & numbers,int begin,int end){ if(numbers.empty() || begin<0 || end>=numbers.size() || begin>end) return -1; //还是return0?? int index = randomInRange(begin,end); //随机选择数组中的一个数 Swap(numbers,index,end); //可以直接调用函数么?取引用!!! //int small=-1; //error int small=begin-1; for(index=begin;index<end;index++){ //遍历一遍数组 从begin开始而不是从0开始。处理后半部分数组时,begin!=0 if(numbers[index]<numbers[end]){ //小的树放在左边 small++; if(small!=index) Swap(numbers,index,small); } } small++; Swap(numbers,end,small); return small; } void Swap(vector<int> & numbers,int a,int b){ if(a==b) //一定要有判断,否则会报错 数组中只剩1个元素时,会出现begin==end即a==b return; numbers[a]=numbers[a]+numbers[b]; //a==b时,存储的值会变为2倍 numbers[b]=numbers[a]-numbers[b]; //a==b时,存储的值变为0 numbers[a]=numbers[a]-numbers[b]; //a==b结果永远返回0 } int randomInRange(int begin,int end){ return (rand()% (end-begin+1))+ begin; } };
代码中注意事项:
1. 自己实现的swap函数,由于是对数组的元素进行交换,且没有使用临时变量,当对一个数组元素自己与自己交换时(a==b)无论是什么值,最后都会为0。因为最后计算的都是自己减自己。
因此swap要加上判断,当a==b时,不执行交换。
当数组中只剩下一个元素时,begin==end,此时交换时,a==b
2. 产生随机数
要取得[a,b)的随机整数,使用(rand() % (b-a))+ a;
要取得[a,b]的随机整数,使用(rand() % (b-a+1))+ a;
要取得(a,b]的随机整数,使用(rand() % (b-a))+ a + 1;
通用公式:a + rand() % n;其中的a是起始值,n是整数的范围。
3. 书写Partition函数时,注意,每次传入的范围是[begin,end],两者都在不断的缩小范围,begin只有第一次使用的时候能确定是0,其他调用处不确定是否已经修改。因此在定范围时:small与for循环变量index要用begin初始化,而不是使用0!!!
4. MoreThanHalfNum_Solution函数:
在while循环中要不断的更新end = index-1;与begin = index+1;这样下次调用Partition函数时会会缩小范围。而不是直接传入index-1或者index+1,本次调用的end或者begin范围变小,但下次调用使用end或者begin时,传入并没有减小。因此每次要更新一下。
实现2:使用系统自定义swap函数
class Solution { public: int MoreThanHalfNum_Solution(vector<int> numbers) { //检查输入是否有效:输入为空时 if(numbers.empty()) return 0; //输入不为空 //int mid = (numbers.size()+1)>>1; int end = numbers.size()-1; int begin = 0; int mid = (end-begin+1)>>1; int index = Partition(numbers, begin, end); while(index!=mid){ //不是中位数的时候 if(index>mid){//中位数在(begin,index)之间 end = index-1; index = Partition(numbers, begin, end); //begin end 范围不断缩小 //index = Partition(numbers, begin, index-1); //error: end没有被更新,下一次迭代执行line22时,会遍历到最后。 }else{ //不能用两个if,只能执行一个 //if(index<mid) begin = index+1; index = Partition(numbers, begin, end); } } //判断是否出现多次 int res = numbers[mid]; int count =0; for(index=0;index<numbers.size();index++) if(numbers[index]==res) count++; if(count>mid) //不用加等号 return res; //if(count*2<=numbers.size()) //该判断也可以 //return 0; return 0; } // int Partition(vector<int> & numbers,int begin,int end){ if(numbers.empty() || begin<0 || end>=numbers.size() || begin>end) return -1; //还是return0?? int index = randomInRange(begin,end); //随机选择数组中的一个数 swap(numbers[index],numbers[end]); //Swap(numbers,index,end); //可以直接调用函数么?取引用!!! //int small=-1; //error int small=begin-1; for(index=begin;index<end;index++){ //遍历一遍数组 从begin开始而不是从0开始。处理后半部分数组时,begin!=0 if(numbers[index]<numbers[end]){ //小的树放在左边 small++; if(small!=index) swap(numbers[index],numbers[small]); //Swap(numbers,index,small); } } small++; swap(numbers[end],numbers[small]); //Swap(numbers,end,small); return small; } int randomInRange(int begin,int end){ return (rand()% (end-begin+1))+ begin; } };
2)使用数组特点,找出时间复杂度为O(n)的算法
数组中有一个数字出现的次数超过长度的一半,即;它出现的次数比其他所有数字出现的次数和还要多。
在遍历数组时保存两个值:一个是数组中的一个数字;另一个是次数。当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加1;如果下一个数字和我们保存的数字不同,则次数减1。如果次数为零,那么我们需要保存下一个数字,并把次数设为1。由于我们要找的数字出现的次数比其他所有数字出现的次数之和多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。
class Solution { public: int MoreThanHalfNum_Solution(vector<int> numbers) { if(numbers.empty()) return 0; int res = numbers[0]; int count = 1; for(int i=1;i<numbers.size();i++){ if(count==0){ //要先判断!! 三个条件用else连接,因为每次只执行一次即可 res = numbers[i]; count++; //当前值有一个 }else if(numbers[i]==res){ count++; }else{ count--; } } //检查找到的数字是否出现了n/2次 count = 0; for(int i=0;i<numbers.size();i++){ if(numbers[i]==res) count++; } if(count*2<=numbers.size()) return 0; return res; } };
3)数组排序后,如果符合条件的数存在,则一定是数组中间那个数。由于涉及到快排sort,其时间复杂度为O(NlogN)并非最优;
使用内置sort函数
class Solution { public: int MoreThanHalfNum_Solution(vector<int> numbers) { // 因为用到了sort,时间复杂度O(NlogN),并非最优 if(numbers.empty()) return 0; sort(numbers.begin(),numbers.end()); // 排序,取数组中间那个数 int middle = numbers[numbers.size()/2]; int count=0; // 出现次数 for(int i=0;i<numbers.size();++i) { if(numbers[i]==middle) ++count; } return (count>numbers.size()/2) ? middle : 0; } };
4)使用map,增加了空间复杂度。且map操作要log(n),因此算法整体复杂度nlog(n) 不建议使用
class Solution { public: int MoreThanHalfNum_Solution(vector<int> numbers) { int n = numbers.size(); //map 记录出现次数 map<int, int> m; int count; for (int i = 0; i < n; i++) { count = ++m[numbers[i]]; if (count > n/2) return numbers[i]; } return 0; } };