面試 TopK演算法解析

2021-09-10 08:58:17 字數 3925 閱讀 8709

最簡單且最容易想到的演算法是對陣列進行排序(快速排序),然後取最大或最小的k個元素。總的時間複雜度為o(n*logn)+o(k)=o(n*logn)。該演算法存在以下問題:

快速排序的平均複雜度為o(n*logn),但最壞時間複雜度為o(n2),不能始終保證較好的複雜度

只需要前k大或k小的數,,實際對其餘不需要的數也進行了排序,浪費了大量排序時間

總結:通常不會採取該方案。

雖然我們不會採用快速排序的演算法來實現top-k問題,但我們可以利用快速排序的思想,在陣列中隨機找乙個元素key,將陣列分成兩部分sa和sb,其中sa的元素》=key,sb的元素如此遞迴下去,不斷把問題分解為更小的問題,直到求出結果。

該演算法的平均時間複雜度為o(n * logk)。以求k大的數為例,演算法實現如下:

public static int findtopk(int array, int left, int right, int k)  else if (len < k)  else 

}return index;

}/**

* 按基準點劃分陣列,左邊的元素大於基準點,右邊的元素小於基準點

* * @param array

* @param left

* @param right

* @return

*/public static int partition(int array, int left, int right)

while (array[left] >= x && left < right) //從前向後掃瞄,找到第乙個比基準點小的元素

left++;

if (left < right)

} while (left < right);

array[left] = x;

return left;

}

單元測試:

@test

public void testfindkmax_1() ;

topk.findtopk(array, 0, array.length - 1, k);

logger.info("array top k:{}", arrays.stream(array).maptoobj(value -> string.valueof(value))

.limit(k).collect(collectors.joining(",")));

}

尋找n個數中的第k大的數,可以將問題轉化尋找n個數中第k大的問題。對於乙個給定的數p, 可以在o(n)的時間複雜度內找出所有不小於p的數。

根據分析,可以使用二分查詢的演算法思想來尋找n個數中第k大的數。假設n個數中最大的數為vmax,最小的數為vmin, 那麼n個數中第k大的數一定在區間[vmin,vmax]之間。然後在這個區間使用二分查詢演算法。演算法實現如下:

public static listfindtopk(int array, int k) 

if (min > array[i])

}listtopklist = new arraylist<>();

int key = findk(array, max, min, k);

for (int i = 0; i < array.length; i++)

}return topklist;

}/**

* 尋找第k大的元素

* * @param array

* @param max

* @param min

* @param k

* @return

*/private static int findk(int array, int max, int min, int k) else

}return min;

}/**

* 統計不小於key的元素個數

* * @param array

* @param key

* @return

*/private static int findknum(int array, int key)

return sum;

}

總結:該演算法實際應用效果不佳,尤其是不同的資料型別需要確定max - min > delta,因此時間複雜度跟資料分布有關。 整個演算法的時間複雜度為o(n * log(vmax-vmin)/delta),在資料分布平均的情況下,時間複雜度為o(n * logn)。

上面幾種解法都會對資料訪問多次,那麼就有乙個問題,當陣列中元素個數非常大時,如:100億,這時候資料不能全部載入到記憶體,就要求我們盡可能少的遍歷所有資料。針對這種情況,下面我們介紹一種針對海量資料的解決方案。

在學習堆排序的過程中,我們知道了堆這種資料結構。為了查詢top k大的數,我們可以使用大根堆來儲存最大的k個元素。大根堆的堆頂元素就是最大k個數中最小的乙個。每次考慮下乙個數x時,如果x比堆頂元素小,則不需要改變原來的堆。如果想x比堆頂元素大,那麼用x替換堆頂元素, 同時,在替換之後,x可能破壞最小堆的結構,需要調整堆來維持堆的性質。演算法實現如下:

public static int findtopk(int array, int k) 

buildmaxheap(heaparray);

for (int i = k; i < array.length; i++)

}return heaparray;

}/**

* 構建大根堆

* * @param array

*/public static void buildmaxheap(int array)

}/**

* 調整堆結構

* * @param array

* @param root 根節點

* @param length

*/public static void adjustmaxheap(int array, int root, int length)

if (right < length && array[right] > array[largest])

if (root != largest)

}/**

* 交換

* * @param arr

* @param i

* @param j

*/public static void swap(int arr, int i, int j)

總結:該演算法只需要掃瞄所有的資料一次,且不會占用太多記憶體空間(只需要容納k個元素的空間),尤其適合處理海量資料的場景。演算法的時間複雜度為o(n * logk),這實際上相當於執行了部分堆排序。

擴充套件:當k仍然很大,導致記憶體無法容納k個元素時,我們可以考慮先找最大的k1個元素,然後再找看k1+1到2*k1個元素,如此類推。(其中容量為k1的堆可以完全載入記憶體)

public static listfindtopk(int array, int k) 

}int count = new int[max + 1];

for (int i = 0; i < array.length; i++)

listtopklist = new arraylist<>();

for (int sumcount = 0, j = count.length - 1; j >= 0; j--)

}if (sumcount >= k)

}return topklist;

}

這是乙個典型的以空間換取時間的做法。當陣列中取值範圍比較大時,是及其浪費空間的。如[3,1...9999],為了求出最大的k個元素,需要額外申請乙個長度為10000的陣列。

極端情況下,如果 n 個整數各不相同,我們甚至只需要乙個 bit 來儲存這個整數是否存在,這樣可節省很大的記憶體空間。

Top K演算法詳細解析 百度面試

搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1 255位元組。假設目前有一千萬個記錄,這些查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個。乙個查詢串的重複度越高,說明查詢它的使用者越多,也就是越熱門。請你統計最熱門的10個查詢串,要求...

Top K演算法詳細解析 百度面試

搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1 255位元組。假設目前有一千萬個記錄,這些查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個。乙個查詢串的重複度越高,說明查詢它的使用者越多,也就是越熱門。請你統計最熱門的10個查詢串,要求...

面試 演算法 Top K

top k問題是面試時手寫 的常考題,某些場景下的解法與堆排和快排的關係緊密,所以把它放在堆排後面講。下面先來還原一下top k考試常見的套路。你正緊張地坐在小隔間裡,聽著越來越近的腳步聲,內心忐忑,猶如兔脫。推門聲呷然而起,你扭頭一看,身體已不由自主起立,打量著眼前來人,心裡一陣竊喜 還好,面善。...