快速排序及TOP K問題

目錄

1.介紹1

2.介紹2

3.介紹3 包括各種排序的空間及時間複雜度


1.介紹1

摘自 https://www.cnblogs.com/itxiaok/archive/2019/02/15/10385676.html

前兩天面試3面學長問我的這個問題(想說TEG的3個面試學長都是好和藹,希望能完成最後一面,各方面原因造成我無比想去鵝場的心已經按捺不住了),這個問題還是建立最小堆比較好一些。

    先拿10000個數建堆,然後一次添加剩餘元素,如果大於堆頂的數(10000中最小的),將這個數替換堆頂,並調整結構使之仍然是一個最小堆,這樣,遍歷完後,堆中的10000個數就是所需的最大的10000個。建堆時間複雜度是O(mlogm),算法的時間複雜度爲O(nmlogm)(n爲10億,m爲10000)。
​
    優化的方法:可以把所有10億個數據分組存放,比如分別放在1000個文件中。這樣處理就可以分別在每個文件的10^6個數據中找出最大的10000個數,合併到一起在再找出最終的結果。
​
    以上就是面試時簡單提到的內容,下面整理一下這方面的問題:

top K問題 在大規模數據處理中,經常會遇到的一類問題:在海量數據中找出出現頻率最好的前k個數,或者從海量數據中找出最大的前k個數,這類問題通常被稱爲top K問題。例如,在搜索引擎中,統計搜索最熱門的10個查詢詞;在歌曲庫中統計下載最高的前10首歌等。 針對top K類問題,通常比較好的方案是分治+Trie樹/hash+小頂堆(就是上面提到的最小堆),即先將數據集按照Hash方法分解成多個小數據集,然後使用Trie樹活着Hash統計每個小數據集中的query詞頻,之後用小頂堆求出每個數據集中出現頻率最高的前K個數,最後在所有top K中求出最終的top K。

eg:有1億個浮點數,如果找出期中最大的10000個? 最容易想到的方法是將數據全部排序,然後在排序後的集合中進行查找,最快的排序算法的時間複雜度一般爲O(nlogn),如快速排序。但是在32位的機器上,每個float類型佔4個字節,1億個浮點數就要佔用400MB的存儲空間,對於一些可用內存小於400M的計算機而言,很顯然是不能一次將全部數據讀入內存進行排序的。其實即使內存能夠滿足要求(我機器內存都是8GB),該方法也並不高效,因爲題目的目的是尋找出最大的10000個數即可,而排序卻是將所有的元素都排序了,做了很多的無用功

第二種方法爲局部淘汰法,該方法與排序方法類似,用一個容器保存前10000個數,然後將剩餘的所有數字——與容器內的最小數字相比,如果所有後續的元素都比容器內的10000個數還,那麼容器內這個10000個數就是最大10000個數。如果某一後續元素比容器內最小數字,則刪掉容器內最小元素,並將該元素插入容器,最後遍歷完這1億個數,得到的結果容器中保存的數即爲最終結果了。此時的時間複雜度爲O(n+m^2),其中m爲容器的大小,即10000。

第三種方法是分治法,將1億個數據分成100份,每份100萬個數據,找到每份數據中最大的10000個,最後在剩下的10010000個數據裏面找出最大的10000個。如果100萬數據選擇足夠理想,那麼可以過濾掉1億數據裏面99%的數據。100萬個數據裏面查找最大的10000個數據的方法如下:用快速排序的方法,將數據分爲2堆,如果大的那堆個數N大於10000個,繼續對大堆快速排序一次分成2堆,如果大的那堆個數N大於10000個,繼續對大堆快速排序一次分成2堆,如果大堆個數N小於10000個,就在小的那堆裏面快速排序一次,找第10000-n大的數字;遞歸以上過程,就可以找到第1w大的數。參考上面的找出第1w大數字,就可以類似的方法找到前10000大數字了。此種方法需要每次的內存空間爲10^64=4MB,一共需要101次這樣的比較。

第四種方法是Hash法。如果這1億個書裏面有很多重複的數,先通過Hash法,把這1億個數字去重複,這樣如果重複率很高的話,會減少很大的內存用量,從而縮小運算空間,然後通過分治法或最小堆法查找最大的10000個數。

第五種方法採用最小堆。首先讀入前10000個數來創建大小爲10000的最小堆,建堆的時間複雜度爲O(mlogm)(m爲數組的大小即爲10000),然後遍歷後續的數字,並於堆頂(最小)數字進行比較。如果比最小的數小,則繼續讀取後續數字;如果比堆頂數字大,則替換堆頂元素並重新調整堆爲最小堆。整個過程直至1億個數全部遍歷完爲止。然後按照中序遍歷的方式輸出當前堆中的所有10000個數字。該算法的時間複雜度爲O(nmlogm),空間複雜度是10000(常數)。

實際運行: 實際上,最優的解決方案應該是最符合實際設計需求的方案,在時間應用中,可能有足夠大的內存,那麼直接將數據扔到內存中一次性處理即可,也可能機器有多個核,這樣可以採用多線程處理整個數據集。

 

2.介紹2

摘自 https://blog.csdn.net/wufaliang003/article/details/82940218

TopK,是問得比較多的幾個問題之一,到底有幾種方法,這些方案裏蘊含的優化思路究竟是怎麼樣的,今天和大家聊一聊。

問題描述:

從arr[1, n]這n個數中,找出最大的k個數,這就是經典的TopK問題。

栗子:

從arr[1, 12]={5,3,7,1,8,2,9,4,7,2,6,6} 這n=12個數中,找出最大的k=5個。

 

一、排序

 

排序是最容易想到的方法,將n個數排序之後,取出最大的k個,即爲所得。

 

僞代碼:

sort(arr, 1, n);

return arr[1, k];

 

時間複雜度:O(n*lg(n))
 

分析:明明只需要TopK,卻將全局都排序了,這也是這個方法複雜度非常高的原因。那能不能不全局排序,而只局部排序呢?這就引出了第二個優化方法。

 

二、局部排序

不再全局排序,只對最大的k個排序。

 

冒泡是一個很常見的排序方法,每冒一個泡,找出最大值,冒k個泡,就得到TopK。

 

僞代碼:

for(i=1 to k){

         bubble_find_max(arr,i);

}

return arr[1, k];

 

時間複雜度:O(n*k)

 

分析:冒泡,將全局排序優化爲了局部排序,非TopK的元素是不需要排序的,節省了計算資源。不少朋友會想到,需求是TopK,是不是這最大的k個元素也不需要排序呢?這就引出了第三個優化方法。

 

三、堆

思路:只找到TopK,不排序TopK。

 

先用前k個元素生成一個小頂堆,這個小頂堆用於存儲,當前最大的k個元素。

 

 

接着,從第k+1個元素開始掃描,和堆頂(堆中最小的元素)比較,如果被掃描的元素大於堆頂,則替換堆頂的元素,並調整堆,以保證堆內的k個元素,總是當前最大的k個元素。

 

 

直到,掃描完所有n-k個元素,最終堆中的k個元素,就是猥瑣求的TopK。

 

僞代碼:

heap[k] = make_heap(arr[1, k]);

for(i=k+1 to n){

         adjust_heap(heep[k],arr[i]);

}

return heap[k];

 

時間複雜度:O(n*lg(k))

畫外音:n個元素掃一遍,假設運氣很差,每次都入堆調整,調整時間複雜度爲堆的高度,即lg(k),故整體時間複雜度是n*lg(k)。

 

分析:堆,將冒泡的TopK排序優化爲了TopK不排序,節省了計算資源。堆,是求TopK的經典算法,那還有沒有更快的方案呢?

 

四、隨機選擇

隨機選擇算在是《算法導論》中一個經典的算法,其時間複雜度爲O(n),是一個線性複雜度的方法。

 

這個方法並不是所有同學都知道,爲了將算法講透,先聊一些前序知識,一個所有程序員都應該爛熟於胸的經典算法:快速排序。

畫外音:

(1)如果有朋友說,“不知道快速排序,也不妨礙我寫業務代碼呀”…額...

(2)除非校招,我在面試過程中從不問快速排序,默認所有工程師都知道;

 

其僞代碼是:

void quick_sort(int[]arr, int low, inthigh){

         if(low== high) return;

         int i = partition(arr, low, high);

         quick_sort(arr, low, i-1);

         quick_sort(arr, i+1, high);

}

 

其核心算法思想是,分治法。

 

分治法(Divide&Conquer),把一個大的問題,轉化爲若干個子問題(Divide),每個子問題“都”解決,大的問題便隨之解決(Conquer)。這裏的關鍵詞是“都”。從僞代碼裏可以看到,快速排序遞歸時,先通過partition把數組分隔爲兩個部分,兩個部分“都”要再次遞歸。

 

分治法有一個特例,叫減治法。

 

減治法(Reduce&Conquer),把一個大的問題,轉化爲若干個子問題(Reduce),這些子問題中“只”解決一個,大的問題便隨之解決(Conquer)。這裏的關鍵詞是“只”。

 

二分查找binary_search,BS,是一個典型的運用減治法思想的算法,其僞代碼是:

int BS(int[]arr, int low, inthigh, int target){

         if(low> high) return -1;

         mid= (low+high)/2;

         if(arr[mid]== target) return mid;

         if(arr[mid]> target)

                   return BS(arr, low, mid-1, target);

         else

                   return BS(arr, mid+1, high, target);

}

 

從僞代碼可以看到,二分查找,一個大的問題,可以用一個mid元素,分成左半區,右半區兩個子問題。而左右兩個子問題,只需要解決其中一個,遞歸一次,就能夠解決二分查找全局的問題。

 

通過分治法與減治法的描述,可以發現,分治法的複雜度一般來說是大於減治法的:

快速排序:O(n*lg(n))

二分查找:O(lg(n))

 

話題收回來,快速排序的核心是:

i = partition(arr, low, high);

 

這個partition是幹嘛的呢?

顧名思義,partition會把整體分爲兩個部分。

更具體的,會用數組arr中的一個元素(默認是第一個元素t=arr[low])爲劃分依據,將數據arr[low, high]劃分成左右兩個子數組:


    左半部分,都比t大
    
    
    右半部分,都比t小
    
    
    中間位置i是劃分元素
    


以上述TopK的數組爲例,先用第一個元素t=arr[low]爲劃分依據,掃描一遍數組,把數組分成了兩個半區:


    左半區比t大
    
    
    右半區比t小
    
    
    中間是t
    
partition返回的是t最終的位置i。

 

很容易知道,partition的時間複雜度是O(n)。

畫外音:把整個數組掃一遍,比t大的放左邊,比t小的放右邊,最後t放在中間N[i]。

 

partition和TopK問題有什麼關係呢?

TopK是希望求出arr[1,n]中最大的k個數,那如果找到了第k大的數,做一次partition,不就一次性找到最大的k個數了麼?

畫外音:即partition後左半區的k個數。

 

問題變成了arr[1, n]中找到第k大的數。

 

再回過頭來看看第一次partition,劃分之後:

i = partition(arr, 1, n);


    如果i大於k,則說明arr[i]左邊的元素都大於k,於是只遞歸arr[1, i-1]裏第k大的元素即可;
    
    
    如果i小於k,則說明說明第k大的元素在arr[i]的右邊,於是只遞歸arr[i+1, n]裏第k-i大的元素即可;
    
畫外音:這一段非常重要,多讀幾遍。

 

這就是隨機選擇算法randomized_select,RS,其僞代碼如下:

int RS(arr, low, high, k){

  if(low== high) return arr[low];

  i= partition(arr, low, high);

  temp= i-low; //數組前半部分元素個數

  if(temp>=k)

      return RS(arr, low, i-1, k); //求前半部分第k大

  else

      return RS(arr, i+1, high, k-i); //求後半部分第k-i大

}

 

 

這是一個典型的減治算法,遞歸內的兩個分支,最終只會執行一個,它的時間複雜度是O(n)。

 

再次強調一下:


    分治法,大問題分解爲小問題,小問題都要遞歸各個分支,例如:快速排序
    
    
    減治法,大問題分解爲小問題,小問題只要遞歸一個分支,例如:二分查找,隨機選擇
    
 

通過隨機選擇(randomized_select),找到arr[1, n]中第k大的數,再進行一次partition,就能得到TopK的結果。

 

五、總結

TopK,不難;其思路優化過程,不簡單:


    全局排序,O(n*lg(n))
    
    
    局部排序,只排序TopK個數,O(n*k)
    
    
    堆,TopK個數也不排序了,O(n*lg(k))
    
    
    分治法,每個分支“都要”遞歸,例如:快速排序,O(n*lg(n))


    
    減治法,“只要”遞歸一個分支,例如:二分查找O(lg(n)),隨機選擇O(n)
    
    
    TopK的另一個解法:隨機選擇+partition
    
 

知其然,知其所以然。

思路比結論重要。

 

3.介紹3 包括各種排序的空間及時間複雜度

二分查找與幾種排序方法

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章