堆的那些事
堆的概念及结构
1.1 堆的定义
如果n个元素的序列{k0,k1,…,k(n-1)},
把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,
当且仅当满足下列条件的时候:
(1)ki >= k(2i) 且 ki >= k(2i+1) 或 (2) ki <= k(2i) 且 ki <= k(2i+1)(1<=i<=(n/2))
则称为大堆(或小堆)
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
1.2 堆的存储结构
我们随便给出一组数据:{97,76,65,49,49,13,27,38}
大家一定迫不及待的想知道堆是怎么建立的了吧,别急,下面我们就讲讲堆的实现:
2.堆的实现
2.1 堆向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。
向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {47,15,19,18,28,34,65,49,25,37};
首先我们先画出这个堆的逻辑展示图。
不难发现,此时以47为根节点的左子树和右子树,都满足小堆的性质,只有根节点不满足,所以此时需要向下调整到合适的位置即可形成堆。
那么我们怎么调整呢?
首先以堆顶的元素47和其左右子树的根节点进行比较,如下图(a)所示,由于左子树的值小于右子树的值且小于根节点的值,则将47和15交换,如下图(b)所示,由于47替换了15之后,破坏了左子树的“堆”,则需要进行类似的调整,直至叶子结点,调整后的结果如下图(d)所示,此时新堆满足小堆的性质。
2.2 堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。
根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
举例:建大堆
int array[] = {11,38,83,09,27,96};
首先初始化堆:
步骤一:先从倒数的第一个非叶子节点的子树开始调整,也就是83这个节点。
根节点83小于节点96的值,所以交换83和96。如下图所示:
步骤二:下面调整节点38,节点09小于节点27,但是节点27小于节点38,所以不需要调整。
接下来调整节点11,节点38小于节点96,且节点96大于节点11,所以交换节点11和节点96,如下图所示:
步骤三:不难发现,因为节点11和节点96交换后,破坏了原来的堆的结构,所以此时需要向下调整,重新构成堆。
因为节点11小于节点83,所以需要交换节点11和节点83。如下图所示:
此时大堆就构建完成了。
让我们通过下图再回顾一下大根堆构造的过程,加深一下印象。
2.3 建堆的时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
因此:建堆的时间复杂度为O(N)。
思考:
如果我们是先向上调整建堆,那么建堆的时间复杂度是多少?
下面我们公布答案:
可见向上调整建堆这个方法的时间复杂度高,所以我们一般选择向下调整建堆。
2.4 堆的插入
1.先将元素插入到堆的末尾,即最后一个孩子之后。
2.插入之后如果堆的性质遭到破坏,将新插入的节点顺着双亲节点往上调整到合适的位置即可。
举例:将100插入下面的大根堆
int array[] = {96,38,83,09,27,11};
具体的调整步骤如下:
2.5 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
举例:将100删除
int array[] = {100,38,96,09,27,11,83};
具体的调整步骤如下:
3.堆的代码实现
typedef int HeapDataType;
typedef struct Heap
{
HeapDataType* a;
int size;
int capacity;
}HP;
//堆的初始化
void HeapInit(HP* php);
// 堆的销毁
void HeapDestory(HP* php);
//向上调整建堆
void AdjustUp(HeapDataType* a,int child);
//向下调整建堆
void AdjustDown(HeapDataType* a,int n, int parent);
// 堆的插入
void HeapPush(HP* php, HeapDataType x);
// 堆的打印
void HeapPrint(HP* php);
// 堆的删除
void HeapPop(HP* php);
//取堆头的元素
HeapDataType HeapTop(HP* php);
// 堆的数据个数
int HeapSize(HP* php);
// 堆的判空
bool HeapEmpty(HP* php);
// 堆的构建
void HeapCreate(HP* php, HeapDataType* a, int n);
//堆排序
void HeapSort(int* a, int n);
//Top-k问题
void PrintTopK(int* a, int n, int k);
3.1 堆的初始化
接口声明:
void HeapInit(HP* php);
代码实现:
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
3.2 堆的销毁
接口声明:
void HeapDestory(HP* php);
代码实现:
void HeapDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
3.3 堆向上调整算法
接口声明:
void AdjustUp(HeapDataType* a,int child);
代码实现:
void AdjustUp( HeapDataType* 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;
}
}
}
3.4 堆向下调整算法
接口声明:
void AdjustDown(HeapDataType* a,int n, int parent);
代码实现:
void AdjustDown( HeapDataType* a,int n, int parent)
{
assert(a);
int maxchild = parent * 2 + 1;
while (maxchild < n)
{
// 确认child指向大的那个孩子
if (a[maxchild + 1] > a[maxchild] && maxchild+1 < n)
{
maxchild++;
}
// 1、孩子大于父亲,交换,继续向下调整
// 2、孩子小于父亲,则调整结束
if (a[parent] < a[maxchild])
{
Swap(&a[parent], &a[maxchild]);
parent = maxchild;
maxchild = parent * 2 + 1;
}
else
{
break;
}
}
}
3.5 堆的插入
接口声明:
void HeapPush(HP* php, HeapDataType x);
代码实现:
void HeapPush(HP* php, HeapDataType x)
{
assert(php);
//增容
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType)* newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
HeapPushUp(php, php->a,php->size - 1);
}
3.6 堆的打印
接口声明:
void HeapPrint(HP* php);
代码实现:
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0;i < php->size;i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
3.7 堆的删除
接口声明:
void HeapPop(HP* php)
代码实现:
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size-1]);
php->size--;
HeapPopDown( &php->a,php->size, 0);
}
3.8 取堆头的元素
接口声明:
HeapDataType HeapTop(HP* php);
代码实现:
HeapDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
3.9 堆的数据个数
接口声明:
int HeapSize(HP* php)
代码实现:
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
3.10 堆的判空
接口声明:
bool HeapEmpty(HP* php);
代码实现:
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
3.11 堆的构建
接口声明:
void HeapCreate(HP* php, HeapDataType* a, int n);
代码实现:
void HeapCreate(HP* php, HeapDataType* a, int n)
{
assert(php);
php->a = (HeapDataType*)malloc(php->a, sizeof(HeapDataType) * n);
if (php->a == NULL)
{
perror("realloc fail");
exit(-1);
}
memcpy(php->a, a, sizeof(HeapDataType) * n);
php->size = php->capacity = n;
// 建堆算法
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
HeapPopDown(a, n, i);
}
}
3.12 堆排序
接口声明:
void HeapSort(int* a, int n);
代码实现:
void HeapSort(int* a, int n)
{
//升序:建大堆
//降序:建小堆
//1.向上调整建大堆 -- O(N*logN)
/*for (int i = 1;i < n - 1;++i)
{
AdjustUp(a, i);
}*/
//2.向下调整建大堆
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;
}
}
3.13 Top-k问题
接口声明:
void PrintTopK(int* a,int n, int k);
代码实现:
void TestTopK(int* a,int n, int k)
{
int* minHeap = (int*)malloc(sizeof(int)*n);
if(minHeap)
{
perror("malloc fail");
return ;
}
//前k个先进堆
for(int i = 0;i<k;i++)
{
minHead[i] = a[i];
}
//找topK建小堆
//前K个先建小堆
for (int i = (k-1-1)/2;i >=0;--i)
{
AdjustDown(minHeap, k, i);
}
for(int i = k;i<n;i++)
{
if (minHeap[0] < a[i])
{
minHeap[0] = n;
AdjustDown(minHeap, k, 0);
}
}
Print(minHeap,5);
free(minHead);
minHead = NULL;
}
原码
结语:
感谢大家看到这里,如有错误,希望各位大佬多多指正!
祝大家心情愉悦!