堆
堆是一个按照完全二叉树顺序存储方式在一个二维数组中的数据结构,其性质为
-
堆中的某个结点总是不大于或不小于父节点的值
-
堆是一个完全二叉树
每个结点大于父节点的叫最小堆,每个结点小于父节点的叫最大堆
这就是一个最小堆,因为每个父节点都小于对应的子节点。
比如说1
小于5
和7
,5
小于6
和9
…
堆的根节点总是放置最小或最大的结点,但是其他结点的排序不一定为如此,每个结点只能说是自己的子树上最小或最大的结点。
堆可以构建一个优先序列,也可以进行堆排序。
堆可以利用一个数组进行存储,减少了空间浪费。
比如说
int a[] = [1,5,7,6,9,8,10,8,9,12]
假设结点位置为i则其子节点分别就为child1 = (i * 2)+ 1,child2 = (i * 2)+ 2
下面这道题就用到了这个性质:
一颗完全二叉树上有1001个结点,其叶子结点的个数是( )
A.251 B.500 C.501 D.1001
要判断叶子结点的个数,由上面的公式我们知道每个父亲结点和其子结点的位置编号关系为child = 2 * parent + 1, child2 = 2 * parent + 2,所以从编号500开始的节点就没有孩子节点了, 都为叶子节点,故叶子节点位置[500, 1000],共501个。 所以答案为C。
堆的基本结构:
typedef int DataType;
typedef struct Heap {
DataType* a;
size_t size;
size_t capacity;
}HP;
堆的基本操作:
void HeapInit(HP* php);//初始化堆
void HeapDestroy(HP* php);//销毁堆
void HeapPush(HP* php, DataType x);//向堆中插入元素
void HeapPop(HP* php);//删除堆中最大或最小的元素
void AdjustUp(DataType* a, size_t child);//向上调整堆
void AdjustDown(DataType* a, size_t size, size_t root);//向下调整堆
DataType HeapTop(HP* php);//堆的根元素
void HeapPrint(HP* php);//打印堆中元素
bool HeapEmpty(HP* php);//判断堆是否为空
void Swap(DataType* pa, DataType* pb);//交换元素
size_t HeapSize(HP* php);//堆的大小
堆的初始化:
void HeapInit(HP* php) {
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
堆的销毁:
void HeapDestroy(HP* php) {
free(php->a);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
堆的插入:
我们通过一个插入例子来看看插入操作的细节。我们将数字 3
插入到这个堆中:
数组变为[1,5,7,9,10,3]
我们需要对数组进行改变使其满足最小堆。
插入一个数据然后与其父节点进行比较如果比父节点小就交换结点,直到满足最小堆。
代码如下:
void AdjustUp(DataType* a, size_t child) {
size_t 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 HeapPush(HP* php, DataType x) {
assert(php);
if (php->size == php->capacity) {
size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
DataType* tmp = realloc(php->a, sizeof(DataType) * newCapacity);
if (tmp == NULL) {
printf("realloc fail\n");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
++php->size;
AdjustUp(php->a, php->size - 1);
}
删除根节点:
对这个小堆进行pop操作,首先我们将根节点和尾结点交换
此时为了保持最小堆我们需要对这个堆整体进行调整
最后得到一个最小堆
删除及向下调整代码如下:
void AdjustDown(DataType* a, size_t size, size_t root) {
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size) {
if (child + 1 < size && a[child + 1] < a[child]) {
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
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);
}
得到堆顶元素:
DataType HeapTop(HP* php) {
assert(php);
assert(php->size > 0);
return php->a[0];//返回堆顶元素
}
判断堆是否为空:
bool HeapEmpty(HP* php) {
assert(php);
return php->size == 0;
}
堆的大小:
size_t HeapSize(HP* php) {
assert(php);
return php->size;
}
堆排序
由上面堆的性质我们可以知道堆每个结点一定是他的子树中最小或最大的结点,我们可以运用这个性质进行一个排序操作。
首先我们进行插入操作,这样可以保证这个数组会是一个最小堆,当堆不为空时一直取栈顶元素,这样就可以产生一个由小到大的数列。
算法复杂度:
push操作的算法复杂度为O(logn), 堆排序中push操作的算法复杂度就是O(nlogn), pop操作同理,所以堆排序的算法复杂度就为O(nlogn)。
void HeapSort(int* a, int size) {
HP hp;
HeapInit(&hp);
for (int i = 0; i < size; i++) {
HeapPush(&hp,a[i]);
}
int j = 0;
while (!HeapEmpty(&hp)) {
a[j] = HeapTop(&hp);
j++;
HeapPop(&hp);
}
HeapDestroy(&hp);
}