堆
前景提要
在学习堆之前,需要我们对二叉树有一定的理解。
二叉树,是指度为2的树。在二叉树中有两种比较特殊的类型:满二叉树和完全二叉树
满二叉树:一个二叉树,如果每一个层的节点数都达到最大值,则这个二叉树就是满二叉树,也就是说,如果一个二叉树的层数为k,且节点总数是2^k-1,则它就是满二叉树
完全二叉树:完全二叉树就是效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点——对应时称之为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树
其中满二叉树又是完全二叉树的特殊类型
ps:完全二叉树的最后一层的叶节点必须从左到右连续,若不连续则不能称之为完全二叉树。
而堆便是一种完全二叉树。
除此之外,堆与其他完全二叉树还存在本质上的差别,这里我们就要来介绍堆的类型。
堆分为大堆和小堆,大堆为树上的任何一个父节点都要大于它的子节点,而小堆就是树上的任何一个父节点都要小于它的子节点。这便是堆与其他完全二叉树的差异,若不能满足大堆或小堆成立的条件,那么这个完全二叉树就不能称之为堆。
结构表示
二叉树可以用两种结构表示:一种为数组存储,另一种为链式存储
这里我们就介绍数组存储的方法:
用数组存储,此时的逻辑结构和物理结构(这里的逻辑结构为想象出来的,物理结构为内存中实实在在存储的结构)分离。该存储方式的优点为可以用下标来表示父子节点之间的关系
但是此方法具有局限性:只比较适合用于满二叉树和完全二叉树的类型,若不属于这类型的树,可以使用这个方法,但是会造成很多空间上的浪费,得不偿失。
作用
这里我们已经对堆有了一些简单的了解,那我们实现堆之后可以用它来做什么呢?
接下来这里将会讲解两种堆的作用:堆排序和Top k问题
堆排序
相信在学习堆排序之前,大家应该都学过一种排序:冒泡排序。
在此可能会产生一些疑惑:我已经学习了冒泡排序,为什么还要学别的排序呢?
这里就涉及到了效率的问题。冒泡排序作为一种排序,它的实现很简单,但运行起来的效率极低,在现实生活中不具有编写的意义,相比于现实的运用,教学的意义更大。因此我们要学习更多的排序算法,堆排序便是其中的一种。在此我就不多加赘述,具体的实现方法在下文的“堆排序”块。
Top k 问题
Top K问题即为从很多个数据中选取其中最大的K个数,而我们可以利用堆的性质来解决这个问题,我们可以先建立一个含有k个数据的小堆,然后再依次从取数据与小堆的堆顶进行比较,若该数据比堆顶的数据大,则让堆顶的数据与其交换,再进行向下调整。这样我们就能找到最大的K个数据。
void CreateNDate()
{
const char* file = "data.txt";
FILE* fp = fopen(file, "w");
int n = 10000;
srand(time(0));
if (fp == NULL)
{
perror("fopen fail");
return;
}
for (int i = 0; i < n; i++)
{
int x = rand() % 100000;
fprintf(fp, "%d\n", x);
}
fclose(fp);
}
void PrintTopK(int k)
{
const char* file = "data.txt";
FILE* fl = fopen(file, "r");
if (fl == NULL)
{
perror("fopen fail");
return;
}
HPDataType* date = (HPDataType*)malloc(sizeof(HPDataType) * k);
if (date == NULL)
{
perror("malloc fail");
return;
}
for (int i = 0; i < k; i++)
{
fscanf(fl, "%d", &date[i]);
}
for (int i = (k - 2) / 2; i > 0; i--)
{
AdjustDown(date, k, i);
}
while (!feof(fl))
{
HPDataType x;
fscanf(fl, "%d", &x);
if (x > date[0])
{
date[0] = x;
AdjustDown(date, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", date[i]);
}
free(date);
date = NULL;
fclose(fl);
}
实现
heap.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HPInit(HP* p);
void HPDestroy(HP* p);
void HPPush(HP* p, HPDataType x);
void HPPop(HP* p);
int HPSize(HP* p);
bool HPEmpty(HP* p);
HPDataType HPTop(HP* p);
void AdjustUp(HPDataType* a,int child);
void AdjustDown(HPDataType* a,int n,int parent);
heap.c
#include "heap2.h"
void HPInit(HP* p)
{
assert(p);
p->a = NULL;
p->size = p->capacity = 0;
}
void HPDestroy(HP* p)
{
assert(p);
free(p->a);
p->size = p->capacity = 0;
p->a = NULL;
}
void Swap(HPDataType* n, HPDataType* m)
{
int tmp = *n;
*n = *m;
*m = tmp;
}
//升序建大堆
void AdjustUp(HPDataType* a, int child)
{
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 HPPush(HP* p, HPDataType x)
{
assert(p);
if (p->size == p->capacity)
{
int newcapacity = p->capacity == 0 ? 4 : p->capacity * 2;
HPDataType* pre = (HPDataType*)realloc(p->a, sizeof(HPDataType) * newcapacity);
if (pre == NULL)
{
perror("realloc fail");
return;
}
p->a = pre;
p->capacity = newcapacity;
}
p->a[p->size] = x;
p->size++;
AdjustUp(p->a, p->size - 1);
}
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while(child < n)
{
if (a[child] < a[child + 1] && child + 1 < n)
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HPPop(HP* p)
{
assert(p);
assert(p->size > 0);
Swap(&(p->a[0]), &(p->a[p->size - 1]));
p->size--;
AdjustDown(p->a, p->size, 0);
}
bool HPEmpty(HP* p)
{
assert(p);
return p->size == 0;
}
int HPSize(HP* p)
{
assert(p);
return p->size;
}
HPDataType HPTop(HP* p)
{
assert(p);
return p->a[0];
}
堆排序
建堆
在上述对堆的学习中,我们学到了两种建堆方法:一个是向上调整建堆,另一个是向下调整建堆。那么我想你应该存在一些疑惑:为什么会有两种建堆方法?它们有什么差别?哪个相比之下更具优点?
那么这里就继续往下一一介绍:其实这两种建堆的方法存在的本质差别便是我们经常说的时间复杂度的不同
向上调整建堆
对于向上调整建堆,它的时间复杂度为O(N*logN)
由此可见,向下调整建堆比向上调整建堆所用的时间更少,所以我们更提倡使用向下调整建堆
向下调整建堆
而对于向下调整建堆,它的时间复杂度为O(N)
排序
void HeapSort(HPDataType* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}