找出無序數組中最小的k個數(top k問題)

2019獨角獸企業重金招聘Python工程師標準>>> hot3.png

給定一個無序的整型數組arr,找到其中最小的k個數

該題是互聯網面試中十分高頻的一道題,如果用普通的排序算法,排序之后自然可以得到最小的k個數,但時間復雜度高達O(NlogN),且普通的排序算法均屬于內部排序,需要一次性將全部數據裝入內存,對于求解海量數據的top k問題是無能為力的。

針對海量數據的top k問題,這里實現了一種時間復雜度為O(Nlogk)的有效算法:初始時一次性從文件中讀取k個數據,并建立一個有k個數的最大堆,代表目前選出的最小的k個數。然后從文件中一個一個的讀取剩余數據,如果讀取的數據比堆頂元素小,則把堆頂元素替換成當前的數,然后從堆頂向下重新進行堆調整;否則不進行任何操作,繼續讀取下一個數據。直到文件中的所有數據讀取完畢,堆中的k個數就是海量數據中最小的k個數(如果是找最大的k個數,則使用最小堆)。具體過程請參看如下代碼:

public class FindKMinNums {/*** 維護一個有k個數的最大堆,代表目前選出的最小的k個數** @param read 實際場景中,read提供的數據需要從文件中讀取,這里為了方便用數組表示* @param k* @return*/public static int[] getKMinsByHeap(int[] read, int k) {if (k < 1 || k > read.length) {return read;}int[] kHeap = new int[k];for (int i = 0; i < k; i++) {   // 初始時一次性從文件中讀取k個數據kHeap[i] = read[i];}buildHeap(kHeap, k);            // 建堆,時間復雜度O(k)for (int i = k; i < read.length; i++) { // 從文件中一個一個的讀取剩余數據if (read[i] < kHeap[0]) {kHeap[0] = read[i];heapify(kHeap, 0, k);   // 從堆頂開始向下進行調整,時間復雜度O(logk)}}return kHeap;}/*** 建堆函數** @param arr* @param n*/public static void buildHeap(int[] arr, int n) {for (int i = n / 2 - 1; i >= 0; i--) {heapify(arr, i, n);}}/*** 從arr[i]向下進行堆調整** @param arr* @param i* @param heapSize*/public static void heapify(int[] arr, int i, int heapSize) {int leftChild = 2 * i + 1;int rightChild = 2 * i + 2;int max = i;if (leftChild < heapSize && arr[leftChild] > arr[max]) {max = leftChild;}if (rightChild < heapSize && arr[rightChild] > arr[max]) {max = rightChild;}if (max != i) {swap(arr, i, max);heapify(arr, max, heapSize);  // 堆結構發生了變化,繼續向下進行堆調整}}public static void swap(int[] arr, int i, int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}public static void printArray(int[] arr) {for (int i = 0; i <= arr.length; i++) {System.out.print(arr[i] + " ");}System.out.println();}public static void main(String[] args) {int[] arr = {6, 9, 1, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9};// sorted : { 1, 1, 1, 1, 2, 2, 2, 3, 3, 5, 5, 5, 6, 6, 6, 7, 9, 9, 9 }printArray(getKMinsByHeap(arr, 10));}
}

對于從海量數據(N)中找出TOP K,這種算法僅需一次性將k個數裝入內存,其余數據從文件一個一個讀即可,所以它是針對海量數據TOP K問題最為有效的算法


對于非海量數據的情況,還有一種時間復雜度僅為O(N)的經典算法 —— BFPRT算法,該算法于1973年由Blum、Floyd、Pratt、Rivest和Tarjan聯合發明,其中蘊含的深刻思想改變了世界。

BFPRT算法解決了這樣一個問題:在時間復雜度O(N)內,從無序的數組中找到第k小的數。顯而易見的是,如果我們找到了第k小的數,那么想求arr中最小的k個數,只需再遍歷一遍數組,把小于第k小的數都搜集起來,再把不足部分用第k小的數補全即可。

BFPRT算法是如何找到第k小的數?以下是BFPRT算法的過程,假設BFPRT算法的函數是int select(int[] arr, int k),該函數的功能為在arr中找到第k小的數,然后返回該數。select(arr, k)的過程如下:

  1. 將arr中的n個元素劃分成 n/5 組,每組5個元素,如果最后的組不夠5個元素,那么最后剩下的元素為一組(n%5 個元素)。時間復雜度O(1)

  2. 對每個組進行排序,比如選擇簡單的插入排序,只針對每個組最多5個元素之間的組內排序,組與組之間不排序。時間復雜度 N/5O(1)

  3. 找到每個組的中位數,如果元素個數為偶數可以找下中位數,讓這些中位數組成一個新的數組,記為mArr。時間復雜度O(N/5)

  4. 遞歸調用select(mArr, mArr.length / 2),意義是找到mArr這個數組的中位數x,即中位數的中位數。時間復雜度T(N/2)

  5. 根據上面得到的x劃分整個arr數組(partition過程),劃分的過程為:在arr中,比x小的都在x左邊,比x大的都在x右邊,x在中間。時間復雜度O(N)

  6. 假設劃分完成后,x在arr中的位置記為i,關于i與k的相對大小,有如下三種情況:

    1. 如果 i = k,說明x為整個數組中第k小的數,直接返回。時間復雜度O(1)
    2. 如果 i < k,說明x處在第k小的數左邊,應該在x的右邊繼續尋找,所以遞歸調用select函數,在右半區尋找第k-i小的數。時間復雜度不超過T(7/10N + 6)
    3. 如果 i > k,說明x處在第k小的數右邊,應該在x的左邊繼續尋找,所以遞歸調用select函數,在左半區尋找第k小的數。時間復雜度同樣不超過T(7/10N + 6)

上述過程的代碼實現如下:

public class FindKMinNums {/*** 先用BFPRT算法求出第k小的數,再遍歷一遍數組才能求出最小的k個數,時間復雜度O(N)* 需要將所有數據一次性裝入內存,適用于非海量數據的情況** @param arr* @param k* @return*/public static int[] getKMins(int[] arr, int k) {if (k < 1 || k > arr.length) {return arr;}int kthMin = getKthMinByBFPRT(arr, k);  // 使用BFPRT算法求得第k小的數,O(N)int[] kMins = new int[k];               // 下面遍歷一遍數組,利用第k小的數找到最小的k個數,O(N)int index = 0;for (int i = 0; i < arr.length; i++) {if (arr[i] < kthMin) {              // 小于第k小的數,必然屬于最小的k個數kMins[index++] = arr[i];}}while (index < k) {kMins[index++] = kthMin;            // 不足部分用第k小的數補全}return kMins;}/*** 使用BFPRT算法求第k小的數** @param arr* @param k* @return*/public static int getKthMinByBFPRT(int[] arr, int k) {int[] arrCopy = copyArray(arr); // 在得到第k小的數之后還要遍歷一遍原數組,所以并不直接操作原數組return select(arrCopy, 0, arrCopy.length - 1, k - 1);   // 第k小的數,即排好序后下標為k-1的數}/*** 拷貝數組** @param arr* @return*/public static int[] copyArray(int[] arr) {int[] arrCopy = new int[arr.length];for (int i = 0; i < arrCopy.length; i++) {arrCopy[i] = arr[i];}return arrCopy;}/*** 在數組arr的下標范圍[begin, end]內,找到排序后位于整個arr數組下標為index的數** @param arr* @param begin* @param end* @param index* @return*/public static int select(int[] arr, int begin, int end, int index) {if (begin == end) {return arr[begin];}int pivot = medianOfMedians(arr, begin, end);   // 核心操作:中位數的中位數作為基準int[] pivotRange = partition(arr, begin, end, pivot);   // 拿到分區后中區的范圍if (index >= pivotRange[0] && index <= pivotRange[1]) { // 命中return arr[index];} else if (index < pivotRange[0]) {return select(arr, begin, pivotRange[0] - 1, index);} else {return select(arr, pivotRange[1] + 1, end, index);}}/*** 選基準** @param arr* @param begin* @param end* @return*/public static int medianOfMedians(int[] arr, int begin, int end) {int num = end - begin + 1;int offset = num % 5 == 0 ? 0 : 1;      // 5個成一組,不滿5個的自己成一組int[] mArr = new int[num / 5 + offset]; // 每組的中位數取出構成中位數數組mArrfor (int i = 0; i < mArr.length; i++) {int beginI = begin + i * 5;int endI = beginI + 4;mArr[i] = getMedian(arr, beginI, Math.min(endI, end));}// 求中位數數組mArr的中位數,作為基準返回return select(mArr, 0, mArr.length - 1, mArr.length / 2);}/*** 在數組arr的下標范圍[begin, end]內,找中位數,如果元素個數為偶數則找下中位數** @param arr* @param begin* @param end* @return*/public static int getMedian(int[] arr, int begin, int end) {insertionSort(arr, begin, end);int sum = begin + end;int mid = (sum / 2) + (sum % 2);return arr[mid];}/*** 這里僅用于對一組5個數進行插入排序,時間復雜度O(1)** @param arr* @param begin* @param end*/public static void insertionSort(int[] arr, int begin, int end) {for (int i = begin + 1; i <= end; i++) {int get = arr[i];int j = i - 1;while (j >= begin && arr[j] > get) {arr[j + 1] = arr[j];j--;}arr[j + 1] = get;}}/*** 優化后的快排partition操作** @param arr* @param begin* @param end* @param pivot* @return 返回劃分后等于基準的元素下標范圍*/public static int[] partition(int[] arr, int begin, int end, int pivot) {int small = begin - 1;     // 小區最后一個元素下標int big = end + 1;         // 大區第一個元素下標int cur = begin;while (cur < big) {if (arr[cur] < pivot) {swap(arr, ++small, cur++);} else if (arr[cur] > pivot) {swap(arr, --big, cur);} else {cur++;}}int[] range = new int[2];range[0] = small + 1;      // 中區第一個元素下標range[1] = big - 1;        // 中區最后一個元素下標return range;}public static void swap(int[] arr, int i, int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}public static void printArray(int[] arr) {for (int i = 0; i < arr.length; i++) {System.out.print(arr[i] + " ");}System.out.println();}public static void main(String[] args) {int[] arr = {6, 9, 1, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9};// sorted : { 1, 1, 1, 1, 2, 2, 2, 3, 3, 5, 5, 5, 6, 6, 6, 7, 9, 9, 9 }printArray(getKMins(arr, 10));}
}

關于BFPRT算法為什么在時間復雜度上可以做到穩定的O(N),可以參考《程序員代碼面試指南》P339或《算法導論》9.3節內容,這里不做證明。

轉載于:https://my.oschina.net/u/2935389/blog/3040390

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/275284.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/275284.shtml
英文地址,請注明出處:http://en.pswp.cn/news/275284.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

你應該知道的 Node 基礎知識

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;已進行兩個多月&#xff0c;大家一起交流學習&#xff0c;共同進步。源碼共讀學的多數是 Node.js &#xff0c;今天分享一篇 Node.js 基礎知識的文章。一. N…

C# 中數據緩存總結

在C#嘗試了5種方法進行數據緩存&#xff0c;具體如下&#xff1a;(如有遺漏&#xff0c;錯誤歡迎大家指正&#xff0c;歡迎提建議。)1&#xff1a;Session方法&#xff1a;此方法是針對于每個用戶來的&#xff0c;如果用戶量比較大&#xff0c;那么建議不要采用此方法&#xff…

react 引入 mobx @babel/core: 7.2.2

為什么80%的碼農都做不了架構師&#xff1f;>>> yarn add babel/plugin-proposal-class-propertiesyarn add babel/plugin-proposal-decorators"babel": {"plugins": [["babel/plugin-proposal-decorators", {"legacy": …

面試官問:怎么自動檢測你使用的組件庫有更新

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12本文來自V同學投稿的源碼共讀第六期筆記&#xff0c;寫得很有趣。現在已經進行到第十期了。你或許經常看見 npm 更新的提示。npm 更新提示面試官可能也會問你&#xff0c;組件庫…

設計模式完整備忘錄

小言&#xff1a;這不是設計模式講解型博文&#xff0c;以下將設計模式的概述、類圖&#xff0c;代碼示例&#xff0c;總結分每篇博文單獨展示&#xff0c;現將其歸類&#xff0c;便于以后翻閱&#xff0c;設計模式也不是一兩個月學完了就能完全領悟&#xff0c;它只告訴我們幾…

使用Microsoft Web Application Stress Tool對web進行壓力測試

你的Web服務器和應用到底能夠支持多少并發用戶訪問&#xff1f;在出現大量并發請求的情況下&#xff0c;軟件會出現問題嗎&#xff1f;這些問題靠通常的測試手段是無法解答的。本文介紹 了Microsoft為這個目的而提供的免費工具WAS及其用法。另外&#xff0c;本文介紹了一種Web應…

2021前端高頻面試題整理,附答案

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12若川視野原意是若川的前端視野。但太長了就留下了四個字&#xff0c;不知道的以為關注的不是技術公眾號。今天分享一篇慕課網精英講師河畔一角的好文章~廢話不多說&#xff0c;…

OO第二單元作業小結

總結性博客作業 第一次作業 (1)從多線程的協同和同步控制方面&#xff0c;分析和總結自己三次作業的設計策略。 第一次作業為單電梯傻瓜調度&#xff0c;可以采用生產者——消費者模型&#xff0c;是一個有一個生產者&#xff08;標準輸入電梯請求&#xff09;&#xff0c;一個…

dribbble加速vpn_關于Dribbble設計的幾點思考

dribbble加速vpn重點 (Top highlight)I’d like to start with the following quote from Paul Adam’s “The Dribbbilisation of Design,” a powerful read that examines the superficiality of modern product design portfolios, often containing Dribbble posts that l…

JS Compress and Decompress

<html><head><title>JavaScript字符串之壓縮與還原</title><meta http-equiv"Content-Type"content"text/html; charsetutf-8"/><script type"text/javascript"><!--/** * 壓縮 */functionCompress(strN…

尤雨溪推薦神器 ni ,能替代 npm/yarn/pnpm ?簡單好用!源碼揭秘!

1. 前言大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12想學源碼&#xff0c;極力推薦之前我寫的《學習源碼整體架構系列》jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、…

如何了解自己的認知偏差_了解吸引力偏差

如何了解自己的認知偏差Let me introduce you the attractiveness bias theory known as cognitive bias.讓我向您介紹稱為認知偏差的吸引力偏差理論。 Think about a person with outstanding fashion. It will draw our attention, and maybe encourage us to interact with…

隱馬爾可夫模型(HMM)及Viterbi算法

HMM簡介 對于算法愛好者來說&#xff0c;隱馬爾可夫模型的大名那是如雷貫耳。那么&#xff0c;這個模型到底長什么樣&#xff1f;具體的原理又是什么呢&#xff1f;有什么具體的應用場景呢&#xff1f;本文將會解答這些疑惑。  本文將通過具體形象的例子來引入該模型&#xf…

尤大直播分享:vue3生態進展和展望

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12前言10月23日&#xff0c;參加了前端早早聊組織的【vue生態專場】&#xff0c;準備寫一波分享方便大家學習。早上有4個話題&#xff1a;volar開發&#xff0c;搭建平臺組件開發…

利用Python查看微信共同好友

思路 首先通過itchat這個微信個人號接口掃碼登錄個人微信網頁版&#xff0c;獲取可以識別好友身份的數據。這里是需要分別登錄兩人微信的&#xff0c;拿到兩人各自的好友信息存到列表中。 這樣一來&#xff0c;查共同好友就轉化成了查兩個列表中相同元素的問題。獲取到共同好友…

女生適合學ux嗎_UX設計色彩心理學,理論與可訪問性

女生適合學ux嗎Colour is an interesting topic, which I feel is often overlooked and sometimes under-appreciated. One of the first things I was taught was the power of colour, how it can have an impact on human emotion, and that there should be purpose behin…

初學者也能看懂的 Vue2 源碼中那些實用的基礎工具函數

1. 前言大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12想學源碼&#xff0c;極力推薦之前我寫的《學習源碼整體架構系列》jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、…

清除浮動mini版

1&#xff09; 清除浮動mini版(簡約而不簡單).clr:after { content:"";display:table;clear:both;}.clr{zoom:1;} 轉載于:https://www.cnblogs.com/jinbiao/archive/2011/09/26/2191170.html

Fiddler 十分鐘最全使用介紹

Wireshark 、HTTPWatch、Fiddler的介紹 Firebug雖然可以抓包&#xff0c;但是對于分析http請求的詳細信息&#xff0c;不夠強大。模擬http請求的功能也不夠&#xff0c;且firebug常常是需要“無刷新修改”&#xff0c;如果刷新了頁面&#xff0c;所有的修改都不會保存。Wiresha…

視覺測試_視覺設計流行測驗

視覺測試重點 (Top highlight)I often discuss the topic of improving visual design skills with junior and mid-level designers. While there are a number of design principles the designers should learn and practice, one important skill that is not often consid…