数据结构基础:P5.1-树(三)--->堆

本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-应用实例—>多项式加法运算
数据结构基础:P2.5-应用实例—>多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现
数据结构基础:P4.1-树(二)—>二叉搜索树
数据结构基础:P4.2-树(二)—>二叉平衡树
数据结构基础:P4.3-树(二)—>小白专场:是否同一棵二叉搜索树-C实现
数据结构基础:P4.4-树(二)—>线性结构之习题选讲:逆转链表


一、什么是堆

有的时候我们排队不会按照时间的先后顺序来进行,需要考虑优先的级别。典型的应用是在我们的计算机中 cpu 的调度的:

假如说我们只有一个 cpu,然后很多任务要运行,大家都抢着要。那么这个 cpu 应该给谁呢?它的排队原则是什么呢?简单的方法就是按照时间,谁先来谁排在前面。但这里面就有一些问题,我们很多任务,它们的优先级别是不一样的。比如说我这个机器是用来控制核反应堆的过程,然后我有两个任务,一个是要进行核心的调度,一个要打印一张纸。所以显然这种涉及到核心调度的企业任务要先做。所以这样的话我们有时候要管理一种队列,这个队列就不是按照时间顺序先来先服务的。

我们可以想象一下,我们想管理一个优先队列,最常见的操作是什么呢?

一个是往这个优先队列里面插入一个新的任务,还有就是CPU空了然后我想拿一个优先级比较高的任务来执行,这个时候我要从这个队列里面挑一个最大值,然后来执行。归纳起来我们可以把它抽象成这样的一种数据结构:我们想管理一个树这样的一个序列,主要做两个操作。一个做插入操作,往这个集合里面插入任意值的一个元素。然后从这个集合里面挑个值最大的,把它删除。


我们怎么进行管理优先队列呢?首先我们来看一下,若采用数组或链表实现优先队列:

数组
插入 ---- 元素总是插入尾部 ~ O ( 1 ) O(1) O(1)
删除 ---- 查找最大(或最小)关键字 ~ O ( n ) O(n) O(n)
从数组中删去需要移动元素 ~ O ( n ) O(n) O(n)
链表
插入 ---- 元素总是插入链表的头部 ~ O ( 1 ) O(1) O(1)
删除 ---- 查找最大(或最小)关键字 ~ O ( n ) O(n) O(n)
删去结点 ~ O ( 1 ) O(1) O(1)
有序数组
插入 ---- 找到合适的位置 ~ O ( n ) O(n) O(n) O ( l o g 2 n ) {\rm{O(lo}}{{\rm{g}}_{\rm{2}}}{\rm{n)}} O(log2n)
移动元素并插入 ~ O ( n ) O(n) O(n)
删除 ---- 删去最后一个元素 ~ O ( 1 ) O(1) O(1)
有序数组
插入 ---- 找到合适的位置 ~ O ( n ) O(n) O(n)
插入元素 ~ O ( 1 ) O(1) O(1)
删除 ---- 删除首元素或最后元素 ~ O ( 1 ) O(1) O(1)


所以不管怎么做,在这四种方案里面,当有令人满意的 O ( 1 ) O(1) O(1),总会有与之对应的令人不满意的~ O ( n ) O(n) O(n)。接下来我们再来想有没有可能用树来存储,用树存储首先想到的是就是搜索树。

我们提到过搜索树的一个优点:插入结点跟树的高度是有关系的。所以如果树的高度合适,那么插入的复杂度为 O ( l o g 2 n ) {\rm{O(lo}}{{\rm{g}}_{\rm{2}}}{\rm{n)}} O(log2n)。要删除时,我们要删除的是最大值或最小值,要么在最左边,要么在最右边。所以用查找树来做的话,它的插入和删除时间效率就是树的高度。但问题在于,我们如果是想每次都删除最大的,意味着每次都要删除最右边的,左边的结点保持不动。删几次之后,造成树歪掉了,树的高度就不再是 O ( l o g 2 n ) {\rm{O(lo}}{{\rm{g}}_{\rm{2}}}{\rm{n)}} O(log2n)了。这显然不是我们所希望的一种结果。


那还有别的方法吗?

我们再来想一想,如果我们采用树的结构,我们操作是插入任何结点和删除最大值。而我们应该重点考虑删除最大值,因为删除最大值跟插入比更难做。所以在这种情况下面,我们很自然的想法就是把这些数据放在二叉树里面,同时最大的值在树根,这样的话你要删除就把树根拿掉就行了。如果是这样子做的话,我们得树结构可以安排得平衡一点,最好的方法就是用完全二叉树。所以 的一个特点就是用完全二叉树来进行存储,然后它的每一个结点都满足一个特性,任何结点值都比他左右的子树所有结点值都要大。


下面我们看到是一个完全二叉树,我们可以把它放在数组里面,从根结点(下标为1的地方开)始逐步地往后面存放。
在这里插入图片描述
堆的两个特性

结构性:用数组表示的完全二叉树;
有序性:任一结点的关键字是其子树所有结点的最大值(或最小值)
最大堆(MaxHeap),也称“大顶堆”:最大值
最小堆(MinHeap),也称“小顶堆” :最小值


下面我们来看一下堆的一些例子。前面两个都是完全二树,而且对任何结点来讲,它的值都比它左右两边的所有的儿子包括这些子树上面的所有结点都要大。所以前面两个叫做最大堆,后面两个是最小堆。我们可以看出:从根结点到任意结点路径上结点序列是有序的。
在这里插入图片描述

下面我们来看一下不是堆的一些例子。前面两个缺少了左边的叶结点,后面两个值不满足。
在这里插入图片描述


堆的抽象数据类型描述

类型名称最大堆(MaxHeap)
数据对象集完全二叉树,每个结点的元素值不小于其子结点的元素值
操作集:最大堆 H ∈ M a x H e a p {\rm{H}} \in {\rm{MaxHeap}} HMaxHeap,元素 i t e m ∈ E l e m e n t T y p e {\rm{item}} \in {\rm{ElementType}} itemElementType,主要操作有:
•MaxHeap Create( int MaxSize ):创建一个空的最大堆。
•Boolean IsFull( MaxHeap H ):判断最大堆H是否已满。
•Insert( MaxHeap H, ElementType item ):将元素item插入最大堆H。
•Boolean IsEmpty( MaxHeap H ):判断最大堆H是否为空。
•ElementType DeleteMax( MaxHeap H ):返回H中最大元素(高优先级)。


二、堆的插入

首先我们来看一下 堆 这个数据结构我们怎么定义。

typedef struct HeapStruct *MaxHeap;
struct HeapStruct {
	ElementType *Elements; /* 存储堆元素的数组 */
	int Size; /* 堆的当前元素个数 */
	int Capacity; /* 堆的最大容量 */
};

接着我们看看如何建立这样的一个结构。

MaxHeap Create( int MaxSize )
{ /* 创建容量为MaxSize的空的最大堆 */
	MaxHeap H = malloc( sizeof( struct HeapStruct ) );
	H->Elements = malloc( (MaxSize+1) * sizeof(ElementType));
	H->Size = 0;
	H->Capacity = MaxSize;
	H->Elements[0] = MaxData; 
	 /* 定义“哨兵”为大于堆中所有可能元素的值,便于以后更快操作 */
	return H;
}

我们接下来看看插入怎么做,现在我有如下的一个完全二叉树,要在6那个结点(最后那个结点)插入一个元素。
在这里插入图片描述

①如果我们插入一个20,把20放在下标为6的位置,没有破坏有序性。
在这里插入图片描述
在这里插入图片描述
②如果插入35,有序性被破坏。
在这里插入图片描述
这个时候需要换位置,让31与35互换位置。
在这里插入图片描述
在这里插入图片描述
③插入58,有序性被破坏,需要一直往上换位置,分别与31和44互换位置,直到满足有序性。
在这里插入图片描述
在这里插入图片描述

算法对应代码如下:

void Insert( MaxHeap H, ElementType item )
{ /* 将元素item 插入最大堆H,其中H->Elements[0]已经定义为哨兵 
它不小于堆中的最大元素,控制顺环结束。
*/
	 int i;
	 if ( IsFull(H) ) {
		 printf("最大堆已满");
		 return;
	 }
	 i = ++H->Size; /* i指向插入后堆中的最后一个元素的位置 */
	 //如果插入得元素值大于其父结点,就将父结点挪到下面,进行互换
	 for ( ; H->Elements[i/2] < item; i/=2 )
		 H->Elements[i] = H->Elements[i/2]; /* 向下过滤结点 */
	 H->Elements[i] = item; /* 将item 插入 */
}

三、堆的删除

我们接下来看看最大堆的删除,现在我们还是这样一棵完全二叉树
在这里插入图片描述
假设我们现在要删除58那个结点,删除后就只有4个结点了,删除步骤如下:

①将数组尾部的元素(我这里是31)放到要删除的那个结点(58)去。此时满足结构性,但是不满足有序性,需要进行调整。
在这里插入图片描述
②找出31的较大的孩子,然后与31比较大小。如果31较小,则互换位置。
在这里插入图片描述
③然后继续找出31较大的孩子,继续进行比较互换。最后结果如下:
在这里插入图片描述

算法对应代码如下:

ElementType DeleteMax( MaxHeap H )
{ /* 从最大堆H中取出键值为最大的元素,并删除一个结点 */
	int Parent, Child;
	ElementType MaxItem, temp;
	if ( IsEmpty(H) ) {
		printf("最大堆已为空");
		return;
	}
	MaxItem = H->Elements[1]; /* 取出根结点最大值 */
	/* 用最大堆中最后一个元素从根结点开始向上过滤下层结点 */
	temp = H->Elements[H->Size--];
	//如果有左儿子
	for( Parent=1; Parent*2<=H->Size; Parent=Child ) {
		Child = Parent * 2;//左儿子,我们先假定它大
		//如果右儿子大且Chile不是最后一个结点。如果Child=Size,说明只有左儿子是最后一个元素,没右儿子
		if( (Child!= H->Size) &&
			(H->Elements[Child] < H->Elements[Child+1]) )
			Child++; /* Child指向左右子结点的较大者 */
		if( temp >= H->Elements[Child] ) break;
		else /* 移动temp元素到下一层 */
		//将左右儿子较大的拷贝到父结点
			H->Elements[Parent] = H->Elements[Child];
	}
	H->Elements[Parent] = temp;
	return MaxItem;
}

四、最大堆的建立

最大堆很有用,我们后面会讲到。在排序里面我们可以用堆排序,用堆来排序首先要形成一个堆。所以必须要考虑给你 n 个元素,怎么样把它建成最大堆。

方法1:通过插入操作,将N个元素一个个相继插入到一个初始为空的堆中去,其时间代价最大为 O ( N l o g N ) O(NlogN) O(NlogN)
方法2:在线性时间复杂度 O ( N ) O(N) O(N)下建立最大堆。
(1)将N个元素按输入顺序存入,先满足完全二叉树的结构特性。
(2)调整各结点位置,以满足最大堆的有序特性。

在方法二中,怎么调整各结点位置?

调整的操作跟我们前面删除的操作是很像的。
删除操作核心的思想是:是已知左边是个堆,右边是个堆,我来了一个元素怎么把它调成一个堆。调的方法就是跟下面左右儿子里面去比较,然后调一个上来,然后一直重复这个过程。在这个过程中,每一个结点的左右子树仍然是堆。
对于最大堆的建立,情况则不一样:现在我有这样一个堆,这里面对根结点79来讲,左边不是堆,右边不是堆。对66来讲,左边不是堆,右边也不是堆。
在这里插入图片描述
那怎么办这个过程怎么做呢?我们可以从底下往上做。具体而言,可以从底下倒数第一个有儿子的结点87开始。
在这里插入图片描述
然后继续往上一层调整。此时上面那一层的各个结点的左右子树已经是堆了,可以按照删除操作中的调整步骤进行调整。
在这里插入图片描述
最后调整根结点,这个时候根结点的左右子树已经被我们从上到下调整成堆了,所以可以按照删除操作中的调整步骤进行调整。
在这里插入图片描述


五、C语言代码:堆的定义与操作

typedef struct HNode *Heap; /* 堆的类型定义 */
struct HNode {
    ElementType *Data; /* 存储元素的数组 */
    int Size;          /* 堆中当前元素个数 */
    int Capacity;      /* 堆的最大容量 */
};
typedef Heap MaxHeap; /* 最大堆 */
typedef Heap MinHeap; /* 最小堆 */

#define MAXDATA 1000  /* 该值应根据具体情况定义为大于堆中所有可能元素的值 */

MaxHeap CreateHeap( int MaxSize )
{ /* 创建容量为MaxSize的空的最大堆 */

    MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode));
    H->Data = (ElementType *)malloc((MaxSize+1)*sizeof(ElementType));
    H->Size = 0;
    H->Capacity = MaxSize;
    H->Data[0] = MAXDATA; /* 定义"哨兵"为大于堆中所有可能元素的值*/

    return H;
}

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;
}

#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 ) {
        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;

    return MaxItem;
} 

/*----------- 建造最大堆 -----------*/
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 );
}

六、小测验

1、下列序列中哪个是最小堆?

A. 2, 55, 52, 72, 28, 98, 71
B. 2, 28, 71, 72, 55, 98, 52 
C. 2, 28, 52, 72, 55, 98, 71
D. 28, 2, 71, 72, 55, 98, 52

答案:C

2、在最大堆 {97,76,65,50,49,13,27}中插入83后,该最大堆为:

A. {97,76,65,83,49,13,27,50}
B. {97,83,65,76,49,13,27,50}
C. {97,83,65,76,50,13,27,49}
D. {97,83,65,76,49,50,13,27}

答案:B

3、对由同样的n个整数构成的二叉搜索树(查找树)和最小堆,下面哪个说法是不正确的:

A. 二叉搜索树(查找树)高度大于等于最小堆高度
B. 对该二叉搜索树(查找树)进行中序遍历可得到从小到大的序列
C. 从最小堆根节点到其任何叶结点的路径上的结点值构成从小到大的序列
D. 对该最小堆进行按层序(level order)遍历可得到从小到大的序列

答案:D

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值