(最近學習一些冷門但是高階的資料結構,頗為吃力,但非常實用,故做篇筆記)
實踐上經常使用基於完全二叉樹的堆來實現優先佇列,其訪問最小值的操作gettop
、插入操作push
以及刪除操作pop
,時間複雜度均不超過o(logn),而且結構簡單使用方便,所以應用也最廣泛。
今天介紹另一款重量級嘉賓:基於左偏樹的可並堆(mergeable heap)。從應用層面來講,它完全相容二叉堆的操作,而且另外新增了乙個擴充套件功能:將兩個堆合併起來。可並堆的優勢在於:如果將兩個普通的沙堆進行合併,那麼只能是對其中某個沙堆一鏟子一鏟子地把沙土揚到另乙個沙堆上去,時間代價是o(nlogn)。但是可並堆能夠將時間代價降低至對數級別o(logn),接近一次性混合,挖掘機技術我最強。
二叉堆是利用完全二叉樹實現的,而可並堆是利用左偏樹實現的。關於左偏,先給出這麼幾個設定:
1.1 外結點
左子樹或右子樹為null的結點,即為外結點。要注意葉子結點,也是外結點。
1.2 結點的距離結點具有距離屬性,它的值為:從該結點出發向下找到最近的外結點時,所經過的路徑長度。
外結點本身的距離,值為0;
規定空結點null也具有距離屬性,值為-1.
1.3 左偏對於某個結點來說,如果左子樹的距離不低於右子樹的距離,則稱該結點左偏。
下面這棵樹(fig-1.1)中,標記為藍色的即為外結點,結點旁邊的數字表示其距離值,由於結點c
的左子樹null
具有相對於右子樹f
更低的距離屬性,所以c是右偏結點,不是左偏結點。
左偏樹是指所有結點都左偏的二叉樹。遞迴定義時,它要麼是個空樹,要麼是滿足下列性質的二叉樹:
1. 根結點左偏;
2. 左子樹和右子樹也都是左偏樹。
根據這種定義,可以得到一些非常重要的推論:
2.1 推論一:左偏樹的距離只與右子樹有關
由於左子樹的距離不低於右子樹的距離(左偏性質),所以結點向下尋找最近的外結點,一定總是位於右子樹,路徑也一定總是位於樹的最右側。所以,乙個結點的距離便等於向下的最右側路徑的長度。
輕而易舉地我們也可以推知,非空結點的距離dist
,等於該結點的右子結點的距離dist-rchild
,再加上1,公式如下:
2.2 推論二:左偏樹距離的最大值(左偏樹定理)
可證得定理:若一棵左偏樹具有n
個結點,那麼左子樹的距離(即根結點的距離)dist
有上界,關係式為:
嚴格的證明方法這裡不贅述,只簡述思路如下:因為左子樹的距離一定不低於右子樹的距離,所以在最壞情況下如果右子樹的距離取到了上界,那麼左子樹的距離也一定取到了上界,那麼左偏樹便被填滿成為滿二叉樹,這時根結點距離為k
的滿二叉樹,總共有2^(k+1)-1
個結點,與上述公式吻合。
2.3 左偏樹的形狀
左偏樹不是二叉堆那樣的完全二叉樹,也不是平衡的二叉樹,它可能是任意形狀。在最壞情況下,所有非葉結點都只有左子樹而沒有右子樹,這時整棵左偏樹退化為乙個單鏈表,但它仍然是一棵高效的、距離為0的左偏樹:
這裡我們一定要注意,左偏樹的高效在於:它總是騰出並且能夠騰出右側的空間以供插入,不論要插入的規模有多大。這便是可並堆的核心思想。
左偏樹最好以二叉鍊錶方式實現,因為順序陣列只在完全二叉樹的表示上有優勢。
左偏樹無需考慮元素的優先性、比較性;但是堆的定義要求堆頂元素總是最小的,自頂向下也都是有序的。所以利用左偏樹實現堆的時候,元素之間的關係成為了問題的主要方面:
兩個堆的合併(可並堆的基本性質);
彈出堆頂的元素(最小元);
插入乙個元素。
3.1 左偏樹的合併操作
我們以左偏樹來表示可並堆,合併過程便是兩棵左偏樹合併的過程:將優先順序靠後的堆(堆頂元素相對較大的那個堆)插入到另乙個堆的右子樹上,然後如果右側比重變大導致了右偏,那就交換左右子樹。演算法(偽**)如下:
typedef priority_queue mergeable_heap;
//優先佇列型別:可並堆
function merge
(mergeable_heap& a,
& b)
if a is null, then return b;
//空堆不用混合
if b is null, then return a;
if(b.
heaptop
() takes more priority than a.
heaptop()
) then swap
(a, b)
;//將a置為主堆,b往a裡面插
a.rchild =
merge
(a.rchild, b)
;//b與a的右子樹合併,作為a的新的右子樹if(
dist
(a.lchild)
<
dist
(a.rchild)
) then swap
(a.lchild, a.rchild)
;//交換左右子樹,保持左偏性質
dist
(a)=
dist
(a.rchild)+1
;//更新a的距離,依據是推論一
return a;
//返回主堆
end
可以從下圖加深理解。每一步merge之前,都要先比較兩個堆頂元素的優先順序,必要時進行swap以確立新的堆頂:
3.2 訪問並彈出堆頂元素
這個簡單,取出堆頂元素,然後左右子樹合併即可:
function pop
(mergeable_heap& a)
temp = a;
a =merge
(a.lchild, a.rchild)
;return temp;
end
3.3 插入乙個元素
這個也簡單,將元素視為只有乙個容量的堆,再合併:
function insert
(mergeable_heap& a, elem x)
a =merge
(a,(mergeable_heap)x)
;end
3.4 合併操作的時間複雜度分析(重點):
遞迴地合併到右子樹上,最壞情況下,每一次合併都要swap一下主堆和副堆的次序,即兩個堆的最右側路徑都走了一遍。
假設兩個隊的元素分別有n1
和n2
個,那麼最右側路徑的長度,也就是左偏樹的距離,可以表示為log(n1) + log(n2)
量級,所以時間複雜度是o(logn)。
// 結點型別定義
typedef
struct binode };
// 求結點的距離
intdist
(binode* r)
// 優先順序比較器類
typedef
struct cmp };
//交換
void
mswap
(binode*
& ra, binode*
& rb)
//合併
binode*
merge
(binode* ra, binode* rb)
左偏樹(可並堆)
左偏樹其實是一種可並堆,它可以 o log2 n o l og2n 合併兩個堆。那左偏?也就是說他左邊肯定有什麼東西比右邊大 別著急,在左偏樹上有乙個叫距離的東西 個點的距離,被定義為它子樹中離他最近的外節點到這個節點的距離 這與樹的深度不同 其中我們定義乙個節點為外節點,當且僅當這個節點的左子樹和...
可並堆 左偏樹
題目描述 如題,一開始有n個小根堆,每個堆包含且僅包含乙個數。接下來需要支援兩種操作 操作1 1 x y 將第x個數和第y個數所在的小根堆合併 若第x或第y個數已經被刪除或第x和第y個數在用乙個堆內,則無視此操作 操作2 2 x 輸出第x個數所在的堆最小數,並將其刪除 若第x個數已經被刪除,則輸出 ...
可並堆 左偏樹 斜堆
經典的二叉堆已經可以在 o log n 的複雜度的情況下維護堆這樣的資料結構,也有d 堆可以維護成 o log d n 雖然pop操作的複雜度是 o d log d n 然而這兩種堆不能滿足 o log n 的合併操作,它們的經常是 o n log n 即每次將乙個堆中的堆頂拿出來放到另乙個堆裡。雖...