《劍指Offer》面試題:超過陣列長度的一半的數
題目描述:
陣列中有一個數字出現的次數超過陣列長度的一半,請找出這個數字。
例如輸入一個長度為9的陣列{1,2,3,2,2,2,5,4,2}。由於數字2在陣列中出現了5次,超過陣列長度的一半,因此輸出2。
思路
解決此題的思路有很多。
1、最容易想到的方法:將陣列進行排序,取中位數即可。但是時間複雜度為O(nlogn)
2、考慮用雜湊,key儲存陣列元素,value儲存出現的次數,這樣在遍歷O(n)能做出key-value的對映,再用O(k)(k為需要的槽的個數)可以找出出現次數超過一半的key,但是由於陣列中元素的大小範圍未知,因此使用這種方法,首先不能確定雜湊表的大小,即使通過遍歷一次求得了最大值,範圍很大的話,又要花費很大心思設計很好的雜湊函式來完成key-value的對映,且不具有通用性,而且還要考慮陣列中元素為負值的情況,因此用雜湊表不合適。如果說數字只有0-9的話可以考慮設計一個Hash table,遍歷一次就能知道每個數字出現的次數。
3、思路1中需要將陣列進行排序,而事實上可以不用對陣列進行排序,
或者說僅部分排序,受快速排序的partition函式的啟發,
我們可以利用反覆呼叫partition函式來求的該數字。我們現在陣列中隨機選取一個數字,
而後通過Partition函式返回該數字在陣列中的索引index,如果index剛好等於n/2,則這個數字便是陣列的中位數,也即是要求的數,
如果index大於n/2,則中位數肯定在index的左邊,在左邊繼續尋找即可,反之在右邊尋找。這樣可以只在index的一邊尋找,
而不用兩邊都排序,減少了一半排序時間。這種情況的平均時間複雜度大致為:T(n) = n+n/2+n/4+n/8+….+1,很明顯當n很大時,T(n)趨近於2n,
也就是說平均情況下時間複雜度為O(n),但是這種情況下,最壞的時間複雜度依然為O(n*n),最壞情況下,index總是位於陣列的最左或最右邊,
這樣時間複雜度為T(n) = n+n-1+n-2+n-3+….+1 = n(n-1)/2,顯然,時間複雜度為O(n*n),空間複雜度為O(1)。
思路3的實現程式碼如下
/*
題目描述:
陣列中有一個數字出現的次數超過陣列長度的一半,請找出這個數字。
例如輸入一個長度為9的陣列{1,2,3,2,2,2,5,4,2}。由於數字2在陣列中出現了5次,超過陣列長度的一半,因此輸出2。
輸入:
每個測試案例包括2行:
第一行輸入一個整數n(1<=n<=100000),表示陣列中元素的個數。
第二行輸入n個整數,表示陣列中的每個元素,這n個整數的範圍是[1,1000000000]。
輸出:
對應每個測試案例,輸出出現的次數超過陣列長度的一半的數,如果沒有輸出-1。
樣例輸入:
9
1 2 3 2 2 2 5 4 2
樣例輸出:
2
*/
#include<stdio.h>
#include<stdlib.h>
//定義一個全域性變數,用來指示輸入的資料中是否有效。這裡的“有效”指的是:輸入的資料中次數超過一半的數
bool isValid=false;
/*
函式功能:判斷在陣列arr中result出現的次數是否為arr長度的一半以上。
引數的說明
@param arr:陣列的指標
@param len:陣列的長度
@param result:待檢測的數
*/
bool isMoreThanHalfInArray(int *arr,int n,int result){
if(arr==NULL||n<=0){
return false;
}
int times=0;
for(int i=0;i<n;i++){
if(arr[i]==result){
times++;
}
}
bool isHalf=false;
if(times*2>=n){
isHalf=true;
}
return isHalf;
}
void swap(int *a,int *b){
if(a!=NULL&&b!=NULL){
int temp=*a;
*a=*b;
*b=temp;
}
}
int partition(int *arr,int begin,int end){
if(arr==NULL||begin<0||end<0||begin>end){
return -1;
}
int index=begin;
//選取首元素為主元
for(int i=begin;i<=end;i++) {
if(arr[i]<arr[begin]){
index++;
swap(&arr[index],&arr[i]);
}
}
swap(&arr[begin],&arr[index]);//將主元交換到他應該在的位置
return index;
}
int findMoreThanHalfNumInArray(int *arr,int n){
if(arr==NULL||n<=0){//輸入資料無效
isValid=false;
return -1;
}
int begin=0;
int end=n-1;
int mid=n>>1;
int index=partition(arr,begin,end);//利用快排得到主元的索引
while(index!=mid) {
if(index>mid){//說明mid在index的左邊
end=index-1;
index=partition(arr,begin,end);
}
else{
begin=index+1;
index=partition(arr,begin,end);
}
}
//上面的while迴圈就找到的中位數,但是在返回之前,還是需要判斷此種位數的出現的次數是否達到了陣列長度的一半以上。若達到了,則返回就可以了
bool isMoreThanHalf=isMoreThanHalfInArray(arr,n,arr[index]);
if(!isMoreThanHalf){
isValid=false;
return -1;
}
return arr[index];
}
int main(void){
int n;
while(scanf("%d",&n)!=EOF&&n>0){
int *arr=(int *)malloc(n*sizeof(int));
if(arr==NULL)
exit(EXIT_FAILURE);
for(int i=0;i<n;i++){
int val;
scanf("%d",&val);
arr[i]=val;
}
isValid=true;
int result=findMoreThanHalfNumInArray(arr,n);
if(isValid){
printf("%d",result);
}
else{
printf("-1");
}
printf("\n");
}
return 0;
}
4、出現的次數超過陣列長度的一半,表明這個數字出現的次數比其他數字出現的次數的總和還多。所以我們可以考慮每次刪除兩個不同的數,那麼在剩下的數中,出現的次數仍然超過總數的一半。
通過不斷重複這個過程,不斷排除掉其它的數,最終找到那個出現次數超過一半的數字。
這個方法,免去了上述思路一的排序,也避免了思路2中空間O(N)的開銷,總得說來,時間複雜度只有O(N),空間複雜度為O(1),不失為最佳方法。
例:陣列 a[5]={0,1,2,1,1};
我們要查詢的數字為1,操作步驟為:遍歷整個陣列,然後每次刪除不同的兩個數字,過程如下:
0 1 2 1 1 =>2 1 1=>1
具體實現:我們在考慮刪除兩個不同的數字的時候,實際上可以通過計數來實現,而不是物理上真正的刪除。
在遍歷陣列的時候儲存兩個值:一個是陣列中的一個數字,一個是次數。當我們遍歷到下一個數字的時候,
如果下一個數字和我們之前儲存的數字相同,則次數加1。如果下一個數字和我們之前儲存的數字不同,則次數減1。
如果次數為零,我們需要儲存下一個數字,並把次數設為1。由於我們要找的數字出現的次數比其他所有數字出現的次數之和還要多,因此,最後一次把次數設定為1的數可能是我們要尋找的數,但不是一定。
注意:那麼要找的數字肯定是最後一次把次數設為1時對應的數字,網上都是這麼說,但此句話欠妥,例如:陣列為1、3、2;則最後一次把次數設為1的數字為2,但陣列中並沒有次數多於一般的數。因此,最後還需要判斷。
下面的實現程式碼是按照思路4來實現的。
/*
測試函式的要求如下
輸入:
每個測試案例包括2行:
第一行輸入一個整數n(1<=n<=100000),表示陣列中元素的個數。
第二行輸入n個整數,表示陣列中的每個元素,這n個整數的範圍是[1,1000000000]。
輸出:
對應每個測試案例,輸出出現的次數超過陣列長度的一半的數,如果沒有輸出-1。
樣例輸入:
9
1 2 3 2 2 2 5 4 2
樣例輸出:
2
*/
#include<stdio.h>
#include<stdlib.h>
bool isExist; //定義一個全域性變數,用來標識陣列中是否存在超過一半的數。 當為false時,表示,不存在並輸出-1.也是為了區分isExist=true,而超過一半的數為-1的情況。
/*
函式功能:判斷在陣列arr中result出現的次數是否為arr長度的一半以上。
引數的說明
@param arr:陣列的指標
@param len:陣列的長度
@param result:待檢測的數
*/
bool isMoreThanHalfNumInArray(int *arr,int len,int result){
if(arr==NULL||len<=0){
isExist=false;
return false;
}
int times=0;
for(int i=0;i<len;i++){
if(arr[i]==result){
times++;
}
}
bool isMoreThanHalf=false;
if(times*2>=len){
isMoreThanHalf=true;
}
return isMoreThanHalf;
}
/*
函式的功能:在陣列中尋找次數超過一半的數。
*/
int findMoreThanHalfNumInArray(int *arr,int len){
if(arr==NULL||len<=0){
isExist=false;
return -1;
}
int result;//用來儲存結果
int times=0;//用來儲存次數
for(int i=0;i<len;i++){
if(times==0){//如果times為零時,則應該儲存當前的數,並將times=1
result=arr[i];
times=1;
}
else if(result==arr[i]){
times++;
}
else{
times--;
}
}
//到這裡為止,我們還需要判斷reuslt是否真的是陣列中次數出現一半的數字。
if(!isMoreThanHalfNumInArray(arr,len,result)) {
isExist=false;
return -1;
}
return result;
}
int main(void){
int n;
while(scanf("%d",&n)!=EOF){
if(n>0){
int *arr=(int *)malloc(n*sizeof(int));
if(arr==NULL){
exit(EXIT_FAILURE);
}
int val;
for(int i=0;i<n;i++){
scanf("%d",&val);
arr[i]=val;
}
//開始尋找超過一半的數在陣列中
isExist=true;
int result=findMoreThanHalfNumInArray(arr,n);
if(isExist){
printf("%d",result);
}
else{
printf("-1");
}
printf("\n");
}
}
return 0;
}