出現次數超過一半的數(面試題)
出現次數超過一半的數
題目描述
陣列中有一個數出現的次數超過了陣列長度的一半,找出這個數。
分析與解法
因為不確定給定的陣列是無序還是有序的,所以要分情況討論。
解法一:排序
如果給定的陣列是無序的,那麼可以先對陣列進行排序(至於排序方法可選取最常用的快速排序)。排完序後遍歷陣列,在遍歷整個陣列的同時統計每個數的出現次數,然後把那個出現次數超過一半的數直接輸出,題目便算解答完了。總的時間複雜度為O(nlogn+n)。
但是,如果給定的陣列是有序的,或者經過排序後把無序的陣列變成有序的之後,是否還需要再遍歷一次陣列,以統計每個數出現的次數呢?
實際上,如果某個數在陣列中的出現次數超過一半,那麼在已經排好序的陣列索引的
然而,時間複雜度從O(nlogn+n)降到O(nlogn)並無本質上的改變,我們需要找到一種更有效的思路或方法。
解法二:散列表
通常來說,要想降低時間複雜度,有這麼幾個思路可以選擇。
·減少不必要的操作,比如解法一中陣列排完序後可以直接輸出第n/2處的那個數,不必再統計每個數的出現次數。
·以空間換時間,比如藉助散列表達到快速對映的目的。
應根據問題本身的特性使用對應的技巧。比如在
針對以空間換時間,我們自然而然想到了查詢時間複雜度為O(1)的散列表。首先用散列表完成陣列中每個數出現次數的統計,其中,散列表的鍵為陣列中的數,值為該數出現的次數。這樣,利用散列表完成統計後,如果需要找出那個出現次數超過一半的數,直接遍歷整個散列表,然後輸出該數即可。
構照敘列表後,查一次的時間複雜度為O(1),遍歷一遍查詢n次,則總的時間複雜度為O(1)。但是,散列表的方法需要O(n)的空間開銷,且要設計雜湊函式,還有沒有更好的辦法呢?
解法三:每次刪除兩個不同的數
根據這個問題本身的特殊性,可以試著這麼考慮,通過
舉個簡單的例子,如陣列a[5]={0, 1, 2, 1, 1}。很顯然,若要找出陣列a中出現次數超過一半的數,這個數便是1。通過一次性遍歷整個陣列,然後每次刪除不相同的兩個數,過程簡單表示如下。
(1)給定序列0, 1, 2, 1, 1。
(2)刪除不相同的兩個數0和1,序列變為2, 1, 1。
(3)最後再刪去兩個不同的數2和1,序列變為1。
(4)最終1即為所要找的結果。
解法四:記錄兩個值
更進一步,我們可以在遍歷陣列的時候儲存兩個值:一個是candidate,用來儲存陣列中遍歷到的某個數;另一個是nTimes,表示當前數的出現次數,其中nTimes初始化為1。當遍歷到陣列中下一個數的時候:
·如果下一個數與之前candidate儲存的數相同,則nTimes加1;
·如果下一個數與之前candidate儲存的數不同,則nTimes減1;
·每次當出現次數nTimes變為0後,用candidate儲存下一個數,並把nTimes重新設為1。
·直到遍歷完陣列中的所有數為止。
舉個例子,假定陣列為{0, 1, 2, 1, 1},按照上述思路執行的步驟如下。
(1)開始時,candidate儲存數0,nTimes初始化為1。
(2)然後遍歷到數字1,與數0不同,則nTimes減1變為0。
(3)因為nTimes變為了0,故candidate儲存下一個遍歷到的數2,且nTimes被重新設為1。
(4)繼續遍歷到第4個數1,與之前candidate儲存的數2不同,故nTimes減1變為0。
(5)因nTimes再次被變為了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}執行上述程式後,candidate和nTimes等相關變數的變化如表4-1所示。
表4-1
舉一反三
出現次數剛好是一半的數
有n個數,其中有一個數剛好出現一半次數,要求線上性時間內求出這個數。
點評:如果是剛好出現一半,如此例的{0, 1, 2, 1},開始時,candidate儲存數0,nTimes初始化為1;遍歷到1時,與candidate不同,nTimes減為0;遍歷到2時,因nTimes為0,故candidate更新為2,nTimes重新設為1;遍歷到1後,與之前candidate儲存的數2不同,則nTimes減為0;最終返回candidate所儲存數(2)的下一個數1。
問題擴充套件
給定一個有限集合U,S1, S2,…, Sn都是U的非空子集,且它們滿足任意多個集合的並集仍然在這些集合裡。請證明:一定存在某一個元素,存在於至少一半的集合裡。
點評:1999年,有人證明了存在一個元素在至少n/log2n個集合裡出現。但離本題的證明目標還差很遠。