TopK問題

原文連接:http://www.6aiq.com/article/1538030833699php

介紹了五種解決TopK問題的方法:git

1. 全排序;2. 冒泡式部分排序;3. 堆排序(部分排序);4. 隨機選擇;5. 比特圖。程序員

這裏我認爲最好的是堆排序,時間複雜度O(n*lg(k));github

全排序不穩定,即便快排,平均也有O(n*lg(n));web

冒泡部分排序的時間複雜度O(n*k);面試

隨機選擇與比特圖我認爲是不穩定的,隨機選擇的平均時間複雜度較低,爲O(n),比特圖也是,但最壞就會很大。算法

如下是原文:數組

-----------------------------------------------------------------------------------------------------------------------------------------------------數據結構

5 種方法求解 TopK!面試不要再問我 Topk 了~


 

 AIQ - 最專業的機器學習大數據社區  http://www.6aiq.com架構

AIQ 機器學習大數據 知乎專欄 點擊關注

 

前言:本文將介紹隨機選擇,分治法,減治法的思想,以及 TopK 問題優化的前因後果,原理與細節,保證有收穫。

 

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

_ 畫外音:_除非校招,我在面試過程當中從不問 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 個。

 

1、排序

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

sort(arr, 1, n);

return arr[1, k];

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

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

 

2、局部排序

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

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

 

僞代碼

for(i=1 to k){

         bubble_find_max(arr,i);

}

return arr[1, k];

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

 

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

 

3、堆

思路:只找到 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 的經典算法,那還有沒有更快的方案呢?

 

4、隨機選擇

隨機選擇算在是《算法導論》中一個經典的算法,其時間複雜度爲 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 的結果。

 

5、總結

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

知其然,知其因此然。

思路比結論重要。

但願你們對 TopK 有新的認識,謝轉。

挖坑:TopK,你覺得這就是最快的解法?過小看架構師之路了,更快方案,且聽下一期分解。

其中隨機選擇 (randomized select) 最爲經典,用減治法 (Reduce & Conquer) 的思想,將數據規模急速下降,整體複雜度爲 O(n)。

結尾挖了一個坑:求 TopK,有沒有比隨機選擇更快的方法呢?

空間換時間,是算法優化中最多見的手段,若是有相對充裕的內存,能夠有更快的算法。

畫外音:即便內存不夠,也能夠水平切分,使用分段的方法來操做,減小每次內存使用量。

TopK 問題描述

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

比特位圖(bitmap)

bitmap,是空間換時間的典型表明。它是一種,用若干個 bit 來表示集合的數據結構。

例如,集合 S={1,3,5,7,9},容易發現,S 中全部元素都在 1-16 之間,因而,能夠用 16 個 bit 來表示這個集合:存在於集合中的元素,對應 bit 置 1,不然置 0。

畫外音:究竟須要多少存存儲空間,取決於集合中元素的值域,在什麼範圍以內。

上述集合 S,能夠用 1010101010000000 這樣一個 16bit 的 bitmap 來表示,其中,第 1, 3, 5, 7, 9 個 bit 位置是 1。

 

假設 TopK 的 n 個元素都是 int,且元素之間沒有重複,只須要申請 2^32 個 bit,即 4G 的內存,就可以用 bitmap 表示這 n 元素。

掃描一次全部 n 個元素,以生成 bitmap,其時間複雜度是 O(n)。生成後,取 TopK 只須要找到最高位的 k 個 bit 便可。算法總時間複雜度也是 O(n)。

僞代碼爲:

bitmap[4G] = make_bitmap(arr[1, n]);

return bitmap[top k bits];

 

bitmap 算法有個缺點,若是集合元素有重複,相同的元素會被去重,假設集合 S 中有 5 個 1,最終 S 製做成 bitmap 後,這 5 個 1 只對應 1 個 bit 位,至關於 4 個元素被丟掉了,這樣會致使,找到的 TopK 不許。該怎麼優化呢?


比特位圖計數

優化方法是,每一個元素的 1 個 bit 變成 1 個計數。

如上圖所示,TopK 的集合通過比特位圖計數處理後,會記錄每一個 bit 對應在集合 S 中出現過多少次。

接下來,找 TopK 的過程,就是 bitmap 從高位的計數開始,往低位的計數掃描,獲得 count 之和等於 k,對應的 bit 就是 TopK 所求。

如上圖所示,k=5:

(1)第一個非 0 的 count 是 1,對應的 bit 是 9;

(2)第二個非 0 的 count 也是 1,對應的 bit 是 8;

(3)第三個非 0 的 count 是 2,對應的 bit 是 7;

(4)第四個非 0 的 count 是 2,對應的 bit 是 6,但 TopK 只缺 1 個數字了,故只有 1 個 6 入選;

故,最終的 TopK={9, 8, 7, 7, 6}。

結論:經過比特位圖精準計數的方式,求解 TopK,算法總體只須要不到 2 次掃描,時間複雜度爲 O(n),比減治法的隨機選擇會更快。

爲了鞏固今天的內容,例行挖個坑。

面試中,還有個問題問得比較多:求一個正整數的二進制表示包含多少個 1?

例如:7 的二進制表示是 111,即 7 的二進制表示包含 3 個 1。

畫外音:我面試過程當中從不問這個問題。

最多見的解法是:

uint32_t count_one(uint32_t n){

    uint32_t count=0;

    while(n){

        count ++;

        n &= (n-1);

    }

    return count;

}

 

 

更多高質資源 盡在AIQ 機器學習大數據 知乎專欄 點擊關注

—— AIQ - 最專業的機器學習大數據社區  http://www.6aiq.com

 

這裏是國內最早進活躍的機器學習大數據生態社區,你們相互學習,以平等 • 自由 • 樂於分享的價值觀進行分享交流,AIQ - 讓咱們一塊兒定義互聯網下一個時代!        

關於 API 公告 領域 標籤 數據統計 站點地圖 站長統計 魯ICP備18016225號

© 2018 www.6aiq.com 機器學習大數據社區

Feel easy about trust.

Powered by B3log開源 • 3.4.5 • 33ms Sym 3.4.5 • 33ms

0

機器學習社區機器學習社區

5 種方法求解 TopK!面試不要再問我 Topk 了~