面試題:無序整數數組中找第 K 大的數

2021-03-03 算法愛好者

(給算法愛好者加星標,修煉編程內功)

作者:蒼痕

blog.csdn.net/wangbaochu/article/details/52949443

前言】 找一個無序數組裡面的第 K 大數,你有什麼好方法嗎? 本文介紹了五種可行的方案,希望能對大家有所啟發,如果你有什麼更好的方法,也歡迎你留言,大家一起討論。

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

【解法一】先排序,然後輸出第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)較小的情況下,可以選擇部分排序。

【解法二】改進的快速排序方法:避免對所有的數排序,利用快速排序分堆,然後遞歸另外一半(不需要兩半都遞歸),直到最終所有小於基準數的個數為K

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


在本問題中,假設 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)。

【解法三】二分搜索法:直接劃定中值區間,通過比較的方法,找到第K個數所在的數值區間,然後二分搜索遞歸下去尋找 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){    Vmid = Vmin + (Vmax – Vmin) * 0.5;    if(f(arr, N, Vmid) >= K)      Vmin = Vmid;    else     Vmax = Vmid;}

偽代碼中 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』)次。

【解法五】鍵值索引法:適用於數據的取值範圍不太大的情景,可以將每個數作為輔助數組的索引,計算每個數出現的次數。統計所有的次數,找到第K個數。

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

如果所有 N 個數都是正整數,且它們的取值範圍不太大,可以考慮申請空間,記錄每個整數出現的次數,然後再從大到小取最大的 K 個。

比如,所有整數都在(0, MAXN)區間中的話,利用一個數組 count[MAXN]來記錄每個整數出現的個數(count[i]表示整數 i 在所有整數中出現的個數)。

我們只需要掃描一遍就可以得到 count 數組。然後,尋找第 K 大的元素:

for (sumCount = 0, v = MAXN-1; v >= 0; v–-){   sumCount += count[v];   if(sumCount >= K)     break;}return v;

- EOF -

覺得本文有幫助?請分享給更多人

推薦關注「算法愛好者」,修煉編程內功

點讚和在看就是最大的支持❤️

相關焦點

  • 數組(並不只是數組)
    看到 O(log L) 這樣的複雜度,而且還是有序數組,能想到哪個算法嗎?沒錯,就是二分查找!要找中位數,就是要找第 k 大的數(k = (L / 2 + 1),其中L 是上面提到的合併後新數組的長度,當 L 是偶數時,要求第 (L / 2) 大和第 (L / 2 + 1) 大的兩個數)。
  • 詳解一道高頻算法題:數組中的第 K 個最大元素
    今天分享的題目來源於 LeetCode 第 215 號問題,是面試中的高頻考題。在 未排序 的數組中找到第 k 個最大的元素。請注意,你需要找的是數組排序後的第 k 個最大的元素,而不是第 k 個不同的元素。
  • LeetCode刷題第三周【數組(簡單)】
    第 k 個缺失的正整數(難度:簡單)Oct.28 刷題請點擊或見附錄:1539. 第 k 個缺失的正整數[3]題目要求:給你一個 嚴格升序排列 的正整數數組 arr 和一個整數 k 。示例 2:輸入:arr = [1,2,3,4], k = 2輸出:6解釋:缺失的正整數包括 [5,6,7,...] 。第 2 個缺失的正整數為 6 。1.1 解題思路我們通過示例發現,數組的下標 i 與 arr[i] 的差值 distance 就是缺失的正整數個數。
  • LeetCode面試系列 第2天:No.136 - 只出現一次的數
    LeetCode面試題題系列的上一篇文章 Leetcode面試系列 第1天:Leetcode 89 - 格雷碼 中,我們介紹了
  • 十道Google面試題,你能搞定幾道?
    Google 面試題1 數組補丁給出一個從小到大排好序的整數數組nums和一個整數n,在數組中添加若干個補丁(元素)使得[1,n]的區間內的所有數都可以表示成nums中若干個數的和。返回最少需要添加的補丁個數。
  • 你面試中,遇到的php數組的面試題,快收藏
    網上找的PHP數組題,準備自己做一遍並且記錄下來。不斷添加中。<?,數組中的數為遞增的等比數,比值為3,首項為1.,數組中的元素滿足斐波拉契數列的規律.特別指出:第0項是0,第1項是第一個1。)<?
  • LeetCode 題解 | 523. 連續的子數組和
    連續的子數組和(點擊文末閱讀原文查看題目)題目描述給定一個包含非負數的數組和一個目標整數 k,編寫一個函數來判斷該數組是否含有連續的子數組,其大小至少為 2,總和為 k 的倍數,即總和為 n*k,其中 n 也是一個整數。
  • 數據分析師面試最常見的十道面試題分享!
    以下是容大教育小編日常整理的數據分析師面試時經常遇到的十道數據分析面試題,下面讓我們一起看看數據分析師面試最常見的十道面試題:1、海量日誌數據,提取出某日訪問百度次數最多的那個IP首先是這一天,並且是訪問百度的日誌中的IP取出來,逐個寫入到一個大文件中。
  • ​LeetCode刷題實戰15題: 三數之和
    算法的重要性,我就不多說了吧,想去大廠,就必須要經過基礎知識和業務邏輯面試+算法面試
  • 【每日一題】643. 子數組最大平均數 I
    各位題友大家好,今天是每日算法題公眾號堅持日更的第 11 天。今天力扣上的每日一題是第 643 題「子數組最大平均數 I」。可以通過每日一題的小程序查看題目詳情:題目大意給定 n 個整數,找出平均數最大且長度為 k 的連續子數組,並輸出該最大平均數。
  • java數組之完美洗牌算法
    >定義3對模指數,a對模m的原根定義為,st:中最小的正整數d再比如,2是9的原根,因為,為了讓除以9的餘數恆等於1,可知最小的正整數d=6,而(m)=6,滿足原根的定義。>定理1同餘定理:兩個整數a,b,若它們除以正整數m所得的餘數相等,則稱a,b對於模m同餘,記作,讀做a與b關於模m同餘。
  • 面試經驗分享之數據結構、算法題
    若干年前, Google、微軟的面試題讓大家眼前一亮,覺得能選拔出個性十足的聰明人來,不過隨著大家對這類題目的適應,可能選拔出來的人也在趨同,至少很多人都會 在面試前用心準備,據報導 Google 最近也是放棄了這類面試題目。沒有什麼一勞永逸、一成不變的考查方式,畢竟面試是人和人之間的動態「較量」。不要貪戀算法的奇技淫巧,也不要因為題目篩選力度的衰減而否定考察初衷。
  • 高階測試 · 大廠面試 · 真題題解(7)
    有測友去某知名大廠面試了,遭遇了幾道編程題,帶回來分享給大家研究學習!11.給定一個整數數組 nums 和一個目標值 target請你在該數組中找出和目標值的那兩個整數,並返回他們的數組下標。你可以假設每種輸入只會對應一個答案。但是,數組中同一個元素不能使用兩遍。
  • 如何求二維數組的前綴和?
    這個概念其實很容易理解,即一個數組中,第 n 位存儲的是數組前 n 個數字的和。通過一個例子來進行說明會更清晰。題目描述:有一個長度為 N 的整數數組 A,要求返回一個新的數組 B,其中 B 的第 i 個數 B[i]是「原數組 A 前 i 項和」。
  • 每天一道算法題-大冪冪
    a是正整數,b是一個超大的數,b用一個數組表示。因為冪超大,所以我把這道題稱為大冪冪,不是那個大冪冪,大家不要失望啊,哈哈哈~慄子Input: a = 2, b = [3]Output: 8Input: a = 2, b = [1,0]Output: 1024思路這道題要用到餘數的乘法定理,即:ab % k = (a%k) * (b%k) % k所以
  • 秒殺2Sum 3Sum 4Sum 算法題
    有了這個思路,假設當前在看 x,那就是需要把 x 之前或者之後的數放在 HashSet 裡,然後看下 target - x 在不在這個 hashSet 裡,如果在的話,那就匹配成功~誒這裡有個問題,這題要求返回這倆數的下標,可是 HashSet 裡的數是無序的...
  • LeetCode刷題指南(數組和矩陣)
    ,這裡也準備和大家分享他的LeetCode題解,於是我結合自己在進行刷題時做的分析和理解,按照題目類型進行劃分,形成本系列的LeetCode刷題指南。本文主要介紹的是LeetCode題庫中與數組和矩陣相關的經典題目,提供了LeetCode原題題號,參考答案,以及題目的部分解析。數組和矩陣把數組中的 0 移到末尾283.
  • 漫畫:美團面試題(TOPK:求第K個最大的元素)
    這個題目的變形很多,比如找 "前 K 個高頻元素"、 "數據流中的第K大元素" 、"最接近原點的 K 個值" 等等等等。第215題:在未排序的數組中找到第 k 個最大的元素。請注意,你需要找的是數組排序後的第 k 個最大的元素,而不是第 k 個不同的元素。
  • 面試題解| Median of two Sorted Arrays
    解題思路分析這道題的直觀解法:就是掃一遍兩個數組,找到中間位置的那一個/兩個數,然後得到中位數。時間複雜度是 O(m+n),其中 m 和 n 分別是數組 A 和數組 B 的長度。深入分析但是!面試官會這麼輕易就放過你嗎?顯然是不可能滴~~小編偷看了一下題目描述下的「challenge」標籤,原來這道題的最優解是 O(log(m + n)) 的複雜度。(m + n) 是倆數組合併後的總長度  L,看到 O(log L) 這樣的複雜度,而且還是有序數組,能想到哪個算法嗎?沒錯,就是二分查找!
  • 「面試必問」leetcode高頻題精選
    認真仔細的閱讀完本文,相信對於你在算法方面的面試一定會有不小的幫助!🤠兩數之和 🦊❝題目難度easy,涉及到的算法知識有數組、哈希表❞題目描述給定一個整數數組 nums  和一個目標值 target,請你在該數組中找出和為目標值的那兩個整數,並返回他們的數組下標。