1. 程式人生 > >【演算法】陣列中出現次數超過一半的數字

【演算法】陣列中出現次數超過一半的數字

面試題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;
}