無序整數陣列中找第k大的數

2021-08-19 07:22:30 字數 3546 閱讀 3580

經典問題:寫一段程式,找出陣列中第k大的數,輸出數所在的位置。

我們先假設元素的數量不大,例如在幾千個左右,在這種情況下,那我們就排序一下吧。在這裡,快速排序或堆排序都是不錯的選擇,他們的平均時間複雜度 都是 o(n * logn)。然後取出前 k 個,o(k)。總時間複雜度 o(n * logn)+ o(k) = o(n * logn)。

你一定注意到了,當 k=1 時,上面的演算法也是 o(n * logn)的複雜度,而顯然我們可以通過 n-1 次的比較和交換得到結果。上面的演算法把整個陣列都進行了排序,而原題目只要求最大的 k 個數,並不需要前 k 個數有序,也不需要後 n-k 個數有序。

怎麼能夠避免做後 n-k 個數的排序呢?我們需要部分排序的演算法,選擇排序和交換排序都是不錯的選擇。把 n 個數中的前 k 大個數排序出來,複雜度是o(n * k)。那乙個更好呢?o(n * logn)還是 o(n * k)?這取決於 k 的大小,這是你需要在面試者那裡弄清楚的問題。在 k(k < = logn)較小的情況下,可以選擇部分排序。

回憶一下快速排序,快排中的每一步,都是將待排資料分做兩組,其中一組的資料的任何乙個數都比另一組中的任何乙個大,然後再對兩組分別做類似的操作,然後繼續下去……

在本問題中,假設 n 個數儲存在陣列 s 中,我們從陣列 s 中隨機找出乙個元素 x,把陣列分為兩部分 sa 和 sb。sa 中的元素大於等於 x,sb 中元素小於 x。

這時,有兩種可能性:

1. sa中元素的個數小於k,sa中所有的數和sb中最大的k-|sa|個元素(|sa|指sa中元素的個數)就是陣列s中最大的k個數。

2. sa中元素的個數大於或等於k,則需要返回sa中最大的k個元素。

這樣遞迴下去,不斷把問題分解成更小的問題,平均時間複雜度 o(n *logk)

尋找 n 個數中最大的 k 個數,本質上就是尋找最大的 k 個數中最小的那個,也就是第 k 大的數。可以使用二分搜尋的策略來尋找 n 個數中的第 k 大的數。

(1)按數值區間二分搜尋

對於乙個給定的數 p,可以在 o(n)的時間複雜度內找出所有不小於 p 的數。假如 n 個數中最大的數為 vmax,最小的數為 vmin,那麼這 n 個數中的第 k 大數一定在區間[vmin, vmax]之間。那麼,可以在這個區間內二分搜尋 n 個數中的第 k大數 p。偽**如下:

while(vmax-vmin

> delta)

偽**中 f(arr, n, vmid)返回陣列 arr[0, …, n-1]中大於等於 vmid 的數的個數。

上述偽**中,delta 的取值要比所有 n 個數中的任意兩個不相等的元素差值之最小值小。如果所有元素都是整數,delta 可以取值 0.5。迴圈執行之後,最終得到乙個很小的區間(vmin, vmax),這個區間僅包含乙個元素(或者多個相等的元素),則這個元素就是第 k 大的元素。整個演算法的時間複雜度為 o(n * log2(|vmax – vmin|/delta))。

由於 delta 的取值要比所有 n 個數中的任意兩個不相等的元素差值之最小值小,因此時間複雜度跟資料分布相關。在資料分布平均的情況下,時間複雜度為 o(n * log2(n))。

(2)從另乙個角度: 按位元位二分搜尋

假設所有整數的大小都在[0, 2^m-1]之間,也就是說所有整數在二進位制中都可以用 m bit 來表示(從低位到高位,分別用 0, 1, …, m-1 標記)。我們可以先考察在二進位制位的第(m-1)位,將 n 個整數按該位為 1 或者 0 分成兩個部分。也就是將整數分成取值為[0, 2m-1-1]和[2m-1, 2m-1]兩個區間。前乙個區間中的整數第(m-1)位為 0,後乙個區間中的整數第(m-1)位為 1。如果該位為 1 的整數個數 a 大於等於 k,那麼,在所有該位為 1 的整數中繼續尋找最大的 k 個。否則,在該位為 0 的整數中尋找最大的 k-a 個。接著考慮二進位制位第(m-2)位,以此類推。思路跟上面的區間中值數的情況本質上一樣。

對於上面兩個方法,我們都需要遍歷一遍整個集合,統計在該集合中大於等於某乙個數的整數有多少個。不需要做隨機訪問操作,如果全部資料不能載入內 存,可以每次都遍歷一遍檔案。經過統計,更新解所在的區間之後,再遍歷一次檔案,把在新的區間中的元素存入新的檔案。下一次操作的時候,不再需要遍歷全部 的元素。每次需要兩次檔案遍歷,最壞情況下,總共需要遍歷檔案的次數為2 * log2(|vmax – vmin|/delta)。由於每次更新解所在區間之後,元素數目會減少。

當所有元素能夠全部載入記憶體之後,就可以不再通過讀寫檔案的方式來操作了。

我們已經得到了三個解法,不過這三個解法有個共同的地方,就是需要對資料訪問多次,那麼就有下乙個問題,如果 n 很大呢,100 億?(更多的情況下,是面試者問你這個問題)。這個時候資料不能全部裝入記憶體(不過也很難說,說知道以後會不會 1t 記憶體比 1 斤白菜還便宜),所以要求盡可能少的遍歷所有資料。

不妨設 n > k,前 k 個數中的最大 k 個數是乙個退化的情況,所有 k 個數就是最大的 k 個數。如果考慮第 k+1 個數 x 呢?如果 x 比最大的 k 個數中的最小的數 y 小,那麼最大的 k 個數還是保持不變。如果 x 比 y 大,那麼最大的 k個數應該去掉 y,而包含 x。如果用乙個陣列來儲存最大的 k 個數,每新加入乙個數 x,就掃瞄一遍陣列,得到陣列中最小的數 y。用 x 替代 y,或者保持原陣列不變。這樣的方法,所耗費的時間為 o(n * k)。

進一步,可以用容量為 k 的最小堆來儲存最大的 k 個數。最小堆的堆頂元素就是最大 k 個數中最小的乙個。每次新考慮乙個數 x,如果 x 比堆頂的元素y 小,則不需要改變原來的堆,因為這個元素比最大的 k 個數小。如果 x 比堆頂元素大,那麼用 x 替換堆頂的元素 y。在 x 替換堆頂元素 y 之後,x 可能破壞最小堆的結構(每個結點都比它的父親結點大),需要更新堆來維持堆的性質。更新過程花費的時間複雜度為 o(log2k)

因此,演算法只需要掃瞄所有的資料一次,時間複雜度為o(n * log2k)。這實際上是部分執行了堆排序的演算法。在空間方面,由於這個演算法只掃瞄所有的資料一次,因此我們只需要儲存乙個容量為 k 的堆。大多數情況下,堆可以全部載入記憶體。如果 k 仍然很大,我們可以嘗試先找最大的 k』個元素,然後找第 k』+1個到第 2 * k』個元素,如此類推(其中容量 k』的堆可以完全載入記憶體)。不過這樣,我們需要掃瞄所有資料 ceil1(k/k』)次。

能否有確定的線性演算法呢?是否可以通過改進計數排序、基數排序等來得到乙個更高效的演算法呢?答案是肯定的。但演算法的適用範圍會受到一定的限制。

如果所有 n 個數都是正整數,且它們的取值範圍不太大,可以考慮申請空間,記錄每個整數出現的次數,然後再從大到小取最大的 k 個。比如,所有整數都在(0, maxn)區間中的話,利用乙個陣列 count[maxn]來記錄每個整數出現的個數(count[i]表示整數 i 在所有整數中出現的個數)。我們只需要掃瞄一遍就可以得到 count 陣列。然後,尋找第 k 大的元素:

for(sumcount = 0, v = maxn-1; v >= 0; v–)  

return v;

無序整數陣列中找第k大的數

寫一段程式,找出陣列中第k大小的數,輸出數所在的位置。解法一 我們先假設元素的數量不大,例如在幾千個左右,在這種情況下,那我們就排序一下吧。在這裡,快速排序或堆排序都是不錯的選擇,他們的平均時間複雜度 都是 o n log2n 然後取出前 k 個,o k 總時間複雜度 o n log2n o k o...

無序陣列中找第K個的數

題目分析 也就是從小往大排,第k小那個數。方法1 排序 nlogn 方法2 利用堆 nlogk 首先將前k個元素構建最大堆,堆頂是前k個元素中第k小的元素。這步複雜度klogk 遍歷剩餘元素 這步複雜度 n k logk 如果新元素 堆頂 堆頂不可能是第k大元素 移除堆頂 將新元素插入堆 否則 新元...

找陣列中第k大的數

但會修改陣列中的資料,所以這裡有新的不修改資料的思路 1 用multiset實現自動排序,大小為k,每個數與set中最大的數比較,如果小於則替換。2 可以用最大堆來實現,方法類似上面的。關鍵是看 注釋在下面。時間複雜度為 include include includeusing namespace ...