首先,什么是树,数学定义是连通且不含回路的图。通俗点来讲,有点像一棵树倒过来,根在上,也在下。
(更加严谨的定义,请学习离散数学相关知识)
而二叉树就是每个节点的度数最多不大于2。
堆便是一种完全二叉树。不过堆中某个节点的值总是不大于或不小于其父节点的值。父节点总是大于或等于子节点称之为大堆,反之,若父节点总是小于或等于其子节点称之为小堆。
那么我们在计算机中怎么表示一个堆呢?
答案是数组。
想要知道如何用一个数组来表示堆,首先我们得知道父节点和子节点的关系。
我们假设父节点是parent,左子节点是leftchild,又子节点是rightchild。
parent = (child-1)/2; //这里的孩子,左孩子和右孩子随便哪一个都可以
leftchild = parent*2+1;
rightchild = parent*2+2;
这样我们就能在数组中通过这样的关系来找到每一个节点了。
//相关定义
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
//初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
//销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity;
}
//直接创建,不需初始化
void HeapCreate(HP* php, HPDataType* array, int size)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * size);
if (php->a == NULL)
{
perror("malloc fail");
exit(-1);
}
memcpy(php->a, array, sizeof(HPDataType) * size);
php->size = php->capacity = size;
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(php->a, size, i);
}
}
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
//向上调整
AdjustUp(php->a, php->size - 1);
}
由于增加那个数是直接插在最后面的,不一定能构成堆,所以需要向上调整。
void AdjustUp(HPDataType* a, int child)
{
assert(a);
//找到父节点
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;
}
}
}
删除数一般是删除堆顶元素
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);
}
删除数是将堆顶元素和最后一个元素交换,然后删除掉最后一个元素。然后通过向下调整使之重新构成堆。
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[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//取堆顶元素
HPDataType 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 == 0;
}
堆排是首先把数据建成堆,然后再进行排序。
//向上调整建堆的思想是第一个数已经是堆了,从第二个数开始向上调整建堆
//最终的时间复杂度是O(N*logN)
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
//向下调整建堆的思想是从最后一个节点的父节点开始向下调整,
//然后-1找到前一个父节点,再向下调整,不断循环,找到根节点
//时间复杂度是O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
所以就建堆而言,向下调整建堆是明显由于向上调整建堆的。
升序建大堆,降序建小堆
//这里假设是升序,建大堆
//通过把堆顶和最后一个元素交换,因为堆顶元素是最大的,
//然后向下调整建堆
//再把次大的放到倒数第二的位置
//通过不断的循环,便把大的全部放到后面,这样便做到了升序
//若想降序,建小堆即可,这样是把小的元素放到后面
//整体的时间复杂度是O(N*logN)
//冒泡排序的时间复杂度是O(N^2),可见堆排是效率很高的算法
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
//直接传数组地址和数组元素的个数过来
void HeapSort(int* a, int size)
{
//建堆
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, size, i);
}
//排序
//升序建大堆,降序建小堆
//这里是升序建大堆
int end = size - 1;
while (end)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
本文介绍了堆的增删查改和堆排序,希望对大家有所帮助。