資料結構篇 優先順序佇列(堆)

2021-09-29 12:07:16 字數 3496 閱讀 5371

​ 優先順序佇列,也叫二叉堆、堆(不要和記憶體中的堆區搞混了,不是乙個東西,乙個是記憶體區域,乙個是資料結構)。

​ 堆的本質上是一種完全二叉樹,分為:

均以大根堆為例

​ 堆本質上是一顆完全二叉樹,使用陣列進行儲存,從\(a[1]\)開始儲存,這樣對於下標為\(k\)的結點\(a[k]\)來說,其左孩子的下標為\(2*k\),右孩子的下標為\(2*k+1\)。,且不論 \(k\) 是奇數還是偶數,其父親結點(如果有的話)就是 $\left \lfloor k/2 \right \rfloor $。

​ 假如我們向乙個堆中插入乙個元素,要使其仍然保持堆的結構。應該怎麼辦呢?

​ 可以把想要新增的元素放在陣列的最後,也就是完全二叉樹的最後乙個結點的後面,然後進行向上調整(heapinsert)。向上調整總是把欲調整結點與父親結點比較,如果權值比父親結點大,那麼就交換其與父親結點,反覆比較,直到到達堆頂或父親結點的值較大為止。向上調整示意圖如下:

**如下,時間複雜度為\(o(logn)\):

void heapinsert(int* arr, int n) 

}

這樣新增元素就很簡單了

void insert(int* arr, int n, int x)
​ 假如我們要刪除乙個堆中的堆頂元素,要使其仍然保持堆的結構。應該怎麼辦呢?

​ 移除堆頂元素後,將最後乙個元素移動到堆頂,然後對這個元素進行向下調整(heapify),向下調整總是把欲調整結點 \(k\) 與其左右孩子結點比較,如果孩子中存在權值比當前結點 \(k\) 大的,那麼就將其中權值最大的那個孩子結點與結點 \(k\),反覆比較,直到到結點 \(k\) 為葉子結點或結點 \(k\) 的值比孩子結點都大為止。向下調整示意圖如下:

**如下,時間複雜度也是\(o(logn)\):

void heapify(int* arr, int k, int n) 

}

這樣刪除堆頂元素也就變得很簡單了

void deletetop(int* arr, int n)
自頂向下建堆的思想是,從第 \(i=1\) 個元素開始,對其進行向上調整,始終使前 \(i\) 個元素保持堆的結構。時間複雜度 \(o(nlogn)\)

void arraytoheap(int *a,int n) 

}

自底向上建堆的思想是,從底 $i=\left \lfloor n/2 \right \rfloor $ 個元素開始,對其進行向下調整,始終讓後 \(n-i\) 個元素保持堆的結構。

void arraytobheap(int *a, int n) 

}

​ 如果僅從**上直觀觀察,會得出構造二叉堆的時間複雜度為\(o(nlogn)\)的結果。當然這個上界是正確的,但卻不是漸近界,可以觀察到,不同結點在執行heapify的時間與該結點的樹高(樹高是指該結點到最底層葉子結點的值,不要和深度搞混了)相關,而且大部分結點的高度都很小。利用以下性質可以得到乙個更準確的漸近界:

【可以畫顆樹試一下,具體證明請看演算法導論】

在乙個高度為 \(h\) 的結點上執行heapify的代價為 \(o(h)\),我們可以將自頂向下建堆的總複雜度表示為

\[ \sum ^_ \lceil \frac } \rceil o(h)= o(n\sum ^ _\frac }) \]

這個式子

\[ \sum ^ _\frac } \]

其實就是求前 \(n\) 項和,高中數學的知識

\[ t(k)=\frac+\frac+\frac+\cdots+\frac\\\fract(k)=\frac+\frac+\frac+\cdots+\frac+\frac\\t(k)-\fract(k)=\frac+\frac+\frac+\cdots+\frac-\frac}\\\fract(k)=\frac(1-(\frac)^k)}}-\frac}\\t(k)=2-\frac}-\frac} \]

到這兒就需要求極限,高等數學的知識 \(\frac}\) 當 \(k\) 趨於無窮大時極限是 \(0\),對 \(\frac}\) 用洛必達法則極限也是 \(0\)

也就是說當 \(h\) 趨向於無窮大時,\(o(n\sum ^ _\frac })=o(n\cdot 2)\) ,去掉常數項,所以自底向上建堆複雜度為 \(o(n)\)

堆排序的思想:假設乙個大根堆有 \(n\) 個元素,每次把第 \(1\) 個元素,與第 \(n\) 個元素交換,對第乙個元素進行向下調整(heapify),並使得 \(n=n-1\) ,直到 \(n=1\)

void heapsort(int* arr, int n) 

for (int i = 50; i > 1; i--)

}

首先用陣列的前k個元素構建乙個小根堆,然後遍歷剩餘陣列和堆頂比較,如果當前元素大於堆頂,則把當前元素放在堆頂位置,並調整堆(heapify)。遍歷結束後,堆頂就是陣列的最大k個元素中的最小值,也就是第k大元素

void heapify(int* a, int index, int length) 

}void arraytobheap(int* a, int length)

}void findkmax(int* a, int k, int length)

}

時間複雜度\(o(n)\),只是舉個例子。

事實上對於這個問題是有更快的做法的,快速排序的思想,時間複雜度 \(o(logn)\)

int search_k(int left, int right, int k) 

swap(a[i], a[left]);

if (i - left + 1 == k)return a[i];

if (i - left + 1 < k)return search_k(i + 1, right, k - (i - left + 1));

else return search_k(left, i - 1, k);

}

堆更多時候,因為它建堆\(o(n)\),調整\(o(logn)\),當需要有序得到某些資料,是要優於排序(\(o(nlogn)\))演算法的,而且如果資料規模是動態增加的,那堆就要完全優於排序演算法了,在c++的stl是有堆的實現的,叫做priority_queue

資料結構 優先順序佇列(堆)

一 堆 0 預備知識 使用陣列儲存二叉樹結構,方式即將二叉樹用層序遍歷方式放入陣列中。這種方式的主要用法就是堆的表示。在陣列中 左孩子 left 下標 2 parent 1 右孩子 right 下標 2 parent 2 雙親 parent 下標 child 1 2 1 堆的概念 堆在邏輯上是乙個完...

資料結構 優先順序佇列

設初始序列為 49,38,65,97,76 大根堆 父節點的值大於或等於子節點的值 令數值越大優先順序越高 此時堆頂的元素為所有元素的最大值 97 小根堆 父節點的值小於或等於子節點的值 令數值越小優先順序越高 此時堆頂的元素為所有元素的最小值 38 將佇列中的所有元素按從大到小的數值輸出 分析 因...

資料結構 優先順序佇列

優先佇列的底層實現 二叉堆實現優先順序佇列 練習優先順序佇列也屬於佇列,因此也提供以下介面 利用二叉堆作為優先佇列的底層實現 可以通過comparator或comparable去自定義優先順序高低 利用二叉堆實現優先順序佇列。二叉堆實現優先順序佇列 author yusael public clas...