1. 程式人生 > >資料結構--堆的實現之深入分析

資料結構--堆的實現之深入分析

一,介紹

以前在學習堆時,寫了兩篇文章:資料結構--堆的實現(上)   和   資料結構--堆的實現(下),  感覺對堆的認識還是不夠。本文主要分析資料結構 堆(討論小頂堆)的基本操作的一些細節,比如 insert(插入)操作 和 deleteMin(刪除堆頂元素)操作的實現細節、分析建堆的時間複雜度、堆的優缺點及二叉堆的不足。

二,堆的實現分析

堆的物理儲存結構是一維陣列,邏輯儲存結構是完全二叉樹。堆的基本操作有:insert--向堆中插入一個元素;deleteMin--刪除堆頂元素

故堆的類架構如下:

public class BinaryHeap<T extends
Comparable<? super T>> { private T[] array; private int currentSize; public BinaryHeap() { } public BinaryHeap(T[] array){ } public void insert(T x){ //do something } public T deleteMin(){ }
//other operations.... }

①insert操作

1     public void insert(T x){
2         if(currentSize == array.length - 1)//陣列0號位置作為哨兵
3             enlarge(array.length * 2 + 1);0
4         
5         int hole = currentSize++;
6         for(array[0] = x; x.compareTo(array[hole / 2]) < 0; hole /= 2)
7             array[hole] = array[hole / 2];//
將父節點往下移 8 array[hole] = x;//將待插入的元素放置到合適位置 9 }

1)陣列0號元素作為哨兵,可以避免交換操作。

因為,在與父節點的比較過程中,若父節點比待插入的節點大(子節點),則需要交換父節點和待插入節點。而引入哨兵,將待插入節點儲存在陣列0號元素處,當父節點比待插入的節點大時,直接用父節點替換待插入的節點大(子節點)。

2)複雜度分析

可以看出,最壞情況下,比較進行到根節點時會結束。因此,insert操作時間取決於樹的高度。故複雜度為O(logN)。但是在平均情況下,insert操作只需要O(1)時間就能完成,因為畢竟並不是所有的節點都會被排程至根結點,只有在待插入的節點的權值最小時才會向上調整堆頂。

此外,對於二叉樹,求解父節點操作: hole = hole / 2, 除以2可以使用右移一位來實現。

因此,可以看出d叉樹(完成二叉樹 d=2 ),當 d 很大時,樹的高度就很小,插入的效能會有一定的提高。為什麼說是一定的??後面會詳細分析。

②deleteMin操作

deleteMin操作將堆中最後一個元素替換第一個元素,然後在第一個元素處向下進行堆調整。

 1     public AnyType deleteMin( )
 2     {
 3         if( isEmpty( ) )
 4             throw new UnderflowException( );
 5 
 6         AnyType minItem = findMin( );
 7         array[ 1 ] = array[ currentSize-- ];//最後一個元素替換堆頂元素
 8         percolateDown( 1 );//向下執行堆調整
 9 
10         return minItem;
11     }
 1     /**
 2      * Internal method to percolate down in the heap.
 3      * @param hole the index at which the percolate begins.
 4      */
 5     private void percolateDown( int hole )
 6     {
 7         int child;
 8         AnyType tmp = array[ hole ];
 9 
10         for( ; hole * 2 <= currentSize; hole = child )
11         {
12             child = hole * 2;
13             if( child != currentSize &&
14                     array[ child + 1 ].compareTo( array[ child ] ) < 0 )
15                 child++;
16             if( array[ child ].compareTo( tmp ) < 0 )
17                 array[ hole ] = array[ child ];
18             else
19                 break;
20         }
21         array[ hole ] = tmp;
22     }

當從第一個元素(堆頂元素)處向下進行堆調整時,一般該元素會被調整至葉子結點。堆頂元素的高度為樹的高度。故時間複雜度為:O(logN)。

③其他一些操作

1)decreaseKey(p,Δ)/increaseKey(p,Δ)---更改位置p處元素的權值

這兩個操作一般不常用。它們會破壞堆的性質。因此,當修改了p處元素的權值時,需要進行堆調整(decreseKey為向上調整,increaseKey為向下調整)

2)delete(p)--刪除堆中位置為p處的元素

前面介紹的deleteMin操作刪除的是堆頂元素,那如何刪除堆中的任一 一個元素?

其實,可以將刪除堆中任一 一個元素(該元素位置為 p)轉換成刪除堆頂元素。

藉助 1)中的修改位置p處元素的權值操作:decrese(p,Δ)。將p處元素的權值降為負無窮大。此時,該元素會向上調整至堆頂,然後執行deleteMin即可。

三,建堆(buildHeap)

從最後一個非葉子結點開始向前進行向下調整。

1     /**
2      * Establish heap order property from an arbitrary
3      * arrangement of items. Runs in linear time.
4      */
5     private void buildHeap( )
6     {
7         for( int i = currentSize / 2; i > 0; i-- )
8             percolateDown( i );
9     }

i 的初始值為最後一個非葉子結點的位置。

 時間複雜度分析:

建堆的時間複雜度與堆中所有的結點的高度相同。

 分析如下:首先,葉子結點的高度為0。而建堆,就是從最後一個非葉子結點開始,不斷呼叫percolateDown(i),percolateDown(i)方法的時間複雜度就是位置 i 處節點的高度。在上面第7行for迴圈中,當 i 自減為1時,表明已經到了堆頂元素,因此整個buildHeap的時間複雜度就是所有非葉子結點的高度之和。而葉子結點的高度為0,故buildHeap的時間複雜度可理解成 整個二叉堆的所有的結點的高度之和。

而對於理想二叉堆而言:(二叉堆是一顆完全二叉樹,理想二叉堆為滿二叉樹)

所有結點的高度之為:2^(h+1)-1-(h+1)。其中,h表示二叉堆的高度

又可以表示成:N-b(N),N是堆中結點的個數,b(N)是N的二進位制表示法中1的個數,如:b(7)=3

四,d 堆

上面分析了二叉堆的基本操作。那什麼是 d 堆呢?為什麼要有 d 堆呢?

對於二叉堆,d=2。顧名思義,d堆就是所有節點都有d個兒子的堆。為什麼需要這種堆?

分析二叉堆的基本操作,insert操作需要定位父結點,這需要一個除法操作,操作的次數與樹的高度有關。deleteMin操作需要找出所有兒子中權值最小的那個兒子,而尋找兒子節點則需要乘法操作,操作的複雜度與兒子的個數有關(d越大,節點的兒子數越多,查詢越慢)。

假設,我們的需求是有大量的insert操作,而僅有少量的deleteMin,那d堆從理論上講就有效能優勢了。因為d 遠大於2時,樹的高度很小啊,但是當d不是2的倍數時,除法操作不能通過移位來實現,也許會有一定的效能損失,這也是為什麼insert操作分析中講的“插入效能會有一定的提高”。

而如果有大量的deleteMin操作,那d堆反而可能會除低效能,因為:d 越大,說明節點的兒子個數越多,找出權值最小的兒子就需要更多的比較次數了。

可見,d堆的提出,是因為需求不同而導致的。比如,insert屬於高頻需求.....

五,二叉堆的不足

根據上面的分析,二叉堆的insert複雜度O(logN),deleteMin最壞也是O(logN)。

但是如果需要查詢堆中某個元素呢?或者需要合併兩個堆呢?

對於二叉堆而言,對find 和 merge操作的支援不夠。這是由二叉堆的儲存結構決定的,因為二叉堆中的元素實際儲存在陣列中。正因為如此,所有支援有效合併的高階資料結構都需要使用鏈式資料結構。另外,關於資料結構的合併操作,可參考:資料結構--並查集的原理及實現

六,其他形式的“堆”

為了克服二叉堆的不足,提出了一面一些型別的堆,它們主要是為了支援merge 和 find 操作。這就不詳細介紹了。

①左式堆

對堆的結構有一定的要求:它有一個“零路徑長”的概念,①任意一個節點的零路徑長比它的各個兒子的零路徑長的最小值大1。②對於堆中每一個節點,它的左兒子的零路徑長至少與右兒子的零路徑長相等。

②斜堆

對堆的結構沒有要求。

③二項佇列

 最大的特點就是,做到了merge操作時間複雜度為O(logN),而insert操作的平均時間複雜度為O(1)。

參考的BinaryHeap的完整實現如下:

package c9.shortestPath;
// BinaryHeap class
//
// CONSTRUCTION: with optional capacity (that defaults to 100)
//               or an array containing initial items
//
// ******************PUBLIC OPERATIONS*********************
// void insert( x )       --> Insert x
// Comparable deleteMin( )--> Return and remove smallest item
// Comparable findMin( )  --> Return smallest item
// boolean isEmpty( )     --> Return true if empty; else false
// void makeEmpty( )      --> Remove all items
// ******************ERRORS********************************
// Throws RuntimeExceptionException as appropriate

/**
 * Implements a binary heap.
 * Note that all "matching" is based on the compareTo method.
 * @author Mark Allen Weiss
 */
public class BinaryHeap<AnyType extends Comparable<? super AnyType>>
{
    /**
     * Construct the binary heap.
     */
    public BinaryHeap( )
    {
        this( DEFAULT_CAPACITY );
    }

    /**
     * Construct the binary heap.
     * @param capacity the capacity of the binary heap.
     */
    public BinaryHeap( int capacity )
    {
        currentSize = 0;
        array = (AnyType[]) new Comparable[ capacity + 1 ];
    }
    
    /**
     * Construct the binary heap given an array of items.
     */
    public BinaryHeap( AnyType [ ] items )
    {
            currentSize = items.length;
            array = (AnyType[]) new Comparable[ ( currentSize + 2 ) * 11 / 10 ];

            int i = 1;
            for( AnyType item : items )
                array[ i++ ] = item;
            buildHeap( );
    }

    /**
     * Insert into the priority queue, maintaining heap order.
     * Duplicates are allowed.
     * @param x the item to insert.
     */
    public void insert( AnyType x )
    {
        if( currentSize == array.length - 1 )
            enlargeArray( array.length * 2 + 1 );

            // Percolate up
        int hole = ++currentSize;
        for( array[ 0 ] = x; x.compareTo( array[ hole / 2 ] ) < 0; hole /= 2 )
            array[ hole ] = array[ hole / 2 ];
        array[ hole ] = x;
    }


    private void enlargeArray( int newSize )
    {
            AnyType [] old = array;
            array = (AnyType []) new Comparable[ newSize ];
            for( int i = 0; i < old.length; i++ )
                array[ i ] = old[ i ];        
    }
    
    /**
     * Find the smallest item in the priority queue.
     * @return the smallest item, or throw an UnderflowException if empty.
     */
    public AnyType findMin( )
    {
        if( isEmpty( ) )
            throw new RuntimeException( );
        return array[ 1 ];
    }

    /**
     * Remove the smallest item from the priority queue.
     * @return the smallest item, or throw an UnderflowException if empty.
     */
    public AnyType deleteMin( )
    {
        if( isEmpty( ) )
            throw new RuntimeException( );

        AnyType minItem = findMin( );
        array[ 1 ] = array[ currentSize-- ];
        percolateDown( 1 );

        return minItem;
    }

    /**
     * Establish heap order property from an arbitrary
     * arrangement of items. Runs in linear time.
     */
    public void buildHeap( )
    {
        for( int i = currentSize / 2; i > 0; i-- )
            percolateDown( i );
    }

    /**
     * Test if the priority queue is logically empty.
     * @return true if empty, false otherwise.
     */
    public boolean isEmpty( )
    {
        return currentSize == 0;
    }

    /**
     * Make the priority queue logically empty.
     */
    public void makeEmpty( )
    {
        currentSize = 0;
    }

    private static final int DEFAULT_CAPACITY = 10;

    private int currentSize;      // Number of elements in heap
    private AnyType [ ] array; // The heap array

    /**
     * Internal method to percolate down in the heap.
     * @param hole the index at which the percolate begins.
     */
    private void percolateDown( int hole )
    {
        int child;
        AnyType tmp = array[ hole ];

        for( ; hole * 2 <= currentSize; hole = child )
        {
            child = hole * 2;
            if( child != currentSize &&
                    array[ child + 1 ].compareTo( array[ child ] ) < 0 )
                child++;
            if( array[ child ].compareTo( tmp ) < 0 )
                array[ hole ] = array[ child ];
            else
                break;
        }
        array[ hole ] = tmp;
    }

        // Test program
    public static void main( String [ ] args )
    {
        int numItems = 10000;
        BinaryHeap<Integer> h = new BinaryHeap<>( );
        int i = 37;

        for( i = 37; i != 0; i = ( i + 37 ) % numItems )
            h.insert( i );
        for( i = 1; i < numItems; i++ )
            if( h.deleteMin( ) != i )
                System.out.println( "Oops! " + i );
    }
}
View Code

參考資料

資料結構與演算法分析 Mark Allen Weiss著

相關推薦

的python實現及其應用 資料結構--實現深入分析

堆的概念 優先佇列(priority queue)是一種特殊的佇列,取出元素的順序是按照元素的優先權(關鍵字)大小,而不是進入佇列的順序,堆就是一種優先佇列的實現。堆一般是由陣列實現的,邏輯上堆可以被看做一個完全二叉樹(除底層元素外是完全充滿的,且底層元素是從左到右排列的)。 堆分為最大堆和最小堆,最大堆

資料結構--實現深入分析

一,介紹 以前在學習堆時,寫了兩篇文章:資料結構--堆的實現(上)   和   資料結構--堆的實現(下),  感覺對堆的認識還是不夠。本文主要分析資料結構 堆(討論小頂堆)的基本操作的一些細節,比如 insert(插入)操作 和 deleteMin(刪除堆頂元素)操作的實現細節、分析建堆的時間複雜度、堆的

資料結構(Python實現)連結串列

理解資料結構最好的方式就是用腦洞把它想象出來。 一、節點 class Node():     def __init__(self,data=None):         self.data=data    

資料結構(Python實現)佇列及棧

昨天被面試官問到,怎麼實現一個佇列?佇列原理上很簡單,就是先進先出。我之前用C寫過,卻不曾用Python寫過,但Python只會更簡單不會更復雜。我思考了1秒鐘,告訴面試官,可以利用列表,append方法即是入隊,取出列表的第0元素即是出隊。回家後,我立即動手寫了一遍。鑑於棧與佇列只存在些許不同,本

資料結構程式碼實現佇列的連結串列實現(C/C++)

上班閒著無聊,一直想著要開始寫部落格,但又不知道寫什麼。最近又回顧了下資料結構的知識,那就從資料結構開始吧。 前言 關於C語言結構體的知識以及佇列的特性請讀者自行了解,此處不做過多解釋,嘻嘻。 同時此篇文章僅僅是關於佇列的連結串列實現。 第一步:結構體編寫 我們首先分析一下佇列的

資料結構-實現優先佇列(java)

佇列的特點是先進先出。通常都把佇列比喻成排隊買東西,大家都很守秩序,先排隊的人就先買東西。但是優先佇列有所不同,它不遵循先進先出的規則,而是根據佇列中元素的優先權,優先權最大的先被取出。這就很像堆的特徵:總是移除優先順序最高的根節點。 重點:優先順序佇列,是要看優先順序的,

資料結構圖文解析:二叉詳解及C++模板實現

0. 資料結構圖文解析系列 1. 二叉堆的定義 二叉堆是一種特殊的堆,二叉堆是完全二叉樹或近似完全二叉樹。二叉堆滿足堆特性:父節點的鍵值總是保持固定的序關係於任何一個子節點的鍵值,且每個節點的左子樹和右子樹都是一個二叉堆。 當父節點的鍵值總是大於或等於任何一個子節點的鍵值時為最大堆。 當父節點的鍵值總是小於

java資料結構與演算法順序表與連結串列深入分析

關聯文章:   資料結構與演算法這門學科雖然在大學期間就已學習過了,但是到現在確實也忘了不少,因此最近又重新看了本書-《資料結構與演算法分析》加上之前看的《java資料結構》也算是對資料結構的進一步深入學習了,於是也就打算寫一系列的資料結構的博文以便加

資料結構與演算法美專欄學習筆記-複雜度分析

複雜度分析 什麼是複雜度分析 資料結構和演算法解決是“如何讓計算機更快時間、更省空間的解決問題”。 因此需從執行時間和佔用空間兩個維度來評估資料結構和演算法的效能。 分別用時間複雜度和空間複雜度兩個概念來描述效能問題,二者統稱為複雜度。 複雜度描述的是演算法執行時間(或佔用空間)與資料規模的增長關係

資料結構與演算法美》專欄閱讀筆記1——複雜度分析

蹭可愛的男朋友買的極客時間的專欄【資料結構與演算法之美】,作者讓大家定個學習的flag。o( ̄▽ ̄)o,好吧,最近喜歡做思維導圖(純粹因為好看!),所以flag就是每篇都要寫讀書筆記咯~ 文章目錄 1、如何抓住重點,系統

看得見的資料結構Android版表的陣列實現(資料結構篇)

零、前言: 一講到裝東西的容器,你可能習慣於使用ArrayList和陣列,你有想過ArrayList和陣列的區別嗎? Java的類起名字都不是隨便亂起的,一般前面是輔助,後面是實質:ArrayList = Array + List Array就是陣列,List便是表結構,ArrayList即陣列實現的

看得見的資料結構Android版表的陣列實現(檢視篇)

零、前言: 1.本文的姊妹篇:看得見的資料結構Android版之表的陣列實現(資料結構篇) 2.希望你可以和我在Github一同見證:DS4Android的誕生與成長,歡迎star 3.激動人心的時刻到了,要畫圖了,鉛筆、草稿紙、飲料、花生米準備好,現在開始了 先留圖鎮樓: 表結構的常規

資料結構與演算法美 課程筆記二 複雜度分析(上)

資料結構和演算法本身解決的是“快”和“省”的問題,即如何讓程式碼執行得更快,如何讓程式碼更省空間。所以,執行效率是演算法一個非常重要的考量指標。衡量演算法的執行效率最常用的就是時間和空間複雜度分析。 一、為什麼需要複雜度分析? 把程式碼跑一遍,通過統計、監控來得到演算法執行的時間和佔用的記憶

看得見的資料結構Android版二分搜尋樹結構實現

零、前言 1.個人感覺這個二叉搜尋樹實現的還是很不錯的,基本操作都涵蓋了 2.在Activity中對view設定監聽函式,可以動態傳入資料,只要可比較,都可以生成二分搜尋樹 3.二分搜尋樹的價值:搜尋、新增、刪除、更新速度快,最佳狀態複雜度logn,但極端情況下會退化成單鏈表 4.本例操作演示原始碼:

資料結構與演算法複雜度分析

一、內容        最好情況時間複雜度、最壞情況時間複雜度、平均情況時間複雜度、均攤時間複雜度。 二、為什麼要引入這幾個概念?        有助於我們可以更加全面地表示一段程式碼的執行效率,同樣一段

資料結構與演算法美-排序

堆和堆排序 如何理解堆 堆是一種特殊的樹,只要滿足以下兩點,這個樹就是一個堆。 ①完全二叉樹,完全二叉樹要求除了最後一層,其他層的節點個數都是滿的,最後一層的節點都靠左排列。 ②樹中每一個結點的值都必須大於等於(或小於等於)其子樹中每個節點的值。大於等於的情況稱為大頂堆,小於等於的情況稱為小頂堆。

資料結構與演算法列舉(窮舉)法 C++實現

列舉法的本質就是從所有候選答案中去搜索正確的解,使用該演算法需要滿足兩個條件: 1、可以先確定候選答案的數量; 2、候選答案的範圍在求解之前必須是一個確定的集合。 列舉是最簡單,最基礎,也是最沒效率的演算法 列舉法優點: 1、列舉有超級無敵準確性,只要時間足夠,正確的列舉得出的結

資料結構圖文解析:樹的簡介及二叉排序樹C++模板實現.

  閱讀目錄 0. 資料結構圖文解析系列 1. 樹的簡介 1.1 樹的特徵 1.2 樹的相關概念 2. 二叉樹簡介 2.1 二叉樹的定義 2.2 斜樹、滿二叉樹、完全二叉樹、二叉查詢樹 2

資料結構&&heap&priority_queue&實現

目錄 什麼是堆? 大根堆 小根堆 堆的操作 什麼是堆? 堆是一種資料結構,可以用來實現優先佇列 大根堆 大根堆,顧名思義就是根節點最大。我們先用小根堆的建堆過程學習堆的思想。 小根堆 下圖為小根堆建堆過程 堆的操作 上浮 下沉 插入 彈出 取頂 堆排序 STL heap 所在庫 #include

極客講堂資料結構與演算法美(一):複雜度分析(上)

(本文根據極客講堂——資料結構與演算法之美專欄的問答區整理修改而成,如有侵權還希望聯絡我鴨~) 一、什麼是複雜度分析? 1.資料結構和演算法解決是“如何讓計算機更快時間、更省空間的解決問題”。 2.因此需從執行時間和佔用空間兩個維度來評估資料結構和演算法的效能。 3.分別