文章目录
- 前言
- 一、堆
- 二、顺序存储
- 三、堆的实现
- 1.建堆
- 2.向堆中插入数据
- 3.删除堆顶的数据
- 4.其他对堆的操作
- 感谢阅读,如有错误请批评指正
前言
在数据结构(四):二叉树中,树是通过链式结构来实现的。在本文中,堆将通过顺序结构实现。同样是树,为什么实现时存储方式不同呢?堆又有哪些特殊的性质呢?
需要注意,本文介绍的堆和操作系统虚拟进程地址空间中的不同,前者是一种数据结构,后者是操作系统中管理内存的一块区域分段。
一、堆
如果有一个数据的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中。且满足以下性质:
(1)每个节点的值总是不大于或不小于其父节点的值;
(2)是一棵完全二叉树。
那么这就是一个堆。
如果根节点的值是堆中最小的值,那么这是一个小根堆(大堆)。
如果根节点的值是堆中最大的值,那么这是一个大根堆(小堆)。
二、顺序存储
顺序结构存储就是使用数组来存储,一般只适合表示完全二叉树,因为如果不是完全二叉树,存储时会有空间的浪费。由于堆实际上是一棵完全二叉树,所以堆可以使用数组来存储。
顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
(来自百度图片)
由图中可以看出,左侧一棵完全二叉树存储进一个数组时不存在空间浪费;而右侧非完全二叉树在存储时下标为3、6、7、8的位置没有数据,造成了空间的浪费。
三、堆的实现
堆的结构如下:
typedef int HPData;
typedef struct Heap
{
HPData* a;
int size;
int capacity;
}HP;
可以看到,堆的物理结构实际上是一个可动态增长的数组。
1.建堆
下面给出一个数组,逻辑上看作一个堆,现在把它调整成大堆。
int a[] = { 15, 18, 28, 34, 65, 19, 49, 25, 37, 27 };
调整的思路:从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点,就可以把整棵树调整成堆。
下面以上图为例进行调整:
(1)倒数第一个非叶子结点:65,左右子树的值都比它小(忽略空),不需要调整。
(2)倒数第二个非叶子结点:34,左子树的值比它小,右子树的值比它大,交换它与右子树的值。(新的堆如下)
以37为根节点的堆是大堆,继续找前一个非叶子节点。
(3)倒数第三个非叶子结点:28,左子树的值比它小,右子树的值比它大,交换它与右子树的值。(新的堆如下)
以49为根节点的堆是大堆,继续找前一个非叶子节点。
(4)倒数第四个非叶子结点:18,左右子树的值都比它大,这时选取左右子树中大的那个值与它交换,即交换它与右子树的值。(新的树如下,注意这是不是堆)
这时以18为根节点的堆显然不是大堆,所以需对这个结点再调整,调整逻辑同上。(新的堆如下)
(5)倒数第五个非叶子结点:15(访问到根结点,调整完根结点后结束),左子右树的值都比它大,这时选取左右子树中大的那个值与它交换,即交换它与左子树的值。(新的堆如下)
此时以15位根的子树不是大堆,按照上述逻辑继续调整。(最终的堆如下)
已经访问到根结点并将根结点调整完毕,调整结束。此时这个堆已经是一个大堆。
上面的调整方法叫做向下调整算法。
但是向下调整算法有一个前提:左右子树必须已经是一个堆,才能调整。
所以必须从倒数第一个非叶子结点开始调整,当调整到前面的非叶子结点时,由于它后面的非叶子结点都已经是大堆,所以这个结点的左右子树也都是大堆,就可以继续使用向下调整算法了。
代码中还用到完全二叉树的一个规律 (根结点是0时成立) :如果一个结点的下标为n,那么它的左孩子的下标为(2 * n +1),右孩子的下标为(2 * n +2),父结点的下标(n - 1)/ 2。
下面是代码:
代码如下(示例):
void Swap(HPData* a, HPData* b)
{
HPData temp = *a;
*a = *b;
*b = temp;
}
void AdjustDown(HPData* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child &#43; 1 < n && a[child &#43; 1] > a[child])
child&#43;&#43;;
if (a[child] > a[parent])
{
Swap(&a[parent], &a[child]);
parent &#61; child;
child &#61; parent * 2 &#43; 1;
}
else
break;
}
}
void HeapInit(HP* php, HPData* a, int n)
{
assert(php);
php->a &#61; (data*)malloc(sizeof(data)*n);
if (php->a &#61;&#61; NULL)
{
printf("malloc fail\\n");
exit(-1);
}
php->size &#61; n;
php->capacity &#61; n;
memcpy(php->a, a, sizeof(data)*n);
int i &#61; 0;
for (i &#61; (php->size - 1 - 1) / 2; i >&#61; 0; i--)
AdjustDown(php->a, php->size, i);
}
2.向堆中插入数据
向堆中插入数据时&#xff0c;不能简单的在php->a的末尾加上一个数据&#xff0c;必须要保证插入这个数据后的树仍是一个堆。
这里要用到向上调整算法。就是找到插入数据后最后一个数据的父结点&#xff0c;判断是否符合大堆的特征&#xff0c;如果不符合&#xff0c;交换。重复这一过程直到访问到根结点或者父结点的值大于孩子结点。
以上面的向下调整算法建好的堆为例演示插入数据&#xff1a;
&#xff08;1&#xff09;插入的数据40的父结点是27&#xff0c;40大于27&#xff0c;所以以27为根结点的子树不是大堆&#xff0c;交换27和40。&#xff08;结果如下&#xff09;
&#xff08;2&#xff09;继续向上调整&#xff0c;40的父结点是37&#xff0c;40大于37&#xff0c;所以以37为根结点的子树不是大堆&#xff0c;交换37和40。&#xff08;结果如下&#xff09;
&#xff08;3&#xff09;继续向上调整&#xff0c;发现此时已经访问到根结点&#xff0c;而且根所在的树已经是大堆&#xff0c;调整完毕。
代码如下&#xff1a;
代码如下&#xff08;示例&#xff09;&#xff1a;
void AdjustUp(int* a, int child)
{
if (child <&#61; 0)
return;
int parent &#61; (child - 1) / 2;
if (parent >&#61; 0 && a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
child &#61; parent;
AdjustUp(a, child);
}
else
return;
}
void HeapPush(HP* php, HPData x)
{
assert(php);
if (php->size &#61;&#61; php->capacity)
{
HPData* tmp &#61; (HPData*)realloc(php->a, php->capacity * 2 * sizeof(HPData));
if (tmp &#61;&#61; NULL)
{
printf("realloc fail\\n");
exit(-1);
}
php->a &#61; tmp;
php->capacity *&#61; 2;
}
php->a[php->size] &#61; x;
php->size&#43;&#43;;
AdjustUp(php->a, php->size - 1);
}
3.删除堆顶的数据
最简单的思路是把php->a中的数据从第二个开始整体前移一位&#xff0c;然后再向下调整&#xff0c;但是这样实现起来挪动数据需要时间&#xff0c;由于堆被打乱重新调堆又需要时间&#xff0c;时间复杂度太高。
这里采用一种巧妙的思路&#xff1a;交换堆中第一个和最后一个数据&#xff0c;再让php->size减一&#xff0c;这就相当于删除了第一个数据&#xff08;因为size减一后已经访问不到了&#xff09;&#xff0c;之后再对堆进行向下调整即可。这种思路不需要挪动数据&#xff0c;同时整个树中只有根节点可能不是堆&#xff0c;剩下的子树都仍是堆。时间复杂度很低。
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);
}
4.其他对堆的操作
下面的函数实现时较简单&#xff0c;此处不再赘述。
代码如下&#xff08;示例&#xff09;&#xff1a;
HPData HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
bool HeapEmpty(HP* php)
{
assert(php);
return (php->size &#61;&#61; 0);
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a &#61; NULL;
php->capacity &#61; 0;
php->size &#61; 0;
free(php);
}
感谢阅读&#xff0c;如有错误请批评指正