努力提升自己,永远比仰望别人更有意义
目录
1 二叉树的顺序结构
2 堆的概念及结构
3 堆的实现
3.3堆的插入
3.4 堆的删除
3.5 堆的代码实现
4 堆的应用
4.1 堆排序
4.2 TOP-K问题
总结:
1 二叉树的顺序结构
2 堆的概念及结构
3 堆的实现
3.1 堆向下调整算法
int array [] = { 27 , 15 , 19 , 18 , 28 , 34 , 65 , 49 , 25 , 37 };
具体代码:
void AdjustDown(int* a, int parent, int sz)
{
assert(a);
int child = parent * 2 + 1;
while (child < sz)
{
if (child + 1 < sz && a[child + 1] > a[child])//建立小堆 a[child + 1] < a[child]
child++;
if (a[child] > a[parent])//建立小堆 <
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
3.2 堆向上调整算法
堆的向上调整算法往往与push相搭配,push完一个数据就将该数据向上调整,这样就能够保证堆的结构不会被破坏。
具体图例:
代码实现:
void AdjustUp(int* a, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child>0)//用parent>=0也行,只是这样的话就不是正常结束的了
{
if (a[child] > a[parent])//建小堆 <
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
我们不难发现一个数据向上调整或者向下调整的时间复杂度都为logN.
3.3堆的插入
具体图例:
代码实现:
void HeapPush(Heap* php, HeapDataType x)
{
assert(php);
if (php->capacity == php->sz)
{
int newcapacity = php->a == NULL ? 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->sz] = x;
php->sz++;
//向上调整算法,保证建立的是堆(这里以建小堆为例)
AdjustUp(php->a, php->sz - 1);//第二个参数传的是push这个数据的下标
}
3.4 堆的删除
假设建小堆,要pop掉最小的一个数值(堆顶),要让下面的结构继续保持小堆结构就不能只将数据向前挪动一位,否则堆的结构将会被破坏。正确做法是将堆顶的数据与最后一个数据交换,然后重新向下建堆,再pop掉堆尾数据。
代码实现:
void HeapPop(Heap* php)
{
assert(php);
assert(php->sz > 0);
//假设建小堆,要pop掉最小的一个数值(堆顶),要让下面的结构继续保持小堆结构就不能只将数据向前挪动一位,
//否则堆的结构将会被破坏。正确做法是将堆顶的数据与最后一个数据交换,然后重新向下建堆,再pop掉堆尾数据。
Swap(&php->a[0], &php->a[php->sz - 1]);
php->sz--;
AdjustDown(php->a, 0, php->sz);
}
3.5 堆的代码实现
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int HeapDataType;
typedef struct Heap
{
HeapDataType* a;
int sz;
int capacity;
}Heap;
void HeapInit(Heap* php);
void HeapPush(Heap* php, HeapDataType x);
void HeapPop(Heap* php);
HeapDataType HeapTop(Heap* php);
int HeapSize(Heap* php);
bool HeapEmpty(Heap* php);
void HeapDestroy(Heap* php);
void HeapPrint(Heap* php);
void AdjustDown(int* a, int parent, int sz);
void AdjustUp(int* a, int child);
void Swap(HeapDataType* p1, HeapDataType* p2);
void HeapInit(Heap* php)
{
assert(php);
php->a = NULL;
php->capacity = php->sz = 0;
}
void Swap(HeapDataType* p1, HeapDataType* p2)
{
HeapDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(int* a, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child>0)//用parent>=0也行,只是这样的话就不是正常结束的了
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
void HeapPush(Heap* php, HeapDataType x)
{
assert(php);
if (php->capacity == php->sz)
{
int newcapacity = php->a == NULL ? 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->sz] = x;
php->sz++;
//向上调整算法,保证建立的是堆(这里以建小堆为例)
AdjustUp(php->a, php->sz - 1);//第二个参数传的是push这个数据的下标
}
void AdjustDown(int* a, int parent, int sz)
{
assert(a);
int child = parent * 2 + 1;
while (child < sz)
{
if (child + 1 < sz && 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(Heap* php)
{
assert(php);
assert(php->sz > 0);
//假设建小堆,要pop掉最小的一个数值(堆顶),要让下面的结构继续保持小堆结构就不能只将数据向前挪动一位,
//否则堆的结构将会被破坏。正确做法是将堆顶的数据与最后一个数据交换,然后重新向下建堆,再pop掉堆尾数据。
Swap(&php->a[0], &php->a[php->sz - 1]);
php->sz--;
AdjustDown(php->a, 0, php->sz);
}
HeapDataType HeapTop(Heap* php)
{
assert(php);
assert(php->sz > 0);
return php->a[0];
}
int HeapSize(Heap* php)
{
assert(php);
return php->sz;
}
bool HeapEmpty(Heap* php)
{
assert(php);
return php->sz == 0;
}
void HeapDestroy(Heap* php)
{
assert(php);
free(php->a);
php->capacity = php->sz = 0;
}
void HeapPrint(Heap* php)
{
assert(php);
for (int i = 0; i < php->sz; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
4 堆的应用
4.1 堆排序
口说无凭,这里我们可以通过准确的计算来算出他们各自的时间复杂度:
1 向上建堆:
这里我们都以满二叉树为例,时间复杂度算的只是一个大概值所以可以用满二叉树来代替完全二叉树。(假设数的高度为h)
第一层有2^0个结点,要向上调整0次;
第二层有2^1个结点,要向上调整1次;
第三层有2^2个结点,要向上调整2次;
…………………………
第h-1层有2^(h-2)个结点,要向上调整(h-2)次;
第h层有2^(h-1)个结点,要向上调整(h-1)次;
所以可得:
T(h)=2^1*1+2^2*2+……2^(h-2)*(h-2)+2^(h-1)*(h-1)
利用错位相减法很容易算出:
T(h)=2^h*(h-2)+2;
由于h=logN(大概值就行,不用太精确)
所以求得向上建堆的时间复杂度大概在:
T(N)=N*logN 这个数量级。
2 向下建堆:
八大排序之插入和选择排序
通过计算我们可以知道向下建堆的时间复杂度大概在:
T(N)=N 这个数量级。
所以我们选用向下建堆。
如果建小堆,最小数已经被选出来了,但是不能够pop掉最小数,否则堆结构将被破环,那么又要重新建堆,这样就没有了效率,所以我们要建大堆,将堆顶元素与最后一个元素交换再--数据个数,然后向下调整。
具体代码:
void HeapSort(HeapDataType* a, int sz) { //从最后一个结点的父亲开始建堆 for (int i = (sz - 1 - 1) / 2; i >= 0; i--) { AdjustDown(a, i, sz); } for (int i = sz-1; i>0; i--) { Swap(&a[0], &a[i]); AdjustDown(a, 0, --sz); } }
4.2 TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等。前k个最大的元素,则建小堆前k个最小的元素,则建大堆
具体代码:
//建立一个k个数的小堆,依次遍历数组,比堆顶元素大就替换,然后向下调整,最后堆中数据就是topk
//时间复杂度为:N+N*logk 空间复杂度为O(k)
int topk[5] = {0};
int i;
for (i = 0; i < 5; i++)
{
topk[i] = array[i];
}
//建小堆
for (i = (5 - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(topk, i, 5);
}
//遍历替换
for (i=5; i < sz; i++)
{
if (array[i] > topk[0])
{
topk[0] = array[i];
AdjustDown(topk, 0, 5);
}
}
for (i = 0; i < 5; i++)
printf("%d ", topk[i]);
//这种方法占据内存较小,比较优秀
总结:
文章中我们介绍了堆这种二叉树顺序结构,实现了堆并且将堆的两大比较重要的应用(堆排序和TopK问题)介绍了,这里面比较重要的就是向上/向下调整算法。后面链式二叉树以及相关OJ我们将放在下一篇文章来讲解,大佬们,我们下期再见!