我們前面講過,佇列最大的特性就是先進先出。不過,在優先順序佇列中,資料的出隊順序不是先進先出,而是按照優先順序來,優先順序最高的,最先出隊。
乙個堆就可以看做乙個優先順序佇列。很多時候,它們只是概念上的區分而已。往優先順序佇列中插入乙個元素,就相當與往堆中插入乙個元素,從優先順序佇列中取出優先順序最高的元素,就相當於取出堆頂元素。
1.合併有序小檔案
假設我們有100個檔案,每個檔案的大小是100m,每個檔案中儲存的都是有序的字串。我們希望將這些100個小檔案合併成以乙個有序的大檔案。這裡就會用到優先順序佇列。
思路就跟我們之前合併陣列的操作一樣,我們從這100個檔案中,各取第乙個字串,放入陣列中,然後比較大小,把最小的那個字串放入合併後的大檔案中,並從陣列中刪除。
假設,這個最小的字串來自於13.txt這個小檔案,我們就再從這個小檔案取下乙個字串,並且放到陣列中,重新比較大小,並且選擇最小的放入合併後的大檔案,並且將它從陣列中刪除。依次類推,直到所有檔案中的資料都放入到大檔案為止。
如果我們單單使用陣列這種結構,那每一次都需要迴圈遍歷整個陣列,才能取出最小字元(或者維護乙個有序的陣列,然後每次都把下乙個字串插入到合適的位置,時間複雜度是o(n))。那麼,這個時候就可以使用優先順序佇列,也就是堆。我們把取出來的字串維護乙個小堆頂,堆頂的元素就是優先順序佇列的隊首元素,就是最小的字串。我們將這個字串放入大檔案中,並將其從堆中刪除,然後再下乙個小檔案取出下乙個字串,放入到堆中,迴圈這個過程就行。
這裡使用堆這種資料結構,插入和刪除操作的時間複雜度是o(logn),就變得比較高效。
2.高效能定時器
假設我們有乙個定時器,定時器中維護了很多的定時任務,每個任務都設定了乙個要觸發執行的時間點。定時器每過乙個很小的單位時間(比如1秒),就掃瞄一遍任務,看是否有任務到達設定的執行時間。如果到達了,就拿出來執行。
對於這個需求,我們的第一感覺就是可以把任務按照時間排序,把最早的放在首位,然後與當前的時間點相減得到乙個時間間隔t,這樣定時器就可以設定在t秒之後,再來執行任務。但是如果這個時候,你有新的任務要加入,我們要找到這個資料在陣列中合適的位置,就需要遍歷這陣列。時間複雜度是o(n)。如果我們使用堆來實現就不一樣了,我們維護乙個最小堆頂,然後取第乙個元素與當前時間點相減,得到t。這個時候,如果有新加入的任務,那麼插入乙個資料的時間複雜度是o(logn),就比噹噹使用陣列要快了。
我們把這種求top k 的問題抽象成兩類。一類是針對靜態資料集合,另一類是針對動態資料集合。
針對靜態資料,如何在乙個包含n個資料的陣列中,查詢前k大資料呢?我們可以維護乙個大小為k的小頂堆順序遍歷陣列,從陣列中國取出資料與堆頂元素比較。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,則不做處理。等陣列遍歷完成,堆中的資料就是前k大資料了。
遍歷陣列要o(n)的時間複雜度,每一次堆化的操作需要o(logk)的時間複雜度,所以最壞情況下,n個元素都入堆一次,所以時間複雜度就是o(nlogk)。
針對動態資料,如果每次詢問前k大資料,我們都基於當前的資料重新計算的話,那每一次都是o(nlogk),顯然不合理。實際上,我們可以一直維護乙個k大小的小頂堆,當有資料被新增到集合中時,我們就拿它與堆頂的元素對比。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,就不做處理。這樣就不用每次都計算一次了。
對於一組靜態資料,中位數是固定的,我們可以先排序,第n/2個資料就是中位數。每次詢問中位數的時候,我們直接返回這個固定的值就好了。所以,儘管排序的代價比較大,但是邊際成本會很小。但是,如果我們面對的是動態資料的集合,中位數在不停地變動,如果再用先排序的方法,每次查詢中位數的時候,都排序,那效率就很低了。
借助堆這種資料結構,我們不用排序,就可以非常高效地實現求中位數操作。
我們需要維護兩個堆,乙個大頂堆,乙個小頂堆。大頂堆中儲存前半部分資料,小頂堆中儲存後半部分資料,且小頂堆中的資料都大於大頂堆中的資料。
也就是說,如果有n個資料,我們從小到大排序,前n/2個資料儲存在大頂堆中,後n/2個資料儲存在小頂堆中。這樣,大頂堆中的堆頂元素就是我們要找的中位數。如果n是奇數,情況是類似的,大頂堆就儲存n/2+1個資料,小頂堆就儲存n/2個資料。
那麼如果資料是動態的,當新新增乙個資料的時候,如果新加入的資料小於等於大頂堆的堆頂元素,我們就將這個新資料插入到大頂堆,如果新加入的資料大於小頂堆的堆頂元素,我們就將這個新資料插入到小頂堆。
這個時候就有可能出現,兩個堆中的資料個數不符合前面約定的情況:如果n是偶數,兩個堆中的資料個數都是n/2;如果n是奇數,大頂堆有n/2+1 個資料,小頂堆有n/2個資料。這個時候,我們可以從乙個堆中不停地將堆頂元素移動到另乙個堆,通過這樣的調整,來讓兩個堆中的資料滿足上面的約定。
這樣,動態資料的插入,求中位數,只涉及堆化的操作,因此時間複雜度是o(logn)。
實際上,推廣一下,利用兩個堆不僅可以求出中位數,還可以快速取出其他百分位的資料,原理是類似的。假設我們要求99%的時間,我們可以維護兩個堆,乙個大頂堆,乙個小頂堆。假設當前總資料的個數是n,大頂堆中儲存n*99%個資料,小頂堆中儲存n*1%個資料。大頂堆堆頂的資料就是我們要找的99%的資料。插入資料之後跟前面的中位數是類似的。
我們把這10億個檔案分片之後,分別對這10個檔案建立雜湊表,然後維護乙個大小為10的堆,最後得到的是10個大小為10的堆。
資料結構與演算法之美
什麼是資料結構?什麼是演算法 狹義重點 複雜度分析 方法 邊學邊練,適度刷題 複雜度分析 時間複雜度 常見時間複雜度 非多項式量級 非常低效的演算法 空間複雜度 漸進空間複雜度,表示演算法的儲存空間和資料規模的增長關係 最好情況時間複雜度 理想情況的時間複雜度 最壞情況時間複雜度 最糟糕的情況下的時...
資料結構與演算法之美(筆記9)雜湊演算法
我們前面講到雜湊表,雜湊函式,這裡又是雜湊演算法,實際上,雜湊函式就是雜湊演算法的乙個特例。只不過在雜湊表中,我們通常希望雜湊函式簡單,才不會影響查詢等的效能。雜湊演算法的定義和原理很簡單,就是把任意二進位制串值對映為固定長度的二進位制值串,這個對映的規則就是雜湊演算法,而通過原始的資料對映之後得到...
《資料結構與演算法之美》筆記 鍊錶
typedef struct node node 陣列和鍊錶都是線性表。陣列必須是連續空間,而鍊錶無所謂。鍊錶 單鏈表 迴圈鍊錶 雙向鍊錶 陣列 插入 刪除的時間複雜度是o n 隨機訪問的時間複雜度是o 1 鍊錶 插入 刪除的時間複雜度是o 1 隨機訪問的時間複雜端是o n 快取淘汰策略 先進先出策...