【演算法】陣列中出現次數超過一半的數字
阿新 • • 發佈:2018-12-09
面試題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;
}