数据结构--堆的实现之深入分析

一,介绍

以前在学习堆时,写了两篇文章:数据结构--堆的实现(上)   和   数据结构--堆的实现(下),  感觉对堆的认识还是不够。本文主要分析数据结构 堆(讨论小顶堆)的基本操作的一些细节,比如 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)。

时间: 2024-08-28 16:47:28

数据结构--堆的实现之深入分析的相关文章

基本数据结构——堆(Heap)的基本概念及其操作

基本数据结构――堆的基本概念及其操作 小广告:福建安溪一中在线评测系统 Online Judge 在我刚听到堆这个名词的时候,我认为它是一堆东西的集合... 但其实吧它是利用完全二叉树的结构来维护一组数据,然后进行相关操作,一般的操作进行一次的时间复杂度在 O(1)~O(logn)之间. 可谓是相当的引领时尚潮流啊(我不信学信息学的你看到log和1的时间复杂度不会激动一下下)!. 什么是完全二叉树呢?别急着去百度啊,要百度我帮你百度: 若设二叉树的深度为h,除第 h 层外,其它各层 (1-h-1

【C/C++学院】0828-STL入门与简介/STL容器概念/容器迭代器仿函数算法STL概念例子/栈队列双端队列优先队列/数据结构堆的概念/红黑树容器

STL入门与简介 #include<iostream> #include <vector>//容器 #include<array>//数组 #include <algorithm>//算法 using namespace std; //实现一个类模板,专门实现打印的功能 template<class T> //类模板实现了方法 class myvectorprint { public: void operator ()(const T &

数据结构--堆的实现(下)

1,堆作为优先级队列的应用 对于普通队列而言,具有的性质为FIFO,只要实现在队头删除元素,在队尾插入元素即可.因此,这种队列的优先级可视为按 时间到达 的顺序来衡量优先级的.到达得越早,优先级越高,就优先出队列被调度. 更一般地,很多应用不能单纯地按时间的先后来分优先级,比如按CPU占用时间或者其它方式……在这种情形下,使用堆更容易表达优先级队列. 2,堆的两个性质:①结构性质--堆从结构上看是一颗完全二叉树.然而,从物理存储上看,堆的实现基本上是使用一个一维数组存储堆中所有的结点.②orde

C 数据结构堆

引言 - 数据结构堆 堆结构都很耳熟, 从堆排序到优先级队列, 我们总会看见它的身影. 相关的资料太多了, 堆 - https://zh.wikipedia.org/wiki/%E5%A0%86%E7%A9%8D 无数漂亮的图片接二连三, 但目前没搜到一个工程中可以舒服用的代码库. 本文由此痛点而来. 写一篇奇妙数据结构堆的终结代码. 耳熟终究比不过手热 ->--- 对于 heap 接口思考, 我是这样设计 #ifndef _H_HEAP #define _H_HEAP // // cmp_f

数据结构-堆

堆(英语:Heap),是一种拥有像树那样的特殊数据结构,或者理解为具有优先级的树.它的特点是父节点的值大于(或小于)两个子节点的值(分别称为大顶堆和小顶堆).它常用于管理算法执行过程中的信息,应用场景包括堆排序,优先队列等.堆通常是一个可以被看做一棵树的数组(或ArrayList)对象.常见的堆有二叉堆.二项堆.斐波那契堆等. 二叉堆(Binary heap) 二叉堆是一种特殊的堆,实为二叉树的一种:是完全二叉树或者是近似完全二叉树.二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子

浅谈数据结构-堆

在数据结构的世界中有一个叫堆的玩意,这玩意有什么用呢?无用,都去pq了 堆,其实就是一棵完全二叉树. “若设二叉树的深度为h,除第 h 层外,其它各层 (1-h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树”  by 谋财害命公司 百度 ↑清真的 完全二叉树 ↓ 啊那么为什么会闲的无聊出现这种奇怪的数据结构呢? 因为我们的某些算法可能需要堆来进行优化,如dj,prim. 堆可以在O(1)的时间取出最优值,但是需要O(logn)的时间修改和O(nlogn)

数据结构—堆

堆是二叉树中的一种,是一种常见的数据结构,具有以下性质: 任意节点小于(最小堆)或大于(最大堆)它的所有后裔,最小元或最大元在堆的根上(堆序性). 堆总是一棵完全二叉树 最大堆如图一,最小堆如图二. 最小堆的实现如下: MinHeap.h 1 #include "stdafx.h" 2 #include <iostream> 3 using namespace std; 4 template <typename Type> 5 class MinHeap{ 6

数据结构-----堆的基本操作和应用

(一)用仿函数实现大堆小堆 堆数据结构是一种数组对象,它可以被视为一棵完全二叉树结构. 堆结构的二叉树存储是 最大堆:每个父节点的都大于孩子节点. 最小堆:每个父节点的都小于孩子节点. 仿函数(functor),就是使一个类的使用看上去象一个函数.其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了.在实现大,小堆的过程中,有些功能的的代码,会在不同的成员函数中用到,想复用这些代码,有两种途径. 1)公共的函数,这是一个解决方法,不过函数用到的一些变量,就可

数据结构——堆(Heap)大根堆、小根堆

Heap是一种数据结构具有以下的特点: 1)完全二叉树: 2)heap中存储的值是偏序: Min-heap: 父节点的值小于或等于子节点的值: Max-heap: 父节点的值大于或等于子节点的值: 堆的存储: 一般都用数组来表示堆,i结点的父结点下标就为(i–1)/2.它的左右子结点下标分别为2 * i + 1和2 * i + 2.如第0个结点左右子结点下标分别为1和2. 堆的操作:insert 插入一个元素:新元素被加入到heap的末尾,然后更新树以恢复堆的次序. 每次插入都是将新数据放在数组