一起来学数据结构——保证你理解堆的含义和操作(堆的操作,堆的创建)

一起来学数据结构——堆

前言

今天,我们来介绍一种特殊的数据结构——堆。

这个堆可不是堆栈的那个堆。堆是一种特殊的二叉树——完全二叉树

所以,一定要记住堆不是堆,是树。

image-20211013200258242

这就是我们的一个堆。

堆的存储结构——数组

因为堆(完全二叉树)的这种连续存储的方式,我们可以采用数组来进行存储。

逻辑结构:

image-20211013201031339

物理结构:

image-20211013201209380

我们可以观察到一个规律:

对于一个非叶节点的两个子节点:

左孩子节点的下标=父亲节点*2+1;右孩子节点的下标=父亲节点*2+2;

堆的分类——大堆和小堆

前面我们给出了堆的存储结构——数组,但是,是给一个数组就是堆吗?

答案是:不是。

堆还有一个很严格的标准:父节点大于等于它的子节点或父节点小于等于它的字节点。

大堆

对于父节点大于等于它的字节的的堆,我们叫他大堆

大堆的最大值就是它的根节点。

每个父节点都大于它的子节点

但是,大堆可不是以递减的顺序排列的,只是父节点比子节点大

image-20211013203647143

小堆

父节点小于等于子节点的堆,我们叫它为小堆。

小堆的根就是最小值

每个父节点都小于它的子节点,但是小堆可不是递增的

image-20211013204056413

堆的操作

正是因为堆的特殊的存储结构和堆的特殊的性质。

堆的操作更加特殊且有趣。

下面我们就来看一看经典的算法。

堆的向下调整算法(专门用于根节点不成堆问题)

我们就先来看一个基础简单的一个小操作。

**如果根节点的左右子树都是堆。**但是根节点不符合要求,使这个整体无法成为堆。

我们就可以采用堆的向下调整算法来重新使它变为堆。

例:

image-20211013210655556 image-20211013210639137

​ 我们让根节点为p,两个中较大的子节点为c

image-20211013210901551 image-20211013210931369 image-20211013211001518

image-20211013211036412

​ 15仍然比23和25小

image-20211013211129541

​ 重复上面的操作, image-20211013211222750

上面的就是一个完整的向下调整算法。

我们来分析一下它的时间复杂度,如果考虑到最坏的情况:

设整个堆的个数是N,层数是h,h和N的关系是h=log(N+1+x).

根节点从最开始的第一层,一直移动到最后一层。

进行了h-1次交换和比较,所以时间复杂度是O(logN)

void AdjustDown(int arr[], int sz, int parent)
{
	int child = 2 * parent + 1;
	while (child < sz)//直到child的值为不再数组范围内后停止
	{
		//找到孩子中较大的那一个
		//注意:此时要判断右孩子是否存在
		if (arr[child + 1] > arr[child] && child + 1 < sz)
		{
			child++;
		}
		//父亲和孩子进行比较
		//如果孩子比较大
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		//如果父亲比最小的孩子还小,说明很正常
		else
		{
			break;
		}
	}
}

建堆(根的左右子树都不是堆的只是一个数组的情况)

但是,对于那种堆的左右子树不是堆的那种情况,这种方法就不太使用了。

但是,我们还是可以发现一些线索:

没有条件我们可以创造条件啊!我们可以将堆的左右子树变为堆。

那我们采用什么样的方法呢?

具体方法:

从下往上的让它变为堆。

我们从最后一个非叶节点的值开始,使用我们上面的向下调整,让他的子树全部都是堆。

实例演示:

将下面的数组建成大堆

下面的例子就是一个普通的数组,它不是一个堆,根节点的左右子树也不是堆。

image-20211017201231183

现在就按照我们上面的方法来进行依次建堆

示意图:

image-20211017220433541

每一步具体介绍:

  1. 从第一个非叶子节点开始向下调整,第一个非叶子节点是10,大于它的子节点1,所以10的子树不需要向下调整。
  2. 接着判断其他的非叶子节点,10的左边是4,
  3. 我们发现4比7小,需要向下调整.
  4. 调整完之后,在像左判断,3比9小,需要向下调整。
  5. 调整完3后调整6,这里我们就会发现我们刚才从无到有建立子堆的重要性了。它的两个子树全部都是大堆,可以正常的运用向下调整算法。
  6. 对于根5也可以体现我们这个从上到下依次建堆的优越性,根5的左右子树都是堆,还是可以使用我们的向下调整算法。
  7. 直到我们的下标p到达0为止,建堆完成

时间复杂度分析

知道了如何建堆,我们就要来计算一下时间复杂度。

设堆的深度是h,堆的元素是N。

对于堆的第n层,堆的元素个数是

2 n − 1 2^{n-1} 2n1

如果是最坏的情况,该层的所有元素都要进行向下调整,在最坏条件下每一次的向下调整需要比较的次数是h-n,

所以该层需要比较的次数就是
2 n − 1 ∗ ( h − n ) 2^{n-1}*(h-n) 2n1(hn)
所以n层,也就是整个堆,需要比较的次数就是
S ( h ) = 2 0 ∗ ( h − 1 ) + 2 1 ∗ ( h − 2 ) + . . . . . . + 2 h − 2 ∗ 1 + 2 h − 1 ∗ 0 S(h)=2^{0}*(h-1)+2^1*(h-2)+......+2^{h-2}*1+2^{h-1}*0 S(h)=20(h1)+21(h2)+......+2h21+2h10
我们可以使用错位相减法来计算S(h)
2 ∗ S ( h ) = . . . . . . . . . . . . . . . . . + 2 1 ∗ ( h − 1 ) + . . . . . . + 2 h − 2 ∗ 2 + 2 h − 1 ∗ 1 + 2 h ∗ 0 2*S(h)=.................+2^1*(h-1)+......+2^{h-2}*2+2^{h-1}*1+2^{h}*0 2S(h)=.................+21(h1)+......+2h22+2h11+2h0
用上面减去下面,
S ( h ) = 2 1 + . . . . . . + 2 h − 2 + 2 h − 1 S(h)=2^1+......+2^{h-2}+2^{h-1} S(h)=21+......+2h2+2h1
再利用等比数列求和公式,就可以求出总比较次数为:
S ( h ) = 2 − 2 ∗ 2 h − 1 1 − 2 = 2 h − 2 S(h)=\frac{2-2*2^{h-1}}{1-2}=2^h-2 S(h)=12222h1=2h2
所以时间复杂度为S(h)

因为堆的层数为h,个数为N
N = 2 h − 1 N=2^h-1 N=2h1
时间复杂度也为O(N)

代码实现

注意:第一个非叶子节点是数组最后一个下标php->size,无论是对于左孩子还是右孩子,找到父亲的下标都可以将它们的下标-1后再除以2.

for (int i = (php->size - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(php->a,php->size, i);
	}

堆的排序

现在我们掌握了向下调整和建堆的算法,还有一个值得注意的一个重要算法,那就是堆的排序算法。

我们想要将堆排成降序,那应该怎么来排呢?

将堆排成大堆,然后选出来最大的数,然后再重新建堆。但是这样的时间复杂度就是O(N*N),还不如直接用冒泡排序来排序了,我们建堆也就没有意义了。

思路:

所以,充分考虑到堆的特性,我们考虑先将堆建成小堆,再将然后将根和最后一个数进行互换,这样最小的数就到了最后,新的根的左右子树还是堆,我们使用向下调整算法。就是这样的重复,直到将所有的数字遍历一遍为止,整个的时间复杂度是O(N*logN).这和自前的O(N*N)可是减少了不少的操作。

具体实例

将一个小堆排成降序。

image-20211019152140267

  1. 因为小堆的第一个一定是最小的,所以将根和最后一个元素交换,最小的根就到最后了。
  2. 此时将最小的元素从堆中排除出去,并向下调整堆,继而就选出来第二小的元素
  3. 再重复第一个操作,直到进行了n-1次以后(堆的元素是n个,所以选n-1次,自然排除序列)停止。

代码分析

void HeapSort(int arr[], int sz)
{
	int end = sz - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		end--;
	}
}

堆的实现

那么如果我们要模拟实现一个堆的话,应该怎么实现呢?

堆的结构

首先我们先来考虑一下堆的结构,我们要用数组来表示堆。所以,我们最好使用结构体进行封装,记录下堆的个数。

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

堆的初始化

堆的结构有了之后,我们就可以对堆进行初始化了。这里我们采用外部给出数字,我们将它填入到堆中的方式。

  1. 首先先开辟堆的大小的空间
  2. 将外部给出的数字全部都拷贝到新开辟的空间中去
  3. 建堆,建成大堆或小堆。
void HeapInit(HP* php, HPDataType* a, int n)
{
	assert(php);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("HeapInit");
		exit(-1);
	}
	php->capacity = n;
	php->size = n;
	//将数组a拷贝到php的数组中
	memcpy(php->a, a, sizeof(HPDataType) * n);
	//建堆
	for (int i = (php->size - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(php->a,php->size, i);
	}
	
}

向堆中插入元素

在堆的最后插入元素非常好操作,直接在后面再添加一个元素就可以了(注意扩容)。

但是,要是插入元素后还是成堆就需要向上调整了(和我们的向下调整代码类似)

向上调整代码:

注意操作结束的条件。

void AdjustUp(HP* php,int size)
{
	int father = (size - 1 - 1) / 2;
	int child = size - 1;
	//这个条件是不对的,child=0,father=0,将会循环,但是恰好
	//a[child]==a[father]
	//while (father>=0)
	while(child>0)
	{
		if (php->a[child] > php->a[father])
		{
			Swap(&php->a[child], &php->a[father]);
			child = father;
			father = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

向堆中插入元素的代码:

注意判断是否需要扩容。

void HeapPush(HP* php, HPDataType x)
{
	if (php->size == php->capacity)
	{
		HPDataType* tmp=(HPDataType*)realloc (php->a, sizeof(HPDataType) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("HeapPush");
			exit(-1);
		}
		php->a = tmp;
	}
	php->a[php->size] = x;
	php->size++;
	php->capacity *= 2;
	AdjustUp(php,php->size);
}

删除堆中元素(做法和排序堆类似)

删除堆中元素,这里指的是堆中的第一个元素,

如果直接将第一个元素删掉,还要重新建堆,时间复杂度是O(N)。

反而如果还是将最后一个元素和第一个元素互换,将最后一个元素删掉后(也就是删掉了原来的第一个元素),再向下调整重新成为堆,这样的时间复杂度就是O(logN)

void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

得到堆中第一个元素

直接返回堆的元素的第一个就行。

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}

计算堆的大小

直接返回堆的size

int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

判断堆是否为空

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

释放堆

堆是动态开辟的,不能只是开辟,还要释放

void HeapDestory(HP* php)
{
	free(php->a);
	php->capacity = php->size = 0;
}

这就是堆的知识点,希望有错误给出指正,谢谢。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值