二叉堆
堆有序定義:當一顆二叉樹的每個節點都大于等于它的兩個子節點時, 被稱為堆有序。
二叉堆定義: 二叉堆是一組能夠用堆有序的完全二叉樹排序的元素,并在數組中按照層級儲存(不使用數組中第一個位置)。
在一個二叉堆中,位置k的節點的父節點位置為|k/2|(k/2向下取整),兩個子節點的位置分別為:2k、2k+1。
下文中二叉堆簡稱為堆。
堆的有序化定義:堆的操作會首先進行一些改動,打破堆的狀態,然后再遍歷堆并按照要求將堆的狀態恢復。
在有序化的過程中, 會遇到兩種情況:
1、 當某個節點的優先級上升(或者堆底加入新元素),需要由下至上恢復堆的順序。 算法實現如下:
func swim(k int) {
for k>1&&less(k/2, k) {
exch(k/2, k)
k /= 2
}
}
2、 當某個節點的優先級下降(例如, 將根節點替換為一個較小的元素)時, 需要由上至下恢復堆的順序。
算法實現如下:
func sink(k int) {
for 2*k <= N { //N為二叉堆的元素數量
j := 2*k
if 2*k<N && less(j, j+1) {
j += 1
}
if less(j, k) {
break
}
exch(k, j)
k = j
}
}
算法使用的exch、less方法:
func less(source []int, i, j int) {
return source[i] < source[j]
}
func exch(source []int, i, j int) {
source[i], source[j] = source[j], source[i]
return
}
優先隊列
優先隊列使用場景很多, 例如:處理很多程序中優先級最高的程序;收集一些數據,處理當前最大/最小的元素,再收集一些數據,再處理當前最大/最小的數據等等。
這些使用場景可以抽象為從N個輸入元素中找到最大/最小的M個元素。可以看一下下面表格中一些方法實現這種需求需要的資源,特別是在N非常大的時候,如何利用有限的資源高效的實現是比較大的挑戰。
示例 | 時間 | 空間 |
---|---|---|
排序算法 | NlogN | N |
初級實現(數組/鏈表)的優先隊列 | NM | M |
堆實現的優先隊列 | NlogM | M |
優先隊列也是很多重要算法的基礎, 例如一些重要的圖搜索算法、數據壓縮算法等。
優先隊列兩個最重要的操作是:刪除最大(小)元素和插入元素。使用上跟隊列、棧使用比較相似,在實現上, 和棧、隊列最大的不同是: 對于性能上的要求。隊列和棧的實現能夠在常數時間內完成所有的操作,而對于優先隊列,初級實現(有序/無序的數組/鏈表)在刪除元素和插入元素之一操作的最壞情況下需要線性時間完成,而基于二叉堆的實現能夠保證這兩種操作更快(對數級別)。
基于二叉堆實現的優先隊列:
1、 插入操作:
將新元素插入到數組的尾部,增加堆的大小并讓這個元素swim(對應上面二叉堆的優先級上升操作)到相應的位置。
2、 刪除操作:
從數組頂端刪除最大的元素,并將數組中最后一個元素放置到頂端,減小堆的大小, 并讓這個元素sink(對應上面二叉堆的優先級下降操作)到響應的位置
具體實現代碼:
type MaxPQ struct {
source []int
s int
}
func NewMaxPQ(k int) *MaxPQ {
return &MaxPQ{
source: make([]int, k+1),
s: 0,
}
}
func (q *MaxPQ) insert(key int) { //插入操作
q.s++
q.source[q.s] = key
q.swim(q.s)
}
func (q *MaxPQ) delMax() (x int) { //刪除操作
x = q.source[1]
q.exch(1, q.s)
q.s--
q.sink(1)
return
}
func (q *MaxPQ) swim(k int) {
for k>1&&q.less(k/2, k) {
q.exch(k/2, k)
k /= 2
}
}
func (q *MaxPQ) sink(k int) {
for 2*k <= q.s {
j := 2*k
if j<q.s&&q.less(j, j+1) {
j += 1
}
if !q.less(k, j) {
break
}
q.exch(k, j)
k = j
}
}
func (q *MaxPQ) isEmpty() bool {
return q.s == 0
}
func (q *MaxPQ) size() int {
return q.s
}
func (q *MaxPQ) less(i, j int) bool {
return q.source[i] < q.source[j]
}
func (q *MaxPQ) exch(i, j int) {
q.source[i], q.source[j] = q.source[j], q.source[i]
}
func (q *MaxPQ) show() {
var (
i = 0
)
for i<=q.s {
fmt.Fprintf(os.Stdout, "%v ", q.source[i])
i++
}
fmt.Println()
}
索引優先隊列
索引優先隊列在多向歸并的使用場景中非常有效, 例如:從多個有序的輸入流歸并成一個有序序列,如果有足夠的空間,你可以簡單的把它們讀入一個數組并排序,但如果使用了優先隊列,無論輸入有多長你都可以把他們全部讀入并排序。
索引優先隊列的實現如下:
type IndexMinPQ struct {
keys []int //元素存放的數組
pq []int //二叉堆實現的索引
qp []int //索引二叉堆的逆序, 可以很方便的做contains判斷
N int //PQ中元素的數量
}
func NewIndexMinPQ(max int) (imq *IndexMinPQ) {
imq = &IndexMinPQ{
keys: make([]int, max+1),
pq: make([]int, max+1),
qp: make([]int, max+1),
}
for i:=0; i<=max; i++ {
imq.qp[i] = -1 //初始化為-1
}
return
}
func (imq *IndexMinPQ) insert(k int, key int) { //插入操作
imq.N++
imq.pq[imq.N] = k
imq.keys[k] = key
imq.qp[k] = imq.N
imq.swim(imq.N)
}
func (imq *IndexMinPQ) delMin() (indexOfMin int) { //刪除操作
indexOfMin = imq.keys[imq.pq[1]]
imq.exch(1, imq.N)
imq.N--
imq.sink(1)
imq.keys[imq.pq[imq.N+1]] = -1
imq.qp[imq.pq[imq.N+1]] = -1
return
}
func (imq *IndexMinPQ) swim(k int) {
for k>1 && (!imq.less(k/2, k)) {
imq.exch(k/2, k)
k /= 2
}
}
func (imq *IndexMinPQ) sink(k int) {
for 2*k <= imq.N {
j := 2*k
if 2*k<imq.N && imq.less(j+1, j) {
j += 1
}
if imq.less(k, j) {
break
}
imq.exch(k, j)
k = j
}
}
func (imq *IndexMinPQ) less(i, j int) bool {
return imq.keys[imq.pq[i]] < imq.keys[imq.pq[j]]
}
func (imq *IndexMinPQ) exch(i, j int) {
imq.pq[i], imq.pq[j] = imq.pq[j], imq.pq[i]
imq.qp[imq.pq[i]], imq.qp[imq.pq[j]] = imq.qp[imq.pq[j]], imq.qp[imq.pq[i]]
}
func (imq *IndexMinPQ) show() {
for i:=1; i<=imq.N; i++ {
fmt.Printf("%v ", imq.keys[imq.pq[i]])
}
fmt.Println()
}
func (imq *IndexMinPQ) min() int {
return imq.keys[imq.pq[1]]
}
func (imq *IndexMinPQ) contains(k int) bool {
return imq.qp[k] != -1
}
堆排序
有了上面優先隊列的例子, 再看堆排序就很簡單了。
堆排序算法分為兩階段:在堆的構造階段,將原始數組重新組織到一個堆中,然后在下沉階段,從堆中按遞減順序取出所有元素并得到排序結果。
在堆的構造階段,可以從左到右遍歷數組,用swim()保證掃描到的位置左側都是一顆堆有序的完全樹,但更高效的方法是:從右至左用sink()函數構造子堆,開始時我們只需要掃描一半的元素,因為我們可以跳過大小為1的子堆。
func heapSort(source []int) {
var (
l = len(source)-1
j = l
)
for i:=l/2; i>=1; i-- { //構造堆階段,只需要掃描一半的元素
sink(source, i, l)
}
for j>1 { //下沉階段
exch(source, 1, j)
j--
sink(source, 1, j)
}
}
func sink(source []int, i, length int) {
for 2*i<=length {
x := 2*i
if x<length&&less(source, x, x+1) {
x += 1
}
if !less(source, i, x) {
break
}
exch(source, i, x)
i *= 2
}
}
less函數和exch函數參照上文。
多叉堆
略
參考資料: 《算法》第四版