数据结构――堆的基本概念及其操作

一、什么是堆

堆是利用完全二叉树的结构来维护一组数据,然后进行相关操作,一般的操作进行一次的时间复杂度在O(1)~O(log n)之间。

  1. 堆的两个特性:
  • 结构性:用数组表示的完全二叉树;
  • 有序性:任一结点的关键字是其子树所有结点的最大值(或最小值)。
  1. 堆的两种类型:
  • 最大堆(MaxHeap)”,也称“大顶堆”:任一结点的关键字是其子树所有结点的最大值
  • 最小堆(MinHeap)”,也称“小顶堆” :任一结点的关键字是其子树所有结点的最小值
    下面举一些最大堆最小堆的例子,如图所示:左边两个为最大堆,右边两个为最小堆
    在这里插入图片描述
    以下四个都不是堆:第一个和第二个不是完全二叉树;第三个第四个没有遵守堆的有序性。
    在这里插入图片描述
    注意:从根结点到任意结点路径上结点序列的有序性。
    不过有一点更需要注意:堆内的元素并不一定数组下标顺序来排序的!!很多的初学者会错误的认为大/小根堆中下标为1就是第一大/小,2是第二大/小……

下标从1到9分别加入:{a,b,c,d,e,f,g,h,i}。 如下图所示:(不要问我怎么加,想想你是怎么读入数组的。)
在这里插入图片描述

理解了上图,就理解堆是怎么读入数据得了,就可以进行堆的操作了。

二、堆的操作

1. 最大堆:

  • 最大堆的结构:

因为我们是用数组来存储一棵树,所以在结构体中包含一个数组的指针——Elements。

//define MAXDATA 1000  /* 该值应根据具体情况定义为大于堆中所有可能元素的值 */
typedef struct HeapStruct *MaxHeap;
struct HeapStruct 
{
	ElementType *Elements; /* 存储堆元素的数组 */
	int Size; /* 堆的当前元素个数 */
	int Capacity; /* 堆的最大容量 */
};
MaxHeap Create( int MaxSize )
{ /* 创建容量为MaxSize的空的最大堆 */
	MaxHeap H = malloc( sizeof( struct HeapStruct ) );
	/*创建容量为MaxSize+1的空的数组*/
	/*+1是因为堆的存放是从1开始的,不是从0开始*/
	H->Elements = malloc( (MaxSize+1) * sizeof(ElementType));
	H->Size = 0;
	H->Capacity = MaxSize;
	H->Elements[0] = MaxData;
	 /* 定义“哨兵”为大于堆中所有可能元素的值,便于以后更快操作 */
	return H;
}
  • 最大堆的插入

下面举几个例子解释如何插入:
给定一个最大堆如下图:
在这里插入图片描述
案例一:插入20,由于堆我们把它看成一个完全二叉树,根据完全二叉树的定义,当插入结点时,我们直接插入到最后一个位置,即图中的下标为6的位置。插入后的树是一课完全二叉树,也满足任一结点的关键字是其子树所有结点的最大值,所以得到的树还是一个最大堆,不需要调整。
在这里插入图片描述
案例二:插入35;不管插入什么,都是先插入到最后的位置。插入后的树是一课完全二叉树,但不满足任一结点的关键字是其子树所有结点的最大值——31小于35,所以得到的树不是一个最大堆,需要调整。那需要怎么调整呢?只需要把31和35对调即可。

在这里插入图片描述
得到以下结果:
注意:对调之后还要判断35是否比44大,如果是则还需要将44和35对调
在这里插入图片描述
案例三:插入58;插入后的树是一课完全二叉树,但不满足任一结点的关键字是其子树所有结点的最大值——31小于58,所以得到的树不是一个最大堆,需要调整。那需要怎么调整呢?把31和58对调。
在这里插入图片描述
得到结果如下图:但此时也还是不满足最大堆的定义,因为58比44大,所以还需要将58与44对调。
在这里插入图片描述
得到结果如下:此时就是一个最大堆了,不需要再进行调整。
这里有一个细节:我们要怎么知道58到达下标为1的位置时不用再调整了呢?这时候前面所说的哨兵就发挥作用了。H->Element[ 0 ] 是哨兵元素,它不小于堆中的最大元素,这时候只需要拿58和哨兵元素再比一次,发现比不过,就确定58不用再调整了,控制顺环结束。
在这里插入图片描述
代码如下:

bool IsFull( MaxHeap H )
{
    return (H->Size == H->Capacity);
}
 
bool Insert( MaxHeap H, ElementType X )
{ /* 将元素X插入最大堆H,其中H->Data[0]已经定义为哨兵 */
    int i;
    if ( IsFull(H) ) 
    { 
        printf("最大堆已满");
        return false;
    }
    i = ++H->Size; /* i指向插入后堆中的最后一个元素的位置 */
    for ( ; H->Data[i/2] < X; i/=2 )
        H->Data[i] = H->Data[i/2]; /* 上滤X */
    H->Data[i] = X; /* 将X插入 */
    return true;
}
  • 最大堆的删除

最大堆的删除它删除的位置是确定的,那就是值最大的根节点。而删除了根结点之后,我们需要有另一个结点来代替这个根结点。

下面看一个案例:

我要删除掉根节点58,但删除后会谁来替代根结点?
在这里插入图片描述
这里的策略是用最后一个结点替代,得到下图结果:但是此时并不是最大堆,所以我们还需要调整,该如何调整呢?那就是让根节点31与它的孩子比较,找出31的较大的孩子,然后让他们对调位置。

在这里插入图片描述
得到下图结果:但此时仍然不是最大堆,所以我们继续找出31的较大的孩子,继续对调位置。
在这里插入图片描述
得到下图结果,此时为最大堆,不需要再进行调整。
在这里插入图片描述
代码如下:

define ERROR -1 /* 错误标识应根据具体情况定义为堆中不可能出现的元素值 */
bool IsEmpty( MaxHeap H )
{
    return (H->Size == 0);
}
 
ElementType DeleteMax( MaxHeap H )
{ /* 从最大堆H中取出键值为最大的元素,并删除一个结点 */
    int Parent, Child;
    ElementType MaxItem, X;
    if ( IsEmpty(H) ) {
        printf("最大堆已为空");
        return ERROR;
    }
    MaxItem = H->Data[1]; /* 取出根结点存放的最大值 */
    /* 用最大堆中最后一个元素从根结点开始向上过滤下层结点 */
    X = H->Data[H->Size--]; /* 注意当前堆的规模要减小 */
    for( Parent=1; Parent*2<=H->Size; Parent=Child ) 
    /*因为从最大的根结点开始比较,所以Parent最开始为1*/
    /*如果Parent*2>H->Size,即当前结点的下标值*2大于此时堆的个数,即当前结点没有孩子*/
    {
        Child = Parent * 2;//Child为当前节点的左孩子
        /*找出当前结点左右子结点的较大者 */
        if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
        /*如果当前结点有两个孩子并且右孩子比左孩子大*/
        /*Child!=H->Size即当前结点的左孩子的下表等于此时堆的个数,说明当前结点只有一个左孩子*/
            Child++;  /* Child指向右孩子 */
        if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
        else  /* 下滤X,即让当前结点与其较大的孩子结点对调,继续往下寻找合适位置*/
            H->Data[Parent] = H->Data[Child];
    }
    H->Data[Parent] = X;
 
    return MaxItem;
} 
  • 最大堆的建立

建立最大堆:将已经存在的N个元素按最大堆的要求存放在一个一维数组中。下面我们给出两种办法:
(1)通过插入操作,将N个元素一个个相继插入到一个初始为空的堆中去,其时间代价最大为O(N log N)。
(2)在线性时间复杂度下建立最大堆。
  ①将N个元素按输入顺序存入,先满足完全二叉树的结构特性。
  ②调整各结点位置,以满足最大堆的有序特性。

方法(1)比较简单,只需要在读取到一个数后调用插入函数即可。

我们详细讲一下方法(2)。
给定一棵完全二叉树,我们要怎么把它变成最大堆呢?
在这里插入图片描述
说白了,其实就是还是对调,就是找到最后最后一个有孩子的结点,判断当前结点是否大于其孩子,否则对调,是则跳到当前结点下标-1的结点。
看下面的分析:
第一步,我们找到了最后一个有孩子的结点——87,判断它和它的孩子是否组成最大堆,因为是,所以不需要对调。
在这里插入图片描述
接着跳到87结点下标-1的结点,即30,判断它和它的孩子是否组成最大堆,答案肯定是否。那就找到它两个孩子中的最大值,为72,然后把30和72对调。
在这里插入图片描述
接着跳到83结点,重复上面的步骤。
在这里插入图片描述
调整完之后,接着跳到43结点,重复上面的步骤。
在这里插入图片描述
调整完之后,接着跳到66结点,重复上面的步骤。当66和91对调之后,发现66比83小,所以还需要将83和66对调。
在这里插入图片描述
调整完之后,接着跳到79结点,重复上面的步骤。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后得到了如下结果,此时它已变成一个最大堆。
在这里插入图片描述
由上述我们可以知道:方法二创建最大堆的调整方式与最大堆的删除的调整方式类似:找到当前结点的最大子孩子,与当前结点进行比较,如果比当前结点大,那么就让他们对调位置,一直重复这个操作,直到找到合适位置。调整完之后再将下标-1,去下一个结点重复此操作

代码如下:

/*----------- 建造最大堆 -----------*/
void PercDown( MaxHeap H, int p )
{ /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
    int Parent, Child;
    ElementType X;
 
    X = H->Data[p]; /* 取出根结点存放的值 */
    for( Parent=p; Parent*2<=H->Size; Parent=Child )
    {
        Child = Parent * 2;
        if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
            Child++;  /* Child指向左右子结点的较大者 */
        if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
        else  /* 下滤X */
            H->Data[Parent] = H->Data[Child];
    }
    H->Data[Parent] = X;
}
 
void BuildHeap( MaxHeap H )
{ /* 调整H->Data[]中的元素,使满足最大堆的有序性  */
  /* 这里假设所有H->Size个元素已经存在H->Data[]中 */
 
    int i;
 
    /* 从最后一个结点的父节点开始,到根结点1 */
    for( i = H->Size/2; i>0; i-- )
        PercDown( H, i );
}

2. 最小堆:

最小堆与最大堆类似,仿照最大堆改一改代码就好了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值