有這樣一個算法題:有一個無序數組,要求找出數組中的第K大元素。好比給定的無序數組以下所示:
java
若是k=6,也就是要尋找第6大的元素,很顯然,數組中第一大元素是24,第二大元素是20,第三大元素是17...... 第六大元素是9。
算法
這是最容易想到的方法,先把無序數組從大到小進行排序,排序後的第k個元素天然就是數組中的第k大元素。可是這種方法的時間複雜度是O(nlogn),性能有些差。
數組
維護一個長度爲k的數組A的有序數組,用於存儲已知的K個較大的元素。而後遍歷無序數組,每遍歷到一個元素,和數組A中的最小元素進行比較,若是小於等於數組A中的最小元素,繼續遍歷;若是大於數組A中的最小元素,則插入到數組A中,並把曾經的最小元素"擠出去"。性能
好比K=3,先把最左側的7,5,15三個數有序放入到數組A中,表明當前最大的三個數。
ui
此時,遍歷到3時,因爲3<5,繼續遍歷。
3d
接下來遍歷到17,因爲17>5,插入到數組A的合適位置,相似於插入排序,並把原先最小的元素5「擠出去」。
code
繼續遍歷原數組,一直遍歷到數組的最後一個元素......blog
最終,數組A中存儲的元素是24,20,17,表明着整個數組的最大的3個元素。此時數組A中的最小元素17就是咱們要尋找的第K大元素。
排序
這個方法的時間複雜度是O(nk),可是若是K的值比較大的話,其性能可能還不如方法一。class
二叉堆是一種特殊的徹底二叉樹,它包含大頂堆和小頂堆兩種形式。其中小頂堆的特色是每個父節點都小於等於本身的兩個子節點。要解決這個算法題,咱們能夠利用小頂堆的特性。
維護一個容量爲K的小頂堆,堆中的K個節點表明着當前最大的K個元素,而堆頂顯然是這K個元素中的最小值。
遍歷原數組,每遍歷一個元素,就和堆頂比較,若是當前元素小於等於堆頂,則繼續遍歷;若是元素大於堆頂,則把當前元素放在堆頂位置,並調整二叉堆(下沉操做)。
遍歷結束後,堆頂就是數組的最大K個元素中的最小值,也就是第K大元素。
假設K=5,具體操做步驟以下:
遍歷到元素2,因爲2<3,因此繼續遍歷。
遍歷到元素20,因爲20>3,20取代堆頂位置,並調整堆。
遍歷到元素24,因爲24>5,24取代堆頂位置,並調整堆。
以此類推,咱們一個一個遍歷元素,當遍歷到最後一個元素8時,小頂堆的狀況以下:
這個方法的時間複雜度是多少呢?
1.構建堆的時間複雜度是O(K)
2.遍歷剩餘數組的時間複雜度O(n-K)
3.每次調整堆的時間複雜度是O(logk)
其中2和3是嵌套關係,1和2,3是並列關係,因此總的最壞時間複雜度是O((n-k)logk + k)。當k遠小於n的狀況下,也能夠近似地認爲是O(nlogk)。
這個方法的空間複雜度是多少呢?
剛纔咱們在詳細步驟中把二叉堆單獨拿出來演示,是爲了便於理解。但若是容許改變原數組的話,咱們能夠把數組的前K個元素「原地交換」來構建成二叉堆,這樣就免去了開闢額外的存儲空間。所以空間複雜度是O(1)。
代碼以下:
/** * 尋找第k大元素 * @param array 待調整的數組 * @param k 第幾大 * @return */ public static int findNumberK(int[] array, int k) { //1.用前k個元素構建小頂堆 buildHeap(array, k); //2.繼續遍歷數組,和堆頂比較 for (int i = k; i < array.length; i++) { if(array[i] > array[0]) { array[0] = array[i]; downAdjust(array, 0, k); } } //3.返回堆頂元素 return array[0]; } private static void buildHeap(int[] array, int length) { //從最後一個非葉子節點開始,依次下沉調整 for (int i = (length - 2) / 2; i >= 0; i--) { downAdjust(array, i, length); } } /** * 下沉調整 * @param array 待調整的堆 * @param index 要下沉的節點 * @param length 堆的有效大小 */ private static void downAdjust(int[] array, int index, int length) { //temp保存父節點的值,用於最後的賦值 int temp = array[index]; int childIndex = 2 * index + 1; while (childIndex < length) { //若是有右孩子,且右孩子小於左孩子的值,則定位到右孩子 if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) { childIndex++; } //若是父節點小於任何一個孩子的值,直接跳出 if (temp <= array[childIndex]) break; //無需真正交換,單項賦值便可 array[index] = array[childIndex]; index = childIndex; childIndex = 2 * childIndex + 1; } array[index] = temp; } public static void main(String[] args) { int[] array = new int[] {7, 5, 15, 3, 17, 2, 20, 24, 1, 9, 12, 8}; System.out.println(findNumberK(array, 5)); }
你們都瞭解快速排序,快速排序利用分治法,每一次把數組分紅較大和較小元素兩部分。咱們在尋找第K大元素的時候,也能夠利用這個思路,以某個元素A爲基準,把大於A的元素都交換到數組左邊,小於A的元素交換到數組右邊。
好比咱們選擇以元素7做爲基準,把數組分紅了左側較大,右側較小的兩個區域,交換結果以下:
包括元素7在內的較大元素有8個,但咱們的K=5,顯然較大元素的數目過多了。因而咱們在較大元素的區域繼續分治,此次以元素12爲基準:
這樣一來,包括元素12在內的較大元素有5個,正好和K相等。因此,基準元素12就是咱們所求的。
這就是分治法的思想,這種方法的時間複雜度甚至優於小頂堆法,能夠達到O(n)。