面試--演算法--Top K
Top K問題是面試時手寫程式碼的常考題,某些場景下的解法與堆排和快排的關係緊密,所以把它放在堆排後面講。
下面先來還原一下Top K考試常見的套路。
你正緊張地坐在小隔間裡,聽著越來越近的腳步聲,內心忐忑,猶如兔脫。
推門聲呷然而起,你扭頭一看,身體已不由自主起立,打量著眼前來人,心裡一陣竊喜:還好,面善。
面試官點頭致意,你配合坐下,滿心滿眼一片赤誠,恨不得把公司的茶水阿姨都誇一遍來表明你面試的誠意。
面試官拿著你的簡歷目不斜視,有意無意間想起了經年彼時自己面試的緊張場景,自己走過的彎路不能讓他再重複走,於是決定先暖暖場活躍活躍氣氛,也多少祭奠一下那夕陽晚鐘,那迎風少年,那狗日的青春。
面試官說今天的天氣真好,你說藍天白雲不多見,哈哈哈哈。
你說貴公司的辦公環境真心不錯,面試官看看四周表面剋制內心放浪——那是自…不對,不能這麼說,年輕人面前還是不要喜形於色。
面試官說還好了,哈哈哈哈。
真的,環境很不錯,哈哈哈哈。你說著,手心大汗淋漓,嗓子裡幹得冒煙。
面試官眯著眼看了看你,說公司大了人就多,人多了資料就多了,現在有一組千萬級別的數,你能不能幫我找出最大的5個?儘量少用空間和時間。
你聽完風中凌亂一臉懵逼,電光火石之間抖一抖眼皮,一陣狂喜,還好看過丑旦的這篇筆記。
Offer,穩了。
…
嘿嘿,以上扯的這個淡,希望能加深你對Top K問題的印象^_^。
言歸正傳,筆者見過關於Top K問題最全的分類總結是在
本文主要來看看方便用程式碼解決的問題。
堆排解法
用堆排來解決Top K的思路很直接。
前面已經說過,堆排利用的大(小)頂堆所有子節點元素都比父節點小(大)的性質來實現的,這裡故技重施:既然一個大頂堆的頂是最大的元素,那我們要找最小的K個元素,是不是可以先建立一個包含K個元素的堆,然後遍歷集合,如果集合的元素比堆頂元素小(說明它目前應該在K個最小之列),那就用該元素來替換堆頂元素,同時維護該堆的性質,那在遍歷結束的時候,堆中包含的K個元素是不是就是我們要找的最小的K個元素?
實現:
在堆排的基礎上,稍作了修改,buildHeap和heapify函式都是一樣的實現,不難理解。
速記口訣:最小的K個用最大堆,最大的K個用最小堆。
public class TopK {
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] a = { 1, 17, 3, 4, 5, 6, 7, 16, 9, 10, 11, 12, 13, 14, 15, 8 };
int[] b = topK(a, 4);
for (int i = 0; i < b.length; i++) {
System.out.print(b[i] + ", ");
}
}
public static void heapify(int[] array, int index, int length) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int largest = index;
if (left < length && array[left] > array[index]) {
largest = left;
}
if (right < length && array[right] > array[largest]) {
largest = right;
}
if (index != largest) {
swap(array, largest, index);
heapify(array, largest, length);
}
}
public static void swap(int[] array, int a, int b) {
int temp = array[a];
array[a] = array[b];
array[b] = temp;
}
public static void buildHeap(int[] array) {
int length = array.length;
for (int i = length / 2 - 1; i >= 0; i--) {
heapify(array, i, length);
}
}
public static void setTop(int[] array, int top) {
array[0] = top;
heapify(array, 0, array.length);
}
public static int[] topK(int[] array, int k) {
int[] top = new int[k];
for (int i = 0; i < k; i++) {
top[i] = array[i];
}
//先建堆,然後依次比較剩餘元素與堆頂元素的大小,比堆頂小的, 說明它應該在堆中出現,則用它來替換掉堆頂元素,然後沉降。
buildHeap(top);
for (int j = k; j < array.length; j++) {
int temp = top[0];
if (array[j] < temp) {
setTop(top, array[j]);
}
}
return top;
}
}
時間複雜度
n*logK
速記:堆排的時間複雜度是n*logn,這裡相當於只對前Top K個元素建堆排序,想法不一定對,但一定有助於記憶。
適用場景
實現的過程中,我們先用前K個數建立了一個堆,然後遍歷陣列來維護這個堆。這種做法帶來了三個好處:(1)不會改變資料的輸入順序(按順序讀的);(2)不會佔用太多的記憶體空間(事實上,一次只讀入一個數,記憶體只要求能容納前K個數即可);(3)由於(2),決定了它特別適合處理海量資料。
這三點,也決定了它最優的適用場景。
快排解法
用快排的思想來解Top K問題,必然要運用到”分治”。
與快排相比,兩者唯一的不同是在對”分治”結果的使用上。我們知道,分治函式會返回一個position,在position左邊的數都比第position個數小,在position右邊的數都比第position大。我們不妨不斷呼叫分治函式,直到它輸出的position = K-1,此時position前面的K個數(0到K-1)就是要找的前K個數。
實現:
“分治”還是原來的那個分治,關鍵是getTopK的邏輯,務必要結合註釋理解透徹,自動動手寫寫。
public class TopK {
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] array = { 9, 3, 1, 10, 5, 7, 6, 2, 8, 0 };
getTopK(array, 4);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ", ");
}
}
// 分治
public static int partition(int[] array, int low, int high) {
if (array != null && low < high) {
int flag = array[low];
while (low < high) {
while (low < high && array[high] >= flag) {
high--;
}
array[low] = array[high];
while (low < high && array[low] <= flag) {
low++;
}
array[high] = array[low];
}
array[low] = flag;
return low;
}
return 0;
}
public static void getTopK(int[] array, int k) {
if (array != null && array.length > 0) {
int low = 0;
int high = array.length - 1;
int index = partition(array, low, high);
//不斷調整分治的位置,直到position = k-1
while (index != k - 1) {
//大了,往前調整
if (index > k - 1) {
high = index - 1;
index = partition(array, low, high);
}
//小了,往後調整
if (index < k - 1) {
low = index + 1;
index = partition(array, low, high);
}
}
}
}
}
時間複雜度
n
速記:記住就行,基於partition函式的時間複雜度比較難證明,從來沒考過。
適用場景
對照著堆排的解法來看,partition函式會不斷地交換元素的位置,所以它肯定會改變資料輸入的順序;既然要交換元素的位置,那麼所有元素必須要讀到記憶體空間中,所以它會佔用比較大的空間,至少能容納整個陣列;資料越多,佔用的空間必然越大,海量資料處理起來相對吃力。
但是,它的時間複雜度很低,意味著資料量不大時,效率極高。
好了,兩種解法寫完了,趕緊實現一下吧。