1. 程式人生 > >演算法設計與分析——二叉堆(一)

演算法設計與分析——二叉堆(一)

本blog主要介紹了二叉堆、二項式堆,下一篇部落格將介紹斐波拉契堆。

二叉堆和二項式堆、斐波拉契堆都是用於實現優先佇列的高階資料結構,以不同堆實現的優先佇列會有不同的時間複雜度。

問題引入

在實際應用中,我們經常會遇到在最多由n個數組成的動態集合SS上得到這個集合裡面的最大值或者最小值。這裡的動態是指:集合S裡面的元素可能會隨時增加、刪除、修改、返回最小值、返回最小值並刪除一個最小值。

我們把用於解決此類的問題的抽象資料結構定義為優先佇列:priority queue.之所以叫優先佇列是指裡面的元素都是具有偏序關係的、也就是可以比較大小的。 priority_queue有以下幾種基本操作:

  • insert(H,x) 插入一個值域為x的元素
  • makeheap()建立一個新的堆H
  • extractmin(H)返回優先佇列H的最小值,同時將這個最小值從優先佇列中刪除。
  • decreasekey(H,x,k)把H中的某個值域為x的元素的值改成k
  • union(H1,H2):把H1和H2中的所有元素提取出來形成一個新的優先佇列。

優先佇列的基礎資料結構可以是連結串列、二叉堆、二項式堆、斐波拉契堆, 基於不同的基礎資料結構,優先佇列的各個操作的時間複雜度的關係是: 不同優先佇列的時間複雜度

可以看到基於連結串列的優先佇列的效能最差,二叉堆、二項式堆、斐波拉契堆的效能依次優化。

基於連結串列的優先佇列

我們可以使用連結串列或者陣列這種資料結構來實現優先佇列,我們假設選擇的是陣列。

假設數組裡面的元素是有序的。 這種情況下,makeheap就是隻要申請一個數組即可,時間複雜度為O(1) insert 需要將元素插入合適的位置,選擇合適的位置使用二分查詢需要O(logn)O(logn)時間,但是合適位置只有的元素都要向後移,時間複雜度為O(logn+n)=O(n)O(logn + n)=O(n) extractmin取最小元素需要O(1),刪除這個最小元素需要把後面的元素往前移動一個位置。 decreasekey修改某個元素的值。O(1) delete刪除需要O(n) union也是需要O(n) findmin只需要O(1)

假設數組裡面的元素是無序的。 這種情況下,makeheap

就是隻要申請一個數組即可,時間複雜度為O(1) insert 需要將元素插入到末尾,O(1) extractmin取最小元素需要遍歷整個陣列,並將這個元素刪除,需要O(n) decreasekey修改某個元素的值。O(1) delete刪除需要O(n) union也是需要O(n) findmin只需要O(n)

因此使用陣列的話,無論是否對陣列進行排序,所需要效能都不是很好。

這是因為,我們讓這個陣列全部元素都有序實在太嚴格,甚至是有些浪費了。我們只是想知道最小的元素,但是是在是沒有必要讓整個陣列有序。如果有某種方式,使得我們不用所有元素都有序也得得到當前最小值,那就好啦。

而這個就是二叉堆的核心思想:我們用樹來儲存所有的元素,然後我們讓樹的根比它的子節點的值都小就好啦。

基於二叉堆的優先佇列

二叉堆是一個完全的二叉樹,這種樹裡面的各個節點有指標域和值域。指標域放著指向其父節點、左右孩子的指標。值域放著資料物件T ,必須定義這些物件的偏序關係。即 必須使得二元關係<(T1,T2)=T1<T2<(T_{1},T_{2}) =T_{1} < T_{2}是有定義的。二叉堆對發在各個節點的資料物件有一個很弱的要求:父節點的資料物件必須小於等於(或者大於等於)其子節點的資料物件。若一個堆的父節點的資料物件小於等於(或者大於等於)其子節點的資料物件,這就是一個小頂堆。如果父節點的資料物件大於等於其子節點的資料物件,那麼這就是一個大頂堆。 大小頂堆的原理一樣,以下只介紹小頂堆。

以下是一些小頂堆: 在這裡插入圖片描述

現在,我們使用一個二叉堆來實現優先佇列。 既然要實現優先佇列,那麼就需要實現以下函式:

  • MakeHeap_VOID() 建立一個空的堆
  • MakeHeap([x1,x2,…]) 給定一組資料物件xi,基於這些元素建立堆
  • INSERT(x) 把資料物件插入到堆裡面
  • FINDMIN() 返回堆的最小值
  • EXTRACTMIN() 返回最小值,並把堆頂的元素刪除
  • UNION() 把兩個堆的元素合併起來形成一個新的堆。

OK,我們一個個來看看這些函式如何實現。

INSERT(x)函式

對於一個新的元素x,我們要把這個x插入到堆裡面。那麼我們的做法只需要 1.把x插到堆的末尾。注意一定要是末尾,如果不是末尾,這個堆對應的樹就不是滿二叉樹了。 2.從剛剛插入的這個節點開始,從葉子節點往根節點更新。如果這個插入的值比父節點還小,那麼這個剛剛插入的值就是以其父節點為根的子樹的最小值,此時需要把這個節點的值與父節點的值交換。一直往上找,直到父節點比這個新插入的元素小位置或者到達根節點。 時間複雜度:最壞的情況下,這個新元素就是所有元素中的最小值。此時,從葉子到根遍歷了logn個節點。 所以複雜度是O(logn)

FINDMIN()返回最小值

堆的根節點的元素就是當前最小值。

EXRACTMIN()返回最小值,同時將最小值刪除

得到最小值就只需要當前的堆頂,先把這個堆頂的值儲存。 然後將堆頂刪除掉。 刪除的方法是: 把堆的最後一個元素的值放到堆頂,刪除最後一個元素。然後從堆頂開始,從上往下的維護堆。 時間複雜度:O(logn)

DECREASEKEY(ptr,value)把指標ptr指向的節點的值改成value.

這個問題分兩種情況來看。 1.把ptr對應的節點的資料改小。 這種情況下,ptr對應的節點依然會小於其子節點。所以無需向下維護。 但是ptr節點的新值有可能小於父節點的值,所以需要從ptr向上的維護堆序。 例如: 在這裡插入圖片描述 2.把ptr對應的節點的資料改大。 這種情況下,父節點依然大於這個節點,於是該節點向上的部分可以不用去考慮。 但是該節點被改大以後,就不一定比其子節點都小。此時需要從這個節點開始遞迴的向下維護。單次維護的方式很簡單,就是與其最小的那個子節點交換。然後交換後,從剛剛交換的子節點遞迴的向下維護。 例如:

MakeHeap_LIST()從一堆資料中建堆

從給定的一批資料建立起堆。 有兩種方法。 1.對於每一個元素,呼叫insert函式,將這個元素插入。 時間複雜度O(nlogn),這是因為一共有n個元素待插入,每次插入需要logn複雜度。 所以總的時間是: S1=log1+log2+log3++logn<=nlognS_{1}=log1+log2+log3+\dots+logn<=nlogn 2.先把所有的元素都拷貝到這個堆裡面。當時這個堆肯定是不滿足要求的,這個時候,我們從最後一個非葉子的子樹開始,從後往前一個個小子樹的維護堆序,每個小子樹是從上往下的維護。 此時的複雜度為O(n) 這是因為: 對於第k層節點來說,一共有2k2^{k}個節點,每個節點為根的子樹從上向下維護最多需要logn-k步。 所以總的時間複雜度為: S2=k=lognk=02k(lognk)=O(n)S_{2}=\sum^{k=0}_{k=logn} 2^{k}*(logn-k)=O(n) 這個式子是怎麼推的呢? 記M=lognM=logn,那麼S2S_{2}可以寫成: S2=k=0M2k(Mk)S_{2}=\sum^{M}_{k=0}2^{k}(M-k) S2=M(k=0M2k)k=0Mk×2kS_{2}=M(\sum^{M}_{k=0}2^k)-\sum^{M}_{k=0}k\times2^k 前一部分是個等比數列求和,後一個是一個錯位相減法。 記S3=k=0Mk×2kS_{3}=\sum^{M}_{k=0}k\times2^k 那麼: S3=020+121+222++M2MS_{3}=0*2^{0}+1*2^{1}+2*2^{2}+\dots+M*2^{M} 兩邊同乘以2 2S3=021+122++(M1)2M+M2M+12S_{3}=0*2^{1}+1*2^{2}+\dots+(M-1)*2^{M}+M*2^{M+1} S32S3=S3=0+(k=0M2k)M2M+1S_{3}-2S_{3}=-S_{3}=0+(\sum^{M}_{k=0}2^{k})-M*2^{M+1}

於是有:S2=M(k=0M2k)k=0Mk×2k=M(k=0M2k)S3=M(k=0M2k)+((k=0M2k)M2M+1)S_{2}=M(\sum^{M}_{k=0}2^k)-\sum^{M}_{k=0}k\times2^k=M(\sum^{M}_{k=0}2^k)-S_{3}=M(\sum^{M}_{k=0}2^k)+((\sum^{M}_{k=0}2^{k})-M*2^{M+1}) 容易得到(k=0M2k)=2M+11(\sum^{M}_{k=0}2^{k})=2^{M+1}-1 所以: S2=M2M+1M+2M+11M2M+1=2M+1M1S_{2}=M*2^{M+1}-M+2^{M+1}-1-M*2^{M+1}=2^{M+1}-M-1M=lognM=logn

所以 S2=2M+1M1=nlogn1=O(n)S_{2}=2^{M+1}-M-1=n-logn-1=O(n)

UNION()將兩個二叉堆合併

實際中是由將兩個堆合併的需求的。在二叉堆中,如果要將兩個堆合併,其方法是: 1.將兩個堆的元素全部合併,然後對這些元素呼叫MAKEHEAP_LIST方法,在O(n)的時間內合併 2.遍歷其中一個堆的每個元素,將呼叫INSERT函式把這些元素插入到另外一個堆裡面,需要O(nlogn)的時間。

可以看到:二叉堆對於合併操作的支援為O(n),這個是很慢的!所以,人們提出了二項式堆來加速這個合併操作。

基於二叉堆的優先佇列實現

以下是一個基於二叉堆的優先佇列的實現

#pragma once
#include <vector>
#include <algorithm>
using namespace std;
template <typename T>
class binaryHeap 
	//基於陣列,建立一個二項堆
{
public:
	binaryHeap()
	{
		MakeHeap();
	}
	void MakeHeap()
	{
		min_heap.clear();
		min_heap.push_back(T());//min_heap[0]是無效的
		theLast = 1;
	
            
           

相關推薦

演算法設計分析——

本blog主要介紹了二叉堆、二項式堆,下一篇部落格將介紹斐波拉契堆。 二叉堆和二項式堆、斐波拉契堆都是用於實現優先佇列的高階資料結構,以不同堆實現的優先佇列會有不同的時間複雜度。 問題引入 在實際應用中,我們經常會遇到在最多由n個數組成的動態集合SSS上得到這個

演算法設計分析第四周練習圖論

Network Delay Time 1. 題目 There are N network nodes, labelled 1 to N. Given times, a list of travel times as directed edges times[i

演算法設計分析:Burst BalloonsWeek 6

學號:16340008 Question: Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums.

演算法設計分析:Scramble StringWeek 8

學號:16340008 Question: Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrings recursive

演算法設計分析()之動態規劃

第一篇:動態規劃(Dynamic Programming)求解過程是多階段決策過程

南大算法設計分析課程OJ答案2

sam long bmi 窮舉 bbbb body 算法設計 分配 info 問題 A: 最大子序列和問題 時間限制: 1 Sec 內存限制: 4 MB提交: 184 解決: 66提交 狀態 算法問答 題目描述 給定一整數序列 a1, a2, …, an,

南大算法設計分析課程OJ答案3

完美 語言 偶數 使用 課程 nbsp problems AS btn 問題 A: 動態中位數問題 時間限制: 1 Sec 內存限制: 8 MB提交: 866 解決: 102提交 狀態 算法問答 題目描述 輸入一組整數a1, a2, …, an ,每輸入一

演算法設計分析作業題】第十週:20. Valid Parentheses

題目 C++ solution class Solution { public: bool isValid(string s) { stack<char> cstack; for (int i = 0; i < s.si

【面試演算法】——

一、二叉樹問題概述 二叉樹型別的題目為常考題型 原因: 能夠結合佇列、棧、連結串列、字串等多資料結構 需要掌握圖的基本遍歷方法,比如BFS和DFS 需要掌握遞迴函式的使用,並自己設計出遞迴過程 二叉樹問題與實際工作結合緊密   二、二叉樹先序

數據結構之

reorder system style 序列 urn creat 編寫程序 space ont 設計和編寫程序,按照輸入的遍歷要求(即先序、中序和後序)完成對二叉樹的遍歷,並輸出相應遍歷條件下的樹結點序列。 1 //遞歸實現 2 #include

二叉樹的基本概念 什麼是二叉樹 一棵二叉樹是結點的一個有限集合,該集合或者為空,或者是由一個根節點加上兩棵分別稱為左子樹和右子樹的二叉樹組成 特點 每個結點最多有兩棵子樹,即二叉樹不存在度大於2的結點 二叉樹的子樹有左右之分,其子樹的次序不能顛倒 兩種特殊

C++資料結構:——先序建立

一、二叉樹 (Binary Tree) 定義: 二叉樹是n個節點的有限集合,該集合或者為空集( 稱為空二叉樹 ),或者由一個根節點和兩棵互不相交的的二叉樹組成,這兩棵二叉樹分別稱為根節點的左子樹和右

演算法設計分析

53.Question Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum a

演算法設計分析:Divide And Conquer

Maximum Subarray Given an integer array nums, find the contiguous subarray (containing at least one number) which has the larges

0x05演算法設計分析複習演算法設計策略-分治法2

參考書籍:演算法設計與分析——C++語言描述(第二版) 演算法設計策略-分治法 二分搜尋 問題描述 在有序表(已按關鍵字值非減排序)中搜索給定元素的問題。 分治法求解 設有一個長度為n的有序表(a0,a1,⋯,an−1),要求

java資料結構演算法之樹基本概念及BinaryTree設計實現

關聯文章:   樹博文總算趕上這周釋出了,上篇我們聊完了遞迴,到現在相隔算挺久了,因為樹的內容確實不少,博主寫起來也比較費時費腦,一篇也無法涵蓋樹所有內容,所以後續還會用2篇左右的博文來分析其他內容大家就持續關注吧,而本篇主要了解的知識點如下(還是蠻多

演算法設計分析——動態規劃矩陣連乘

動態規劃——Dynamic programming,可以說是本人一直沒有啃下的骨頭,這次我就得好好來學學Dynamic programming. OK,出發! 動態規劃通常是分治演算法的一種特殊情況,它一般用於最優化問題,如果這些問題能夠: 1.能夠分解為規模更小的子問題 2.遞迴的

演算法設計分析》第十週作業

《演算法設計與分析》第十二週作業 標籤(空格分隔): 課堂作業 文章目錄 《演算法設計與分析》第十二週作業 @[toc] 題目概要 思路 具體實現 心得 原始碼:

遞迴recursion演算法1

筆者按:曾經剛開始學習資料結構和演算法時,總會為簡潔雋永的遞迴程式碼而驚歎,也想寫出如此優雅的程式碼,但是思考過程真的實屬不易!!!那時候遞迴都會盡量用顯式棧來規避。 生活中的遞迴!        首先,對遞迴要有一個類似盜夢空間或者平行世界的認識,就

演算法設計分析

53.Course Schedule There are a total of n courses you have to take, labeled from 0 to n-1. Some courses may have prerequisites, for