腾讯2016招聘笔试:微信红包
春节期间小明使用微信收到很多个红包,非常开心。在查看领取红包记录时发现,某个红包金额出现的次数超过了红包总数的一半。请帮小明找到该红包金额。写出具体算法思路和代码实现,要求算法尽可能高效。
给定一个红包的金额数组gifts及它的大小n,请返回所求红包的金额。
若没有金额超过总数的一半,返回0。
输入:
[1,2,3,2,2],5
输出
2
思路:
刚刚读完这道题,我的第一想法是利用map数组记录每一个红包金额出现的次数,最后在与数组长度的一半进行比较看是否符合题目要求。但是问题要求算法尽可能高效,map底层采用红黑树实现,查找的时间复杂度为O(n)但是对于空间要求要高一些(因为要记录每一个节点的父节点)。因此我又想到了用哈希表实现的unordered_map(查找效率要高于map),但是unordered_map在建立时会比较费时,另外二者都不能直观的体现算法的思想,因此我又参考了一些文献,最后总结了两个较为高效的方法。
方法1:利用partition方法实现部分快速排序找到中位数
算法思想:
分析题目,题目要求找到出现次数大于数组长度一半的红包金额。不难想到,这样一个数一定是序列中的中位数,因此我们只需要找到该数组的中位数,最后判断该中位数出现的次数是否超过数组长度的一半即可。讲到这里,可能就会有部分同学直接采用quick_sort对数组排序求得中位数(下标为2/n的数),但是快排的时间复杂度为O(nlogn),对于使用map的提升并不是很大。这里我们想一下在求第k个小的数时,我们采用了一种部分快速排序的方法(partition)。我们在数组中随机选取一个数,利用partition函数返回该数在初步排序后的数组中的下标,如果下标刚好为2/n,则说明该数便是数组的中位数。假设下标大于2/n说明中位数在该数的左侧,在左侧继续递归查找即可,反之则在该数的右边查找。找到中位数之后在与数组长度的一半比较,看是否符合题意输出即可。
算法复杂度:
该算法的时间复杂度大致为T(n) = n+n/2+n/4+n/8+….+1,在n 的值较大的情况下T(n)趋于2n,也就是O(n)的复杂度,这是在平均情况下,最坏情况时(每次抽取的数在排序后总是在数组的最左边或者最右边)时间复杂度为
T(n) = n+n-1+n-2+n-3+….+1 = n(n-1)/2.也就是O(n*n)。
空间复杂度为O(1)
程序实现:
#include<bits/stdc++.h>
using namespace std;
int partition(int a[],int left,int right);
int getval(int a[],int n){
int left = 0;
int right = n-1;
int mid = (right-left)/2;
int index = partition(a,left,right);
while(index!=n/2){
if(index<n/2){
left = index+1;
index=partition(a,left,right);
}
else{
right = index-1;
index=partition(a,left,right);
}
}
int val = a[index];
int cnt=0;
for(int i=0;i<n;i++){
if(a[i]==val){
cnt++;
}
if(cnt*2>n){
return val;
}
}
return 0;
}
int partition(int a[],int left,int right){
int key = a[left];
int mid = (left+right)/2;
while(left<right){
while(right>left&&a[right]>=key){
right--;
}
while(left<right&&a[left]<=key){
left++;
}
int t = a[left];
a[left] = a[right];
a[right] = t;
}
return left;
}
int main(){
int a[100];
int n;
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
int b = getval(a,n);
cout<<b<<endl;
return 0;
}
方法二:加一减一抵消法
算法思想:
红包金额的出现次数大于总数的一半,意味着该红包金额出现的次数大于其他全部金额出现的次数之和。所以我们可以将第一个元素作为标记,设计一个计数变量cnt,遍历数组,如果出现同样的红包金额,则cnt+1;否则cnt-1。当cnt减少至0时意味着该金额不是我们所求金额,替换a[i](i为当前遍历到的下标)作为新的标记,cnt=1,继续遍历。
最后在遍历一遍数组,判断该金额出现的次数是否大于总数的一半,输出即可。
算法复杂度:
时间复杂度:因为总共遍历了两次数组,所以为O(n)
空间复杂度:O(1)
#include<bits/stdc++.h>
using namespace std;
int panduan(int a[],int n,int val);
int getval(int a[],int n){
if(n<=0)return -1;
int time = 1;
int sol = a[0];
for(int i=1;i<n;i++){
if(sol==a[i]){
time++;
}
else if(a[i]!=sol){
if(time-1==0){
sol=a[i];
time=1;
continue;
}
time--;
}
}
return panduan(a,n,sol);
}
int panduan(int a[],int n,int val){
int cnt=0;
for(int i=0;i<n;i++){
if(a[i]==val){
++cnt;
}
}
if(cnt!=0&&cnt>n/2){
return val;
}
else{
return 0;
}
}
int main(){
int a[100];
int n;
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
int b = getval(a,n);
cout<<b<<endl;
return 0;
}