在计算机科学中,堆是一种特殊的完全二叉树数据结构,常被用于实现优先队列等应用场景。本文将通过一个C语言示例,详细介绍如何实现一个基于数组的大顶堆及其基本操作——包括堆的创建、插入元素(入堆)、删除最大元素(出堆)以及利用这些操作实现堆排序算法。本代码实例主要涉及到了一个Heap
结构体的定义、堆的创建、元素的插入与删除,以及堆排序的整个过程。
创建项目,分别是heap.h,heap.c,test.c
堆的定义与结构体设计
堆是一种每个节点的值都大于或等于其子节点值的完全二叉树(大顶堆),或者每个节点的值都小于或等于其子节点值的完全二叉树(小顶堆)。在我们的示例中,我们实现了一个大顶堆。
如下代码所示:其中还包含了一些声明以及头文件的引用
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
#include <math.h>
typedef int DataType;
typedef struct Heap
{
DataType* data;
int maxSize;
int curSize;
}Heap;
Heap* createHeap(int maxSize);
void swap(Heap* heap, int pos1, int pos2);
void moveUp(Heap* heap, int curPos);
void insertHeap(Heap* heap, DataType data);
DataType popHeap(Heap* heap);
int moveDown(Heap* heap, int curPos);
这里,Heap
结构体包含三个成员:指向堆数据的指针data
、堆的最大容量maxSize
以及当前堆的实际大小curSize
。
有如下函数,分别是堆的操作函数,创建堆,插入元素(入堆),删除最大元素(出堆),上渗和下渗。
我们先给出堆排序的基本步骤:
堆排序是一种利用堆的性质进行排序的方法。其基本思想是:
- 先将待排序序列构造成一个大顶堆。
- 将堆顶元素(即最大值)与堆尾元素交换,此时序列末尾就是最大值。
- 将剩余的n-1个元素重新调整为大顶堆。
- 重复步骤2和3,直到整个序列有序。
在我们的代码中,通过不断调用popHeap
函数并输出,实现了从大到小的堆排序。注意,本文代码中的操作与堆排序的操作略有不同,主要在第二个步骤的实现上。
代码示例:
#include"heap.h"
Heap* createHeap(int maxSize)
{
//申请堆内存
Heap* heap = (Heap*)malloc(sizeof(Heap));
assert(heap);
//申请数组内存
heap->data = (DataType*)malloc((maxSize+1) * sizeof(DataType));
assert(heap->data);
//初始化
heap->maxSize = maxSize;
heap->curSize = 0;
}
//交换数组中(二叉树)的两个数据
void swap(Heap* heap, int pos1, int pos2)
{
DataType temp = heap->data[pos1];
heap->data[pos1] = heap->data[pos2];
heap->data[pos2] = temp;
}
//向上移动:传入元素的下标和堆
void moveUp(Heap* heap, int curPos)
{
//最极端的结束条件是移动到最上面(也就是下标为1的位置)
while (curPos!=1)
{
//得到父节点下标
int parentPos = curPos / 2;
//如果父节点小于该节点
if (heap->data[parentPos] < heap->data[curPos])
{
//交换俩个节点数据
swap(heap, curPos, parentPos);
//更新下标
curPos = parentPos;
}
else
{
//如果插入元素小于父节点值,那么说明不用调整
return;
}
}
}
//入堆(生成堆)
void insertHeap(Heap* heap,DataType data)
{
//如果堆满
if (heap->curSize == heap->maxSize)
{
printf("堆满\n");
return;
}
//注意这里是先++,而后再存入数据(因为下标为0的位置不存放数据)
//目的是保证下标和完全二叉树的标号保持一致
heap->data[++(heap->curSize)] = data;
//插入之后向上渗透,调整位置
moveUp(heap, heap->curSize);
}
int moveDown(Heap* heap, int curPos)
{
//这里每次删除的是该堆中最大的(以大顶堆为例)
int Pos = curPos;
//左孩子和右孩子的下标
int Lchild = curPos * 2;
int Rchild = Lchild + 1;
//计算完全二叉树的深度
int depth = log2(heap->curSize) + 1;
//最极端的结束条件是移动到最下面(就是最下面的一层)并且不能让下标越界
while (log2(curPos) + 1 < depth && Rchild <= heap->curSize)
{
//选择两条路(两个孩子)中较大者,进行比较下渗
if (heap->data[Rchild] > heap->data[Lchild])
{
//由于是最大值下渗,直接交换就行,不用再考虑比较
swap(heap, curPos, Rchild);
//更新当前节点下标
curPos = Rchild;
}
else
{
//由于是最大值下渗,直接交换就行,不用再考虑比较
swap(heap, curPos, Lchild);
//更新当前节点下标
curPos = Lchild;
}
//更新孩子节点
Lchild = curPos * 2;
Rchild = Lchild + 1;
}
return curPos;
}
//出堆(向下渗透)
DataType popHeap(Heap*heap)
{
if (heap->curSize == 0)
{
return;
}
//保存最大值,后续会返回
DataType temp = heap->data[1];
//完成下渗操作
int curPos=moveDown(heap, 1);
//移动到底部之后,调整该节点位置至最后
//直接用原来最后一个节点的数据覆盖掉当前节点数据(是否破坏了原来的顺序呢?存疑)
//如果已经移动到最后一个,那么直接删除即可,再上渗就回去了
if (curPos != heap->curSize)
{
heap->data[curPos] = heap->data[heap->curSize];
//然后对这个节点上滤
moveUp(heap, curPos);
}
//数组伪删除
heap->curSize--;
return temp;
}
样例测试:
#include "heap.h"
int main()
{
Heap* heap = createHeap(7);
insertHeap(heap, 50);
insertHeap(heap, 16);
insertHeap(heap, 41);
insertHeap(heap, 56);
insertHeap(heap, 1);
insertHeap(heap, 23);
insertHeap(heap, 10);
printf("堆遍历\n");
for (int i = 1; i <= heap->curSize; i++)
{
printf("%d ", heap->data[i]);
}
printf("\n");
printf("堆排序:\n");
while (heap->curSize != 0)
{
printf("%d ", popHeap(heap));
}
return 0;
}
测试结果: