1. 程式人生 > >出現次數超過一半的數(面試題)

出現次數超過一半的數(面試題)

出現次數超過一半的數

題目描述

陣列中有一個數出現的次數超過了陣列長度的一半,找出這個數。

分析與解法

因為不確定給定的陣列是無序還是有序的,所以要分情況討論。

解法一:排序

如果給定的陣列是無序的,那麼可以先對陣列進行排序(至於排序方法可選取最常用的快速排序)。排完序後遍歷陣列,在遍歷整個陣列的同時統計每個數的出現次數,然後把那個出現次數超過一半的數直接輸出,題目便算解答完了。總的時間複雜度為O(nlogn+n)

但是,如果給定的陣列是有序的,或者經過排序後把無序的陣列變成有序的之後,是否還需要再遍歷一次陣列,以統計每個數出現的次數呢?

實際上,如果某個數在陣列中的出現次數超過一半,那麼在已經排好序的陣列索引的

n/2 處(從零開始編號)就一定是要我的這個數。因此,對整個陣列排完序之後,只需要直接輸出陣列中的第n/2處的數即可,這個數即是整個陣列中出現次數超過一半的數,總的時間複雜度由於少了最後一次整個陣列的遍歷,而降到O(nlogn)

然而,時間複雜度從O(nlogn+n)降到O(nlogn)並無本質上的改變,我們需要找到一種更有效的思路或方法。

解法二:散列表

通常來說,要想降低時間複雜度,有這麼幾個思路可以選擇。

·減少不必要的操作,比如解法一中陣列排完序後可以直接輸出第n/2處的那個數,不必再統計每個數的出現次數。

·以空間換時間,比如藉助散列表達到快速對映的目的。

應根據問題本身的特性使用對應的技巧。比如在

KMP演算法中,通過對模式串的預處理求解出next陣列,而後匹配失敗時直接查next陣列便可得到下一次匹配的位置。

針對以空間換時間,我們自然而然想到了查詢時間複雜度為O(1)的散列表。首先用散列表完成陣列中每個數出現次數的統計,其中,散列表的鍵為陣列中的數,值為該數出現的次數。這樣,利用散列表完成統計後,如果需要找出那個出現次數超過一半的數,直接遍歷整個散列表,然後輸出該數即可。

構照敘列表後,查一次的時間複雜度為O(1),遍歷一遍查詢n次,則總的時間複雜度為O(1)。但是,散列表的方法需要O(n)的空間開銷,且要設計雜湊函式,還有沒有更好的辦法呢?

解法三:每次刪除兩個不同的數

根據這個問題本身的特殊性,可以試著這麼考慮,通過

每次刪除兩個不同的數(不管是不是我們要查詢的那個出現次數超過一半的數),在剩下的數中,我們要查詢的數的出現次數仍將會超過剩餘總數的一半。通過不斷重複這個過程,不斷排除掉其他的數,最終找到那個出現次數超過一半的數。總的說來,時間複雜度只有O(n),空間複雜度為O(1),免去了排序,也避免了O(n)的空間開銷。

舉個簡單的例子,如陣列a[5]={0, 1, 2, 1, 1}。很顯然,若要找出陣列a中出現次數超過一半的數,這個數便是1。通過一次性遍歷整個陣列,然後每次刪除不相同的兩個數,過程簡單表示如下。

1)給定序列0, 1, 2, 1, 1

2)刪除不相同的兩個數01,序列變為2, 1, 1

3)最後再刪去兩個不同的數21,序列變為1

4)最終1即為所要找的結果。

解法四:記錄兩個值

更進一步,我們可以在遍歷陣列的時候儲存兩個值:一個是candidate,用來儲存陣列中遍歷到的某個數;另一個是nTimes,表示當前數的出現次數,其中nTimes初始化為1。當遍歷到陣列中下一個數的時候:

·如果下一個數與之前candidate儲存的數相同,則nTimes1

·如果下一個數與之前candidate儲存的數不同,則nTimes1

·每次當出現次數nTimes變為0後,用candidate儲存下一個數,並把nTimes重新設為1

·直到遍歷完陣列中的所有數為止。

舉個例子,假定陣列為{0, 1, 2, 1, 1},按照上述思路執行的步驟如下。

1)開始時,candidate儲存數0nTimes初始化為1

2)然後遍歷到數字1,與數0不同,則nTimes1變為0

3)因為nTimes變為了0,故candidate儲存下一個遍歷到的數2,且nTimes被重新設為1

4)繼續遍歷到第4個數1,與之前candidate儲存的數2不同,故nTimes1變為0

5nTimes再次被變為了0,故讓candidate儲存下一個遍歷到的數1,且nTimes被重新設為1

6)最後返回的就是最後一次把nTimes設為1的數1

思路清楚了,完整的參考程式碼如下:

// a代表陣列,length代表陣列長度
int FindOneNumber(int* a, int length)
{
         int candidate = a[0];
         int nTimes = 1;
         for (int i = 1; i < length; i++)
         {
                 if (nTimes == 0)
                 {
                         candidate = a[i];
                         nTimes = 1;
                 }
                 else
                 {
                         if (candidate == a[i])
                        {
                                 nTimes++;
                         }
                         else
                         {
                                 nTimes--;
                         }
                 }
         }
         return candidate;
}

針對陣列{0, 1, 2, 1, 1}執行上述程式後,candidatenTimes等相關變數的變化如表4-1所示。

表4-1


舉一反三

出現次數剛好是一半的數

n個數,其中有一個數剛好出現一半次數,要求線上性時間內求出這個數。

點評:如果是剛好出現一半,如此例的{0, 1, 2, 1},開始時,candidate儲存數0nTimes初始化為1;遍歷到1時,與candidate不同,nTimes減為0;遍歷到2時,nTimes0,故candidate更新為2nTimes重新設為1;遍歷到1後,與之前candidate儲存的數2不同,則nTimes減為0;最終返回candidate所儲存數(2)的下一個數1

問題擴充套件

給定一個有限集合US1, S2,, Sn都是U的非空子集,且它們滿足任意多個集合的並集仍然在這些集合裡。請證明:一定存在某一個元素,存在於至少一半的集合裡。

點評:1999年,有人證明了存在一個元素在至少n/log2n個集合裡出現。但離本題的證明目標還差很遠。