堆的概念
物理与逻辑结构的转换
图片上的二叉树是一颗完全二叉树,并且它还是一颗满二叉树。而数组能完美的表示像完全二叉树这样的结构。
从根节点开始,根节点为第一层,从上往下,从左往右,将数据存储到数组中。二叉树第i层的元素个数为2的(i - 1)次方,知道每层二叉树的元素个数,就能把数组中的数据转化为二叉树的形式。
第一层二叉树有2的(1 - 1)次方,1个元素,一层一层地将数组转化成二叉树
堆的性质
1.堆总是一颗完全二叉树 2.堆的节点总是不大于或不小于其父节点
若一颗二叉树根节点的值小于其孩子节点,并且该树满足堆的性质,该树称作小堆。反之根节点大于其孩子节点,该树称作大堆。
堆的实现
堆在物理上是一个数组,虽然在逻辑上不是顺序表,但可以用顺序表的结构表示堆。
堆结构的声明
typedef int HDataType;
typedef struct Heap
{
HDataType* data;
size_t size;
size_t capacity;
}Heap;
堆的基础接口
void HeapInit(Heap* php);
void HeapDestory(Heap* php);
void HeapPush(Heap* php, HDataType(Heap* php);
void HeapPop(Heap* php);
HDataType HeapTop(Heap* php);
bool HeapEmpty(Heap* php);
size_t HeapSize(Heap* php);
堆的初始化与销毁
void HeapInit(Heap* php)
{
assert(php);
php->size = 0;
php->capacity = 0;
php->data = NULL;
}
void HeapDestory(Heap* php)
{
assert(php);
free(php->data);
php->capacity = 0;
php->data = NULL;
}
堆的Push与Pop
假设往图片中的小堆插入一个9,将9插入数组的最后一位,插入数据时要先检查数组的空间是否足够存储,检查扩容。找到9的父节点,比较其与父节点的大小,如果父节点大于9,则把父节点与9交换,交换后再与交换后的父节点比较,判断是否要再次交换。最坏的情况就是交换到根节点
父节点与子节点的下标关系,(子节点下标 - 1) / 2得到父节点的下标 C的下标为2,- 1 再 / 2得到0,说明A是C的父节点,B的下标是1,-1 再 /2得到0,说明A是B的父节点。 交换节点时,只会影响到插入节点的祖先节点。
void Swap(HDataType* pa, HDataType* pb)
{
HDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void AdjustUp(Heap* php, size_t child)
{
size_t parent = (child - 1) / 2;
while(parent > 0)
{
if (php>data[child] < php->data[parent])
{
Swap(&php>data[child], &php->data[parent]);
child = parent;
parent = (child - 1 ) / 2;
}
else
{
break;
}
}
}
void HeapPush(Heap* php, HDataType x)
{
assert(php);
if (php->size == php->capacity)
{
size_t newCapacity = (php->capacity == 0) ? (4 : php->capacity) * 2;
HDataTpye* tmp = (HDataTpye*)realloc(php->data,sizeof(HDataTpye) * newCapacity);
if (!tmp)
{
printf("realloc fail\n");
exit(-1);
}
php->data = tmp;
php->capacity = newCapacity;
}
php->data[php->size] = x;
php->size++;
AdjustUp(php, php->size - 1);
}
删除堆顶元素,不能将后面的数据往前覆盖,删掉第一个节点,这样操作会打乱堆中元素的父子关系 如图片中的小堆,逻辑上的表示如下 将后面的元素删除,得到的小堆是 3的父节点大于3,很明显不满足小堆的性质。所以删除堆顶元素得用其他算法。
将堆顶元素与数组的最后一个元素交换,再将堆的size减1,此时的堆的堆顶不满足堆的性质,将堆顶向下调整:找到两个子节点中小的那个,将两者交换,再找交换后两子节点中小的那个,再交换,知道子节点中小的那个大于自身。
以上作为一个循环,当其没有子节点时,说明到了叶节点,循环结束
细节:两个子节点中,可能只有一个左子节点,所以要判断右节点是否存在,否则会出现越界访问
void AdjustDown(Heap* php, size_t size, size_t root)
{
size_t parent = root;
size_t child = root * 2 + 1;
while (child < size)
{
if (php->data[child + 1] && \
php->data[child + 1] < php->data[child])
{
child++;
}
if (php->data[parent] > php->data[child])
{
Swap(&php->data[parent], &php->data[child]);
parent = child;
child = parnet * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(Heap* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->data[0], &php->data[php->size - 1]);
AdjustDown(php->data, php->size, 0);
}
堆的判空,堆顶元素的返回与长度的返回
三个接口较简单
bool HeapEmpty(Heap* php)
{
assert(php);
return (php->size == 0);
}
HDataTpye HeapTup(Heap* php)
{
assert(php);
assert(php->size > 0);
return php->data[0];
}
HDataType HeapSize(Heap* php)
{
assert(php);
return php->size;
}
堆排序
堆顶元素总是堆中最小元素,若要将数组以升序的顺序排序,每次只要取出堆顶元素,取出后,调用堆的Pop接口,Pop数据,此时Pop会调整堆中元素的顺序,使得堆顶元素是最小的数,再取堆顶数据,再调Pop接口…
void HeapSort(HDataType* arr, size_t size)
{
Heap hp = { 0 };
HeapInit(&hp);
for (int i = 0; i < size; i++)
{
HeapPush(&hp, arr[i]);
}
for (int i = 0; i < size; i++)
{
arr[i] = HeapTop(&hp);
HeapPop(&hp);
}
}
运行结果如下
|