如何快速獲取到topK?


堆這種數據結構應用場景很多,最經典的莫過於堆排序。堆排序是一種原地的、時間復雜度為O(nlogn)的排序算法。我們今天就來分析一下堆這種數據結構。

一、什么是堆

堆是一種特殊的樹。只要滿足以下兩點,就稱為堆。

  • 堆是一個完全二叉樹。
  • 堆的每一個節點的值都必須大於等於(或小於等於)其子樹中每個節點的值。

       對於每個節點的值都大於等於其子樹中每個節點的值的堆,我們叫做“大頂堆”。對於每個節點的值都小於等於其子樹中每個節點的值的堆,我們叫做“小頂堆”。

二、如何實現一個堆

      首先我們需要知道堆都支持哪些操作以及如何存儲一個堆。

      對於一個完全二叉樹來說,用數組來存儲是非常節省存儲空間的。因為我們不需要存儲指向左右子節點的指針,單純的通過數組下標就可以找到一個節點的左右子節點和父節點。如下圖所示。

如圖所示,數組中下標為i的節點,它的左子節點的下標為2i,它的右子節點下標為2i+1,它的父節點的下標為i/2。下面我們再來看一下堆上有哪些常用的操作。我們以大頂堆為例,來看一下堆的插入操作和刪除操作。

1.往堆中插入元素

      往堆中插入一個新的元素后,我們需要繼續滿足堆的兩個特性。

      如果我們把新插入的元素放到堆的最后,是不符合堆的特性的,所以我們需要調整,以保證其滿足堆的特性。調整分為向上調整和向下調整。我們先以向上調整為例。如下圖所示。

     這個過程很簡單,我們讓新插入的節點和其父節點對比大小。如果不      滿足子節點小於等於父節點,我們就互換兩個節點。一直重復這個過程,直到滿足要求。

2.刪除堆頂元素

  接下來我們來看一下刪除操作。我們首先把堆的最后一個元素和堆頂元素互換位置,然后刪除最后一個元素。剩下的堆元素是不滿足堆的要求的,我們需要從堆頂開始從上往下調整,直到父子節點滿足大小關系為止。如下圖所示。

      我們知道一個包含n個節點的完全二叉樹,樹的高度不會超過log2n,堆調整的過程是順着節點所在的路徑進行比較交換,所以時間復雜度是和堆的高度成正比的,也就是O(logN)。插入數據和刪除堆頂數據的主要邏輯就是堆的調整,所以往堆里插入一個元素和刪除堆頂元素的時間復雜度是O(logN)。

三、堆排序

       我們上次講了很多的排序方法,可以點擊排序算法去查看。今天我們繼續講一種新的排序算法-堆排序,它的時間復雜度是O(nlogN),並且是原地排序算法。我們可以把堆排序的過程大致分為兩大步驟,分別是建堆和排序。

1.建堆

        我們首先將數組原地建一個堆。“原地”的含義就是不借助另一個數組,就在原數組上操作。我們的實現思路是從后往前處理數據,並且每個數據都是從上向下調整。

        我們看一下下面的建堆分解步驟圖。由於葉子節點向下調整只能自己跟自己比較,所以我們直接從最后一個非葉子節點開始,依次向下調整就好了。

      如圖所示,我們對下標從n/2開始一直到1的數據進行向下調整,下標是n/2+1到n的節點是葉子節點,所以我們不需要調整。\

2.排序

       建堆結束后,數組中的數據已經按照大頂堆的特性進行組織了。數組中的第一個元素就是堆頂,也就是最大的元素。我們把它和最后一個元素交換,那最大的元素就放到了下標為n的位置。

       這個過程類似於刪除堆頂操作,當堆頂元素移除以后,我們把下標為n的元素放到堆頂,然后再進行向下調整,將剩下的n-1個元素重新構建成堆。調整完成之后,我們再取堆頂元素,放到下標為n-1的位置,一直重復這個過程,直到堆中最后只剩下下標為1的一個元素,排序工作就完成了。

      現在我們來分析一下堆排序的時間復雜度、空間復雜度以及穩定性。整個堆排序的過程中,只需要個別的臨時存儲空間,所以堆排序是原地排序算法。堆排序包括建堆和排序兩個操作,建堆的時間復雜度是O(n),排序過程時間復雜度是O(nlogN)。所以,堆排序的整個時間復雜度是O(nlogN)。因為在排序的過程中,存在將堆的最后一個節點跟堆頂互換的操作,所以有可能會改變值相同數據的原始相對順序,所以堆排序不是穩定的排序算法。

四、堆的應用

下面我們來說一下堆的幾個非常重要的應用。

1.優先級隊列

 優先級隊列,顧名思義,它首先是一個隊列。隊列的最大特性就是先進先出。但是,在優先級隊列中,出隊的順序不是按照先進先出,而是按照優先級來,優先級高的先出隊。     

如何實現一個優先級隊列呢?其實有很多方法,不過使用堆來實現是最直接、最高效的。因為堆和優先級隊列非常相似。一個堆就可以看做是一個優先級隊列。往優先級隊列中插入一個元素,就相當於往堆中插入一個元素;從優先級隊列中取出最高優先級的元素,就相當於取出堆頂元素。我們來看一下下面這樣一個應用場景。

假如我們有100個小文件,每個文件的大小是100MB。每個文件中存儲的都是有序的字符串。我們希望將這些小文件合並成一個有序大文件。這里就會用到優先級隊列。      我們將從100個小文件中,各取出一個字符串,然后我們建立小頂堆,那堆頂的元素,也就是優先級隊列的隊首元素,也就是最小的字符串。我們將這個字符串放到大文件中,並將其從堆中刪除。然后再從小文件中取出下一個字符串放入堆中。循環此過程,就可以將100個小文件的數據依次放入到大文件中。

2.利用堆求topK

我們可以把求topk的問題抽象成2類。一類是針對靜態數據集合,也就是說數據集合事先確定,不會再變。另一類是針對動態數據集合,也就是說數據集合事先不確定,有數據動態地加入到集合中。    針對靜態數據集合,如何在包含n個數據的數組中,查找前K大數據呢?我們可以維護一個大小為k的小頂堆,順序遍歷數組,從數組中取出數據和堆頂元素比較。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,我們就不做處理,繼續遍歷數組。這樣等數組中的數據都遍歷完成之后,堆中的數據就是前K大數據了。    針對動態數據求得topK,也就是實時topK。怎么理解呢?我舉個例子。一個數據集合中有兩個操作,一個是添加數據,另一個就是詢問當前的前K大數據。     如果每次詢問前k大數據時,我們都基於當前的數據重新計算的話,那時間復雜度就是O(nlogN),n表示當前數據的大小。實際上我們可以一直維護一個k大小的小頂堆,當有數據要添加到集合中時,我們就拿它與堆頂元素做對比。如果比堆頂元素大,我們把堆頂元素刪除,並將這個元素插入到堆中;如果比堆頂元素小,我們則不做處理。這樣,不論何時需要查詢前K大數據,我們都可以立刻返回給它。

      更多硬核知識,請關注公眾號“程序員學長”。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM