个人主页:星纭-CSDN博客
系列文章专栏 :数据结构
踏上取经路,比抵达灵山更重要!一起努力一起进步!
目录
一.堆的介绍
二.堆的实现
1.向下调整算法
2.堆的创建
3.堆的实现
4.堆的初始化和销毁
5.堆的插入
5.1扩容
5.2向上调整与向下调整
5.2.1向上调整
6.堆的删除
5.2.2 向下调整
7.取堆顶数据
8.堆的数据个数
9.判读堆是否为空
一.堆的介绍
1.1堆的概念以及结构
简单来说,堆总是一个完全二叉树,如果每一个节点都比其子节点的值大就叫大堆,反之就叫做小堆。
二.堆的实现
1.向下调整算法
现在我们给出一个数组,逻辑上看做一个完全二叉树。我们通过从根节点开始的向下调整算法可以把他调整成为一个小堆。前提是:左右子树必须是一个堆,这样才能调整。
首先是27与15和19之间最小的数字交换,也就是与15交换位置。然后再与18和28之间最小的数字交换,即与18交换,再与25交换。
通过这三步,最终得到的就是一个小堆。
2.堆的创建
下面我们给出一个数组,这个数组逻辑上可以看作一个完全二叉树,当时它还不是一个堆,现在我们通过算法,把他构建成一个堆。可是这个完全二叉树的根节点的左右两个子树也不是堆,如何进行调整呢?只需要从倒数第一个非叶子节点的子树开始调整即可,一直到根节点。
接下来尝试把下面的数组调整成为一个大堆。
int a[] = {1,5,3,8,7,6}
3.堆的实现
typedef int HPDataType;
typedef struct Heap
{HPDataType* _a;int _size;int _capacity;
}Heap;
//堆的初始化
void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
堆的实现,我们仍然采用多个文件的方式进行,以上是头文件中的代码,也是我们需要完成的函数。
在上述的代码中的结构体中size表示数组的大小(存入有效数据的个数),capacity表示容量。
4.堆的初始化和销毁
初始化和销毁比较简单。
初始化:当这个数组没有使用过的时候,其中容量和大小都未0。也没有动态开辟内存。
销毁:释放掉动态开辟的内存,将容量和大小设置为0。
void HeapInit(Heap* hp) {assert(hp);hp->_a = NULL;hp->_capacity = hp->_size = 0;
}void HeapDestory(Heap* hp)
{assert(hp);free(hp->_a);hp->_a = NULL;hp->_capacity = hp->_size = 0;
}
5.堆的插入
因为在堆的初始化的时候,是将大小和容量均初始化为0的,所以在插入数据的时候,要判断这个堆中的数组是否需要扩容,由于关于扩容的代码不会经常用到,所以这里就不单独封装函数。
5.1扩容
当size和capacity相等的时候,就代表此时的数组已满,需要扩容。
if (hp->_capacity == hp->_size) {int newcapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;HPDataType* tmp = realloc(hp->_a,newcapacity * sizeof(HPDataType));if (tmp == NULL) {perror("malloc fail");exit(1);}hp->_a = tmp;hp->_capacity = newcapacity;}
通常情况下扩容都是扩大二倍,当时由于初始化为0了,所以需要判读是否为0,这里采用三目操作符,如果为0,就设置为4,不为0就乘2。
调整动态开辟的内存的大小需要使用realloc函数。
或许有读者疑惑,realloc函数的第一个参数需要填要调整的动态内存块的起始地址。可是在开始的时候hp->_a是NULL。这怎么办呢???
其实realloc函数的第一个参数是NULL的时候,它的功能就和malloc函数的功能一样了,就会直接开辟空间。
第二个参数是内存的大小(单位字节),所以是newcapacity * HPDataType。
最后不要忘记将tmp和newcapacity赋值给hp。
5.2向上调整与向下调整
将数据拿出来插入到数组的最后一个位置上,可是此时并不能肯定该完全二叉树就是堆。
所以我们需要根据该数据进行调整。
hp->_a[hp->_size++] = x;AdjustUp();
首先将数据放在数组的最后一个位置上。 所以需要对这个数据进行向上调整。
假设此时我们需要得到一个大堆。
5.2.1向上调整
void AdjustUp(HPDataType* a, int child);
首先我们知道,使用数组在逻辑上构成完全二叉树时。父亲和孩子的下标是有一定规律的。
//左孩子 = 父亲 * 2 + 1 //右孩子 = 父亲 * 2 + 2
由于是向上调整,我们知道的参数只有孩子的下标,又因为整数除法,很容易我们就可以知道父亲节点的下标。
int parent = (child - 1) / 2;//整数除法
如果孩子更大就和父亲交换位置。可是交换位置后仍然可能存在不是大堆的情况,所以需要继续调整,直到是大堆为止。
void AdjustUp(HPDataType* a, int child) { //左孩子 = 父亲 * 2 + 1 //右孩子 = 父亲 * 2 + 2 int parent = (child - 1) / 2;//整数除法while (child > 0) {if (a[child] > a[parent]) {Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else {break;}}
}
当child不是0的时候,就代表还可以继续比较,所以结束条件就是child等于0时。或者是parent大于等于0,因为如果parent小于了代表越界了,此时以及不能比较了。
Swap函数是用于交换的。
void Swap(HPDataType* a, HPDataType* b) {HPDataType tmp = *a;*a = *b;*b = tmp;
}
6.堆的删除
// 堆的删除
void HeapPop(Heap* hp);
堆的删除是指删除堆顶(根位置)的数据 。那么该如何删除呢?
假设一下有这样一个数组并且按照小堆的方式存放。
此时需要删除数据1。如果我们采用向前移动覆盖的方式进行删除。那么会得到一个新的堆
不难发现此时这个堆的关系全乱了,不再是小堆,而且我们无法轻易的进行更改。所以直接向前移动覆盖这个堆顶的数据是行不通的。
那么该如何删除数据呢?
将数组最后一个数据覆盖到第一个位置上就行了。
虽然此时的二叉树仍然不是一个小堆,但是只有堆顶的数据关系不正确而且。此时我们就需要用到向下调整算法来使其重新变成一个小堆。
5.2.2 向下调整
首先将最后一个数据放在第一个位置上。
//小堆
void HeapPop(Heap* hp)
{assert(hp);assert(hp->_size > 0);hp->_a[0] = hp->_a[hp->_size - 1];hp->_size--;AdjustDown(hp->_a, hp->_size, 0);
}
然后进行向下调整。
已知父亲的下标,很轻易可以知道左右两个孩子的下标。然后就是找到两个孩子的最小值与父亲比较。
为了更快速的找到最小的那个孩子的下标,我们可以采用假设法。首先假设左孩子最小。如果右孩子比左孩子小,就可以直接+1,因为右孩子的下标比左孩子刚好大1。
然后就是比较。参考一下代码。
void AdjustDown(HPDataType* a, int size, int parent) {int child = parent * 2 + 1;//假设左孩子最小while (child < size) {if (child + 1 < size && a[child] > a[child + 1]) {++child;}if (a[parent] > a[child]) {Swap(&a[parent],&a[child]);parent = child;child = parent * 2 + 1;}else {break;}}
}
7.取堆顶数据
这个很简单,就不过多讲解。
HPDataType HeapTop(Heap* hp) {assert(hp);assert(hp->_size > 0);return hp->_a[0];
}
只需要保证此时堆不为空即可。
8.堆的数据个数
int HeapSize(Heap* hp) {assert(hp);return hp->_size;
}
9.判读堆是否为空
bool HeapEmpty(Heap* hp)
{assert(hp);return hp->_size == 0;
}