TopK問題詳解

2022-05-01 12:06:09 字數 3564 閱讀 3313

【問題描述】(本文**以在面試題40. 最小的k個數中可提交)

在無序陣列 nums 中,找出最小(或最大)的 k 個數。例如,輸入[4, 5, 1, 6, 2, 7, 3, 8]這8個數字,則最小的4個數字是1、2、3、4。

直接將陣列進行排序,然後取出前 k 個元素即可。這是最容易想到的。

**略。

直接排序需要對整個陣列 n 個元素都進行排序(全域性操作),時間複雜度至少是o(n*logn),而我們只需要找出前 k 個元素即可(只需要區域性元素),顯然是小題大做了。我們能不能只進行區域性排序,拿到我們想要的 k 個元素就及時停止呢?

氣泡排序雖然平時很少用到,但我們知道它是乙個全域性排序,也就是說,每執行一次,就會有乙個元素確定其最終位置。因此,我們可以通過氣泡排序,執行 k 次便可以確定最終結果,時間複雜度是o(n*k)。當k << n時,o(n*k)的效能會比o(n*logn)好很多。

class solution }}

int res = new int[k];

for(int i = 0; i < k; i++)

return res;

}}

在面試題40. 最小的k個數中,使用冒泡也是能通過的,只不過效率很低,這是因為題目中並未說明k << n,而可能k == n。這裡僅作為一種思路。anyway,就假定k << n吧,我們已經將全域性排序優化成區域性排序了,但是!通過氣泡排序我們拿到的 k 個元素仍然是有序的,題目只要求我們取出最小的 k 個元素,並未要求這 k 個數有序,因此,我們還有進一步優化的空間。

對於求最小的 k 個元素,我們建立乙個大頂堆,保證堆中的元素不超過 k 個。大頂堆中存放的元素是當前陣列中前 k 個小的數。當要往大頂堆中插入元素時,先跟堆頂元素(也就是當前的最大值)進行比較,如果待插入的元素比堆頂元素要小,那麼堆頂元素不可能是前 k 個小的數了。於是替換掉堆頂元素,並調整堆,以保證堆內的 k 個元素,總是當前最小的 k 個元素。當遍歷完陣列,大頂堆中存留下來的 k 個元素就是所求結果。

時間複雜度為o(n*logk),其中o(n)是因為要變遍歷一趟陣列,o(logk)是每次堆結構調整所需要的時間。

class solution else if(!maxheap.isempty() && num < maxheap.peek()) 

}int res = new int[maxheap.size()];

int i = 0;

while(!maxheap.isempty())

return res;

}}

隨機選擇算在是《演算法導論》中乙個經典的演算法,其時間複雜度為o(n),是乙個線性複雜度的方法。為了說明隨機選擇演算法,需要先了解快速排序演算法。

快速排序演算法的偽**實現如下:

void quicksort(int nums, int left, int right)
其核心思想是分治法

【擴充套件】

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

二分查詢(binary search)就是乙個典型的運用減治法的一種演算法。其偽**如下:

int binarysearch(int nums, int target, int left, int right) else if(nums[mid] < target) else 

}

可以看到,每次查詢時,通過mid把原陣列分為左右兩個子分割槽,根據和target的比較,只需要進入其中乙個分割槽就可解決問題。這是和快速排序的最大不同。

通過上述說明,我們可以知道,減治法一般要比分治法的複雜度更低。

回到本題,解決topk問題可以從排序演算法中借鑑什麼思想呢?排序演算法的核心是劃分操作,即partition。它的作用是根據主元pivot調整陣列,把小於pivot的元素移到左側,把大於pivot的元素移到右側,從而確定pivot的最終位置。假設pivot的位置為k,也就是說,可以確定pivot是該陣列第k小的元素(這裡先假設下標從1開始)——這不就是topk問題要解決的問題嗎?

我們設法找到陣列中第k大的元素,那麼在該元素之前的所有元素,就是我們要求的topk了。

**實現如下:

class solution 

public int quicksort(int nums, int left, int right, int k) else if(pivotindex > k - 1) else

}// 劃分操作,返回主元索引

public int partition(int nums, int left, int right)

swap(nums, left, j);

return j;

}public void swap(int nums, int i, int j)

}

根據之前的分析,這是乙個典型的減治演算法,遞迴的兩個分支,每次只會執行其中乙個。

時間複雜度分析: 因為我們是要找下標為k的元素,第一次切分的時候需要遍歷整個陣列 (0 ~ n) 找到了下標是 j 的元素,假如 k 比 j 小的話,那麼我們下次切分只要遍歷陣列 (0~k-1)的元素就行了,反之如果 k 比 j 大的話,那下次切分只要遍歷陣列 (k+1~n) 的元素,總之可以看作每次呼叫 partition 遍歷的元素數目都是上一次遍歷的 1/2,因此時間複雜度是 n + n/2 + n/4 + ... + n/n = 2n,因此時間複雜度是 o(n)。

當元素值域限定在一定範圍內時,可以直接使用計數排序。比如,在面試題40. 最小的k個數中,限定了元素的大小在[0, 10000]之間,那麼,可以直接使用計數排序(也稱桶排序)來解決。

class solution 

int res = new int[k];

int index = 0;

for(int val = 0; val < count.length; val++)

}return res;

}}

使用計數排序的時間複雜度是o(n),也是很好的解法,只不過不是處理topk問題的通用解法,該解法參考了這篇題解。

處理topk問題,我們的思路演化過程是這樣的:

全域性排序,o(n*logn),直覺做法,不推薦

區域性排序,只排序topk個元素,o(n*k),只提供一種思路,不推薦

堆的應用,topk 個元素也不排序了,o(n*logk),topk問題的經典做法!

分治思想,如何利用partition操作找出topk元素,o(n),topk問題的經典做法!

計數排序(或稱桶排序),當元素的值域限定在一定範圍時,也可以使用這種方法,也是o(n)的時間複雜度,但不是通用解法。

同型別題目集:

面試題40. 最小的k個數

參考:

TopK問題詳解

1.基本topk問題描述 從1百萬個數中找出最大 或最小 的5個數 看到這個問題,很多同學的第一反應會是 排序。那麼,選擇哪種排序方法呢,有同學說 快排,將所有數排序後,再選出最大的5個。雖然快排確實能解決這個問題,但是需要對1百萬個數排序,但我們僅僅需要其中的5個。那麼,有更好的方法嗎?還記得我們...

Top K問題詳解

最容易想到的方法是將資料全部排序,然後在排序後的集合中進行查詢,最快的排序演算法的時間複雜度一般為o nlogn 如快速排序。但是在32位的機器上,每個float型別佔4個位元組,1億個浮點數就要占用400mb的儲存空間,對於一些可用記憶體小於400m的計算機而言,很顯然是不能一次將全部資料讀入記憶...

Top K 演算法詳解

假設目前有一千萬個記錄 這些查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個。乙個查詢串的重複度越高,說明查詢它的使用者越多,也就是越熱門。請你統計最熱門的10個查詢串,要求使用的記憶體不能超過1g。問題解析 要統計最熱門查詢,首先就是要統計每個query出現的次數,然後根據...