3.实现顺序结构二叉树
一般堆使用顺序结构的数组来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。
3.1 堆的概念与结构
如果有一个关键码的集合 K = {k0, k1, k2, …,kn−1},把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足: Ki<= K2∗i+1( Ki>= K2∗i+1且Ki<= K2∗i+2 ),i = 0、1、2...
,则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
小根堆示例 | 大根堆示例 |
---|---|
![]() | ![]() |
父节点小于等于孩子节点 | 父节点大于等于孩子节点 |
小堆堆顶是堆的最小值 | 大堆堆顶是堆的最大值 |
堆具有以下性质
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一棵完全二叉树。
二叉树性质
- 对于具有
n
个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0
开始编号,则对于序号为i
的结点有:
若
i > 0
,i
位置结点的父结点序号:(i-1)/2
;
i=0
,i
为根结点编号,无双亲结点若
2i+1 < n
,左孩子序号:2i+1
,2i+1 >= n
否则无左孩子若
2i+2 < n
,右孩子序号:2i+2
,2i+2 >= n
否则无右孩子
3.2 堆的实现
Heap.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//定义堆的结构---数组
typedef int HPDataType;
typedef struct Heap {
HPDataType* arr;
int size;//有效的数据大小
int capacity;//空间大小
}HP;
//堆的初始化
void HPInit(HP* php);
//堆的销毁
void HPDestroy(HP* php);
//交换函数
void Swap(int* x, int* y);
//堆的向上调整算法
void AdjustUp(HPDataType* arr, int child);
//往堆里面插入数据
void HPPush(HP* php, HPDataType x);
//往堆里面删除数据
void HPPop(HP* php);
//取堆顶数据
HPDataType HPTop(HP* php);
// 判断堆是否为空
bool HPEmpty(HP* php);
Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//堆的初始化
void HPInit(HP* php) {
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
//堆的销毁
void HPDestroy(HP* php) {
assert(php);
if (php->arr)
free(php->arr);
php->arr = NULL;
php->size = php->capacity = 0;
}
//交换函数
void Swap(int* x, int* y) {
int tmp = *x;
*x = *y;
*y = tmp;
}
//堆的向上调整算法
void AdjustUp(HPDataType* arr, int child) {
int parent = (child - 1) / 2;
while (child > 0) {
//建大堆,>
//建小堆,<
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else {
break;
}
}
}
//堆的向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n) {
int child = parent * 2 + 1;//左孩子
while (child < n) {
//小堆:找左右孩子中最小的
//大堆:找左右孩子中最大的
//看arr[child] > arr[child + 1]和if (arr[child] < arr[parent])里面的符号,符号不变就是小堆,反过来就是大堆
if (child + 1 < n && arr[child] > arr[child + 1]) {//防止越界
child++;
}
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
//往堆里面插入数据
void HPPush(HP* php, HPDataType x) {
assert(php);
//判断空间是否足够
if (php->size == php->capacity) {
//扩容
int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));
//空间申请失败
if (tmp == NULL) {
perror("relloc fail!");
exit(1);
}
//空间申请成功
php->arr = tmp;
php->capacity = newCapacity;
}
php->arr[php->size] = x;
AdjustUp(php->arr, php->size);//如果上一步size++的话,这里要size-1
++php->size;
}
//往堆里面删除数据
//出堆:出的是堆顶是数据
//用到了堆的向下调整算法
void HPPop(HP* php) {
assert(php && php->size);
Swap(&php->arr[0], &php->arr[php->size - 1]);//交换栈顶和栈底元素
--php->size;//删除交换后的原栈顶元素
AdjustDown(php->arr, 0, php->size);
}
//取堆顶数据
HPDataType HPTop(HP* php) {
assert(php && php->size);
return php->arr[0];
}
// 判断堆是否为空
bool HPEmpty(HP* php) {
assert(php);
return php->size == 0;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//冒泡排序
//时间复杂度:0(N^2)
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++){
int exchange = 0;
for (int j = 0; j < n - i - 1; j++){
//升序
if (arr[j] > arr[j + 1]){
exchange = 1;
Swap(&arr[j], &arr[j + 1]);
}
}
if (exchange == 0){
break;
}
}
}
//堆排序
//空间复杂度为0(1)
//时间复杂度为O(n*logn)
void HeapSort(int* arr, int n)
{
//建堆
//打印升序---大堆
//打印降序----小堆
//向上调整算法建堆
/*for (int i = 0; i < n; i++)
{
AdjustUp(arr, i);
}*/
//向下调整算法建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--){
AdjustDown(arr, i, n);
}
//循环将堆顶数据跟最后位置(会变化,每次减少一个数据)的数据进行交换
int end = n - 1;
while (end > 0){
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);
end--;
}
}
//空间复杂度为0(n)
void test01() {
HP hp;
HPInit(&hp);
int arr[] = { 17,20,10,13,19,15 };
for (int i = 0; i < 6; i++) {
HPPush(&hp, arr[i]);
}
while (!HPEmpty(&hp)) {
printf("%d ", HPTop(&hp));
HPPop(&hp);
}
//HPPop(&hp);
//HPDestroy(&hp);
}
int main() {
//test01();
//给定一个数组,对数组中的数据进行排序
int arr[] = { 17,20,10,13,19,15 };
//BubbleSort(arr, 6);
HeapSort(arr, 6);
for (int i = 0; i < 6; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
3.2.1 向上调整算法
堆的插入
将新数据插入到数组的尾上,再进行向上调整算法,直到满足堆。
向上调整算法
先将元素插入到堆的末尾,即最后一个孩子之后
插入之后如果堆的性质遭到破坏,将新插入结点顺着其双双亲往上调整到合适位置即可
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 = (parent - 1) / 2;
}
else
{
break;
}
}
}
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size-1);
}
计算向上调整算法建堆时间复杂度:
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果)
如果是根节点,不需要向上调整。
T(n)
:这个表达式代表的是在堆的向上调整算法中,对于一个具有n
个节点的堆,完成调整所需的移动步数。通过分层说明和移动步数计算解释,给出了一个关于
h
(堆的高度)的函数表达式,并最终通过一系列变换得到了T(h)
的表达式。
F(h)
:这个表达式代表的是在堆的向上调整算法中,对于高度为h
的堆,完成调整所需的移动步数的另一种表达形式。最终目的是为了推导出
F(n)
。
F(n)
:这个表达式代表的是对于具有n
个节点的堆,完成向上调整所需的移动步数的最终表达式。通过将T(h)
的表达式转换为关于n
的表达式,得到了F(n)
的表达式。这个表达式是基于二叉堆的性质,即堆的节点数
n
与堆的高度h
之间的关系==(n = 2h - 1),以及高度h
与节点数n
之间的关系(h = log2(n + 1))==。
由此可得:
向上调整算法建堆时间复杂度为:O(n ∗ log2n)
3.2.2 向下调整算法
堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
向下调整算法
将堆顶元素与堆中最后一个元素进行交换
删除堆中最后一个元素
将堆顶元素向下调整到满足堆特性为止
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
// 假设法,选出左右孩子中小的那个孩子
if (child+1 < n && 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 HPPop(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);
}
计算向下调整算法建堆时间复杂度:
最底层不需要向下移动
向下调整算法建堆时间复杂度为:O(n)
3.3 堆的应用
3.3.1 堆排序
代码1:基于已有数组建堆、取堆顶元素完成排序版本
// 1、需要堆的数据结构
// 2、空间复杂度 O(N)
void HeapSort(int* a, int n)
{
HP hp;
for(int i = 0; i < n; i++)
{
HPPush(&hp,a[i]);
}
int i = 0;
while (!HPEmpty(&hp))
{
a[i++] = HPTop(&hp);
HPPop(&hp);
}
HPDestroy(&hp);
}
该版本有一个前提,必须提供有现成的数据结构堆
代码2:数组建堆,首尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次大的数据
// 升序,建大堆
// 降序,建小堆
// O(N*logN)
void HeapSort(int* a, int n)
{
// a数组直接建堆 O(N)
for (int i = (n-1-1)/2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
// O(N*logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
堆排序时间复杂度计算:
通过分析发现,堆排序第二个循环中的向下调整与建堆中的向上调整算法时间复杂度计算一致,因此,堆排序的时间复杂度为O(n + n ∗ log n)
,即O(n log n)
堆排序时间复杂度为:O(n *log n)
3.3.2 TOP-K问题
TOP-K
问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
比如:
要存储4GB的内存数据,现在只有1GB,怎么找到里面最大的10个数?
分四次建堆,每次建堆分别找到堆里面最大的10个数,最后在40个数里找到最大的10个数。
如果只有1KB怎么办呢?
对于Top-K
问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
用数据集合中前
K
个元素来建堆要取前
k
个最大的元素,则建小堆要取前
k
个最小的元素,则建大堆用剩余的
N-K
个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余
N-K
个元素依次与堆顶元素比完之后,堆中剩余的K
个元素就是所求的前K
个最小或者最大的元素
为什么要取前
k
个最大的元素,则建小堆呢?因为在最小堆中,堆顶元素总是堆中最小的元素。当处理一个新元素时,如果这个新元素比堆顶元素大,那么它就有可能是最大的
k
个数之一。此时,我们把堆顶元素弹出,将新元素加入堆中。然后调整新元素在堆里面的位置。这样,堆中始终保持了当前遇到的最大的k
个数。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
//生成随机数
//void CreateNDate()
//{
// // 造数据
// int n = 100000;
// srand(time(0));
// const char* file = "data.txt";
// FILE* fin = fopen(file, "w");
// if (fin == NULL)
// {
// perror("fopen error");
// return;
// }
// for (int i = 0; i < n; ++i)
// {
// int x = (rand() + i) % 1000000;
// fprintf(fin, "%d\n", x);
// }
// fclose(fin);
//}
//定义堆的结构---数组
typedef int HPDataType;
typedef struct Heap {
HPDataType* arr;
int size;//有效的数据大小
int capacity;//空间大小
}HP;
//交换函数
void Swap(int* x, int* y) {
int tmp = *x;
*x = *y;
*y = tmp;
}
//堆的向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n) {
int child = parent * 2 + 1;//左孩子
while (child < n) {
//小堆:找左右孩子中最小的
//大堆:找左右孩子中最大的
//看arr[child] > arr[child + 1]和if (arr[child] < arr[parent])里面的符号,符号不变就是小堆,反过来就是大堆
if (child + 1 < n && arr[child] > arr[child + 1]) {//防止越界
child++;
}
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
//TOP-K排序
void topk()
{
printf("请输入k:>");
int k = 0;
scanf("%d", &k);
//从文件中读取前k个数据,建堆
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
int val = 0;
int* minheap = (int*)malloc(sizeof(int) * k);//创建小堆
if (minheap == NULL)
{
perror("malloc error");
return;
}
for (int i = 0; i < k; i++)//循环读取数据,先读取k个
{
fscanf(fout, "%d", &minheap[i]);
}
// 建k个数据的小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)//循环读取数据,读取n-k个
{
AdjustDown(minheap, i, k);
}
int x = 0;
while (fscanf(fout, "%d", &x) != EOF)
{
// 读取剩余数据,比堆顶的值大,就替换他进堆
if (x > minheap[0])
{
minheap[0] = x;
AdjustDown(minheap, 0, k);
}
}
for (int i = 0; i < k; i++)//打印最大的k个数据
{
printf("%d ", minheap[i]);
}
fclose(fout);
}
int main() {
//CreateNDate();
topk();
return 0;
}
时间复杂度:O(n) = k + (n − k)log2k