面試中,topk,是問得比較多的幾個問題之一,到底有幾種方法,這些方案裡蘊含的優化思路究竟是怎麼樣的,今天和大家聊一聊。
問題描述:
從arr[1, n]這n個數中,找出最大的k個數,這就是經典的topk問題。
栗子:
從arr[1, 12]= 這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(intarr, 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);
其核心演算法思想是,分治法。
分治法有乙個特例,叫減治法。
二分查詢binary_search,bs,是乙個典型的運用減治法思想的演算法,其偽**是:
int bs(intarr, 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]劃分成左右兩個子陣列:
以上述topk的陣列為例,先用第乙個元素t=arr[low]為劃分依據,掃瞄一遍陣列,把陣列分成了兩個半區:
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);
畫外音:這一段非常重要,多讀幾遍。
這就是隨機選擇演算法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,不難;其思路優化過程,不簡單:
TopK大問題的另一種解法
不久前介紹了堆排序python堆排序之heapq,主要是解決下面這個題目 在未排序的陣列中找到第 k 個最大的元素。請注意,你需要找的是陣列排序後的第 k 個最大的元素,而不是第 k 個不同的元素。示例 1 輸入 3,2,1,5,6,4 和 k 2 輸出 5 示例 2 輸入 3,2,3,1,2,4,...
凸包問題的五種解法
首先,什麼是凸包?假設平面上有p0 p12共13個點,過某些點作乙個多邊形,使這個多邊形能把所有點都 包 起來。當這個多邊形是凸多邊形的時候,我們就叫它 凸包 如下圖 然後,什麼是凸包問題?我們把這些點放在二維座標系裡面,那麼每個點都能用 x,y 來表示。現給出點的數目13,和各個點的座標。求構成凸...
凸包問題的五種解法
首先,什麼是凸包?假設平面上有p0 p12共13個點,過某些點作乙個多邊形,使這個多邊形能把所有點都 包 起來。當這個多邊形是凸多邊形的時候,我們就叫它 凸包 如下圖 然後,什麼是凸包問題?我們把這些點放在二維座標系裡面,那麼每個點都能用 x,y 來表示。現給出點的數目13,和各個點的座標。求構成凸...