二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储
完全二叉树的顺序存储
非完全二叉树的顺序存储
可以看到,这样是一种对空间的浪费,所以普通的二叉树不适合用数组来存储
堆的概念及结构
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
- 堆中某个节点的值总是不大于或不小于其父节点的值
- 堆总是一棵完全二叉树
大根堆
小根堆
堆的实现
数据结构
typedef int DataType;
typedef struct Heap
{
DataType *data;
int size;
int capacity;
}Heap;
实现的接口
void Swap(DataType* a, DataType* b);
void AdjustDown(DataType* data, int size, int root);
//向下调整算法
void AdjustUp(DataType* data, int child);
//向上调整算法
void HeapCreate(Heap* hp, DataType* data, int size);
//创建堆
void HeapDestory(Heap* hp);
//销毁堆
void HeapPush(Heap* hp, DataType x);
//入堆
void HeapPop(Heap* hp);
//出堆
int HeapSize(Heap* hp);
//堆的数据个数
int HeapEmpty(Heap* hp);
//判断堆是否为空
void HeapPrint(Heap* hp);
//显示堆
DataType HeapTop(Heap* hp);
//取堆顶数据
向下调整算法
int arr[10] = {27, 15, 19, 18, 28, 34, 65, 49, 25, 37};
这里我们给出一个数组,首先将它化成完全二叉树的样子
如果我们要将它化为一个小堆就需要用到向下调整算法
这个算法的步骤图解如下
首先让27 与 15交换
27与18交换
27与25交换
这样,我们就构建出了一个小堆。
因为小堆的特性是所有的父节点都需要比他的子节点小,所以我们就可以通过判断,当存在子节点比父节点小的时候,让最小的子节点与父节点进行交换。
这就是向下调整算法的思路
代码实现如下:
void AdjustDown(int *arr, int size, int root)
{
int parent = root;
int child = parent * 2 + 1;
while(child < size)
{
if(child + 1 < size && arr[child] > arr[child + 1])
{
++child;
}
if(arr[child] < arr[parent])
{
int temp = arr[parent];
arr[parent] = arr[child];
arr[child] = temp;
}
else
break;
parent = child;
child = parent * 2 + 1;
}
}
堆的创建
但是一趟的向下调整算法并不能完成堆的创建,上面的那个数组仅仅是一个巧合
这里我们在给出一个数组
int arr[] = {34, 53, 36, 46, 726, 14, 86, 65, 27, 4};
执行一趟向下调整算法后
在第一步的判断后就终止了,根本无法创建初始堆。就算不在这里停止,这里构建的也仅仅是更改了一下数组的顺序,达不到建立初始堆的目的。
为什么呢?
因为向下调整算法有一个前提:左右子树必须是一个堆
所以我们如果要调整一个完全二叉树为初始堆,就必须保证所有的节点的左右子树都满足这一个前提,所以我们需要从最后一个节点的父节点开始,倒着进行向下调整算法,这样就可以保证前提的实现,创建初始堆
代码实现如下
void HeapCreate(Heap* hp, DataType* data, int size)
{
hp->data = (DataType*)malloc(size * sizeof(DataType));
memcpy(hp->data, data, size * sizeof(DataType));
hp->size = size;
hp->capacity = size;
for (int i = (size - 2) / 2; i >= 0; i--)
{
AdjustDown(hp->data, hp->size, i);
}
}
这样就创建出了初始堆
向上调整算法
在这里还需要讲到一个向上调整算法,算法的思路与向下调整算法类似,只不过是从最后一个节点开始,与它的父节点进行判断,如果比父节点小,则与父节点交换。
void AdjustUp(DataType* data, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (data[child] < data[parent])
{
Swap(&data[child], &data[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
堆的插入
为什么我们有了向下调整算法还需要用到向上调整算法呢?因为当我们在堆尾插入数据后,我们还需要将插入后的堆调整为小根堆的情况,但是如果这个时候我们直接使用一次向下调整算法,不仅不能将数组调整为小根堆,还会将数据打乱,毁坏他们原本的关系。就算按照建立初始堆的方法使用向下调整算法,效率也是极为的底下。而在这个位置我们直接从插入的开始使用向上调整算法,就可以最快的使这个插入点到达他应该到的位置,这样的效率也是最高的
void HeapPush(Heap* hp, DataType x)
{
if (hp->size == hp->capacity)
{
hp->capacity *= 2;
hp->data = (DataType*)realloc(hp->data, hp->capacity * sizeof(DataType));
}
hp->data[hp->size++] = x;
AdjustUp(hp->data, hp->size - 1);
}
堆的删除
这是指的是从堆顶开始删除。
如果要实现堆的删除,我们不能直接删除堆首,因为这样的话我们就需要移动后面所有的元素,并改变他们的结构,这样的时间复杂度是非常高的。
我们可以借用一个小技巧,先让堆首和堆尾进行交换,然后删除掉堆尾,再对堆进行一次向下调整算法,就可以实现。因为堆的结构并没有被改变,唯一的变化就是堆首换成了原来的堆尾,这样只需要一趟的向下调整算法就可以实现
void HeapPop(Heap* hp)
{
Swap(&hp->data[hp->size - 1], &hp->data[0]);
hp->size--;
AdjustDown(hp->data, hp->size, 0);
}
下面的都比较简单,直接看代码就行
堆的判空
int HeapEmpty(Heap* hp)
{
return hp->size ? 0 : 1;
}
返回堆顶
DataType HeapTop(Heap* hp)
{
return hp->data[0];
}
堆的大小
int HeapSize(Heap* hp)
{
return hp->size;
}
打印堆
void HeapPrint(Heap* hp)
{
int i = 0;
for (i = 0; i < hp->size; i++)
{
printf("%d ", hp->data[i]);
}
}
销毁堆
void HeapDestory(Heap* hp)
{
free(hp->data);
hp->data = NULL;
free(hp);
hp = NULL;
}
完整代码:
头文件:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
typedef int DataType;
typedef struct Heap
{
DataType *data;
int size;
int capacity;
}Heap;
void Swap(DataType* a, DataType* b);
void AdjustDown(DataType* data, int size, int root);
//向下调整算法
void AdjustUp(DataType* data, int child);
//向上调整算法
void HeapCreate(Heap* hp, DataType* data, int size);
//创建堆
void HeapDestory(Heap* hp);
//销毁堆
void HeapPush(Heap* hp, DataType x);
//入堆
void HeapPop(Heap* hp);
//出堆
int HeapSize(Heap* hp);
//堆的数据个数
int HeapEmpty(Heap* hp);
//判断堆是否为空
void HeapPrint(Heap* hp);
//显示堆
DataType HeapTop(Heap* hp);
//取堆顶数据
函数实现
#include "Heap.h"
void Swap(DataType* a, DataType* b)
{
DataType temp = *a;
*a = *b;
*b = temp;
}
void AdjustDown(DataType* data, int size, int root)
{
assert(data);
int parent = root;
int child = root * 2 + 1;
while (child < size)
{
if (child + 1 < size && data[child + 1] < data[child])
{
++child;
}
if (data[child] < data[parent])
{
Swap(&data[child], &data[parent]);
}
else
break;
parent = child;
child = parent * 2 + 1;
}
}
void AdjustUp(DataType* data, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (data[child] < data[parent])
{
Swap(&data[child], &data[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
void HeapCreate(Heap* hp, DataType* data, int size)
{
hp->data = (DataType*)malloc(size * sizeof(DataType));
memcpy(hp->data, data, size * sizeof(DataType));
hp->size = size;
hp->capacity = size;
for (int i = (size - 2) / 2; i >= 0; i--)
{
AdjustDown(hp->data, hp->size, i);
}
}
void HeapPush(Heap* hp, DataType x)
{
if (hp->size == hp->capacity)
{
hp->capacity *= 2;
hp->data = (DataType*)realloc(hp->data, hp->capacity * sizeof(DataType));
}
hp->data[hp->size++] = x;
AdjustUp(hp->data, hp->size - 1);
}
void HeapPop(Heap* hp)
{
Swap(&hp->data[hp->size - 1], &hp->data[0]);
hp->size--;
AdjustDown(hp->data, hp->size, 0);
}
int HeapEmpty(Heap* hp)
{
return hp->size ? 0 : 1;
}
DataType HeapTop(Heap* hp)
{
return hp->data[0];
}
int HeapSize(Heap* hp)
{
return hp->size;
}
void HeapPrint(Heap* hp)
{
int i = 0;
for (i = 0; i < hp->size; i++)
{
printf("%d ", hp->data[i]);
}
}
void HeapDestory(Heap* hp)
{
free(hp->data);
hp->data = NULL;
free(hp);
hp = NULL;
}