堆
目录
一、树的概念和结构
1.1.树的概念
一种非线性的数据结构

1.2.树的相关概念

**根节点:**唯一没有前驱节点的特殊节点
**子树:**某个节点及其后代组成的子结构
**节点的度:**该节点的子树个数
**叶节点(终端节点):**度为0的节点
**分支节点(非终端节点):**度不为0的节点
**父节点:**直接包含子节点的上级节点
**子节点:**作为某个节点子树根节点的直接下级节点
**兄弟节点:**具有相同父节点的同级节点
**堂兄弟节点:**双亲在同一层的节点
**树的度:**最大节点的度
**节点的层次:**以根节点为第1层,根的子节点为第2层,以此类推
**树的深度:**节点的最大层次
**节点的祖先:**从根到该节点所经分支上的所有节点
**子孙:**以某节点为根的子树中任一节点
**森林:**多棵不相交的树的集合(并查集)
1.3.树的递归定义
每棵树都可以看作由一个根和若干个子树组成
1.4.区分树和非树
子树不相交:

除了根节点外,每个节点有且仅有一个父节点:

一个N节点的树有N-1条边:

1.5.树的节点定义
1.5.1.明确树的度为N
cpp
#define N 4
struct TreeNode
{
int val;
struct TreeNode* subs[N];
};
1.5.2.没有明确树的度
左孩子右兄弟表示法
cpp
struct TreeNode
{
int val;
struct TreeNode* leftchild;
struct TreeNode* rightBrother;
};
无论一个父节点有多少子节点
child指向左边开始第一个孩子

1.6.树的应用
Linux树状目录结构

二、二叉树的概念及结构
2.1.二叉树的概念
一种特殊的树结构
由一个根节点及左子树和右子树构成
允许为空树

2.1.二叉树的性质
不存在度大于2的节点
子树有左右之分,次序不能颠倒

2.2.特殊的二叉树
**满二叉树:**每层节点数都为最大值

**完全二叉树:**除最后一层外都为满二叉树,且最后一层从左到右必须连续

三、二叉树与数组
3.1.完全二叉树与数组

用下标算父子关系:
算孩子:
- 假设父亲在数组中的下标是:i
- 左孩子在数组中的下标是:i * 2 + 1
- 右孩子在数组中的下标是:i * 2 + 2
算父亲:
- 假设孩子在数组中的下标是:j
- 父亲在数组中的下标:(j - 1) / 2
3.2.非完全二叉树与数组

非完全二叉树可以用数组存储
但是会造成空间浪费,所以不适合
四、堆
4.1.堆的概念
大堆:完全二叉树,每个父亲节点的值都大于等于其子节点的值

小堆:完全二叉树,每个父亲节点的值都小于 等于其子节点的值

4.2.堆的特点
小堆根最小,谁小谁当爹
大堆根最大,谁大谁当爹
排序效率高
4.3.堆的实现
4.3.1.堆文件结构
- 头文件(Heap.h):顺序表的结构创建,顺序表的方法声明
- 源文件(Heap.c):顺序表的方法实现
- 测试文件(test.c):测试数据结构的方法
4.3.2.头文件编写
cpp
//头文件包含
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
//元素重命名
typedef int HPDataType;
//堆结构的定义
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
//交换元素
void Swap(HPDataType* p1, HPDataType* p2);
//向上调整
void AdjustUp(HPDataType* a,int child);
//向下调整
void AdjustDown(HPDataType* a,int n,int parent);
//堆的初始化
void HPInit(HP* php);
//堆的销毁
void HPDestroy(HP* php);
//堆的插入
void HPPush(HP* php, HPDataType x);
//堆的删除
void HPPop(HP* php);
//堆顶元素
HPDataType HPTop(HP* php);
//堆的判空
bool HPEmpty(HP* php);
4.3.2.源文件编写
4.3.2.1.头文件包含
cpp
#include "Heap.h"
4.3.2.2.堆的初始化
cpp
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
4.3.2.3.堆的销毁
cpp
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
4.3.2.4.堆的插入
cpp
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail!");
return;
}
php->capacity = newcapacity;
php->a = tmp;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
4.3.2.5.向上调整
cpp
#define MIN_HEAP 1
//#define MAX_HEAP 1
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
#ifdef MIN_HEAP
if (a[child] < a[parent])//小堆:子 < 父则交换
#else
if (a[child] > a[parent])//大堆:子 > 父则交换
#endif
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
解析:

从某个子节点的下标向上调整
(子节点下标 - 1)/ 2 == 父节点的下标,依次与祖先节点比较
如果是小堆,子节点值小于父节点时,将子节点与父节点值交换
并且更新子节点的下标,再次找新的父节点的下标继续进行比较
4.3.2.6.堆的删除
cpp
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);
}
**注:**堆的删除只能从堆顶元素删除,随机删除会破坏堆的结构
4.3.2.7.向下调整
cpp
#define MIN_HEAP 1
//#define MAX_HEAP 1
void AdjustDown(HPDataType* a, int n, int parent)
{
//小堆:假设左孩子小 大堆:假设左孩子大
int child = parent * 2 + 1;
while (child < n)
{
//找出较小的孩子(右孩子要存在,否则会越界)
#ifdef MIN_HEAP
if (child + 1 < n && a[child + 1] < a[child])
#else //找出较大的孩子(右孩子要存在,否则会越界)
if (child + 1 < n && a[child + 1] > a[child])
#endif
{
++child;
}
#ifdef MIN_HEAP
if (a[child] < a[parent])//小堆:父 > 子则交换
#else
if (a[child] > a[parent])//大堆:父 < 子则交换
#endif
{
Swap(&a[child], &a[parent]);
parent = child;//更新父节点下标
child = parent * 2 + 1;//算出新的孩子下标
}
else
{
break;
}
}
}
解析:

从根节点的下标向下调整
父节点的下标 * 2 + 2 == 右孩子的下标
父节点的下标 * 2 + 1 == 左孩子的下标
用假设法判断较小的子节点的下标值
交换根节点与子节点的元素
将新的根节点进行向下调整
4.3.2.8.堆顶元素
cpp
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
4.3.2.9.堆的判空
cpp
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
4.3.3.测试文件编写
cpp
void TestHeap01()
{
int a[10] = { 4,2,8,1,5,6,9,7 };
HP hp;
HPInit(&hp);
for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&hp, a[i]);
}
//找出最小的前k个
int k = 0;
scanf("%d",&k);
while(k--)
{
printf("%d ",HPTop(&hp));
HPPop(&hp);
}
HPDestroy(&hp);
}
int main()
{
TestHeap01();
return 0;
}
4.4.堆排序
降序(建小堆):
向上调整建堆:
cpp
void HeapSort(int* a, int n)
{
//建堆
for (int i = 0; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
//排序
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a,end,0);
--end;
}
}
void TestHeap02()
{
int a[] = { 4,2,8,1,5,6,9,7 };
HeapSort(a, sizeof(a) / sizeof(int));
}
向下调整建堆:
cpp
void HeapSort(int* a, int n)
{
//建堆(从最后一个节点(下标为n-1)的父节点开始)
for (int i = (n-2)/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;
}
}
void TestHeap02()
{
int a[] = { 4,2,8,1,5,6,9,7 };
HeapSort(a, sizeof(a) / sizeof(int));
}
**注:**降序建小堆,升序建大堆
向下调整建堆:
假设树的高度为h:

需要移动节点的总步数:
T(h) = 2^0 * (h - 1) + 2^1 * (h - 2) + 2^2 * (h - 3) +...... + 2^(h - 3) * 2 + 2^(h - 2) * 1
错位相减可得:
T(h) = 2^0 + 2^2 + ...... + 2^(h - 2) + 2^(h - 1) - 2^0 * (h - 1)
= 2^1 + 2^2 + ...... + 2^(h - 1) + 2^0 * h
= 2^h - 1 - h
满二叉树:最后一层满的高度
F(h) = 2^0 + 2^1 + ...... + 2^(h - 2) + 2^(h - 1) = 2^h - 1 = N
h = log2(N + 1)
最少情况:最后一层只有一个的高度
F(h) = 2^0 + 2^1 + ...... + 2^(h - 2) + 1= 2^(h - 1) = N
h = log2(N) + 1
综上所述:
满二叉树的总步数为:T(N) = N - log2(N + 1)
时间复杂度为:O(N),建堆效率远高于向上调整
**注:**节点数量多的层调整次数少,节点数量少的层调整次数多
向上调整建堆:

T(h) = 2^1 * 1 + 2^2 * 2 + ...... + 2^(h - 2) * (h - 2) + 2^(h - 1) * (h - 1)
错位相减可得:
T(h) = - (2^2 + 2^3 + ...... + 2^(h - 1)) + 2^h * (h - 1) - 2^1
= - (2^0 + 2^1 + 2^2 + 2^3 + ...... + 2^(h - 1)) +2^h * (h - 1) + 2^0
= - (2^h - 1) +2^h * (h - 1) + 2^0
满二叉树:最后一层满的高度
F(h) = 2^0 + 2^1 + ...... + 2^(h - 2) + 2^(h - 1) = 2^h - 1 = N
h = log2(N + 1)
综上所述:
满二叉树的总步数为:T(N) = - N + (N + 1) * (log2(N + 1) - 1) + 1
时间复杂度为:O(N*logN)
**注:**节点数量多的层调整次数多,节点数量少的层调整次数少
堆排序的整体时间复杂度为O(N*logN)
4.5.TopK问题
N个数找最大的前K个(假设N远大于k)
方法1:
建一个N个数的大堆,时间复杂度为O(N)
Popk次,时间复杂度为O(K * logN)
总时间复杂度为:O(N + K * logN)
方法2:
用前k个数,建一个小堆,时间复杂度为O(K)
剩下数据跟堆顶数据比较,如果比堆顶数据大,就替代堆顶进堆
覆盖根的位置,然后向下调整,时间复杂度为O(logK * (N - K))
总时间复杂度为:O(K + (N - K)*logK) ≈ O(N*logK)
cpp
#include "Heap.h"
#include "time.h"
//造数据
void CreateNData()
{
int n = 100;
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) % 10000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void TestHeap3()
{
int k;
printf("请输入k:");
scanf("%d", &k);
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL)
{
perror("malloc fail!");
return;
}
//打开文件读取
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
free(kminheap);
return;
}
//读取文件中前k个数
for (int i = 0; i < k; i++)
{
fscanf(fout,"%d", &kminheap[i]);
}
//建k个数的小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
//读取剩下的N-K个数
int x = 0;
while (fscanf(fout,"%d", &x) > 0)
{
if (x > kminheap[0])
{
kminheap[0] = x;
AdjustDown(kminheap, k, 0);
}
}
printf("最大的前%d个数:",k);
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap[i]);
}
printf("\n");
free(kminheap);
fclose(fout);
}
int main()
{
//CreateNData();
TestHeap3();
}