目录
一、树的概念
树: 一种非线性的数据结构,由n(n>=0)个有限结点组成的一个具有层次关系的集合。因看起来像一棵倒挂的树,根朝上,叶朝下因此得名树

- 子树是不相交的
- 除了根节点外,每个结点有且仅有一个父节点
- 一棵N个结点的树有N-1条边
Tips:树的子集之间不能有交集,否则就不是树

树的层数我们一般从1开始:
从第一层开始算: 空树高度为0,只有根节点高度为1
从第零层开始算: 空树高度为-1,只有根节点高度为0
所以我们倾向于第一种
树的构成: 根+N棵子树(N>=0)
二、树的定义
1.已知树的度
c
#define N 4
struct TerrNode
{
int val;//
struct TreeNode* subs[N];//定义一个指针数组
};
2.树的度未知
(1)顺序表表示法
c
struct TreeNode
{
int val;
SeqList subs;//用顺序表动态开辟空间,顺序表内部存struct TreeNode*孩子的指针
//这种方式用C++写会更好一点
//vector<struct TreeNode*>subs;
}
(2)左孩子右兄弟表示法
无论一个父亲节点有多少个孩子,leftchild都只指向左边第一个孩子,剩下的孩子用rightBother挨个访问,即可走完整个树

c
struct TreeNode
{
int val;
struct TreeNode* leftchild;
struct TreeNode* rightBrother;
};
数组存储: 只适用于满二叉树和完全二叉树
非完全二叉树也可以用数组存,但是不适合,因为会存在很多空间浪费(要补齐)
非完全二叉树: 链式存储
三、二叉树
1.概念
二叉树: 树的一个子集,限定了树的度最大为二
组成: 为空或者由一个根节点加上两棵称为左子树和右子树的二叉树组成

Tips:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,二叉树是有序树
2.特殊的二叉树
(1)满二叉树

满二叉树: 二叉树的每一层的结点数都达到最大值,最后一层都是叶子节点
已知高度求结点数: 一个高度为k的满二叉树,一共有2k-1个节点
已知结点数求高度: 如果这棵树有N个节点,那么这棵树的高度为log2(N+1)
(2)完全二叉树

完全二叉树: 高度为k的一棵二叉树,前k-1层都是满的,只有最后一层不满且从左到右连续
3.二叉树的存储
(1)链表
适合存储非完全二叉树

(2)数组
a.逻辑结构与物理结构
这里我们采用逻辑结构与物理结构结合的方式去理解

用数组存储的优势: 用数组存储更简单,可以用下标算父子关系,快速找到孩子和父亲
局限性: 由于数组空间是连续的,因此只适合满二叉树和完全二叉树,如果存别的需要空出位置,会造成很多的空间浪费
b.用下标算父子关系
已知父亲在数组中的下标为i:
左孩子下标:2*i+1
右孩子下标:2*i+2
已知孩子在数组中的下标为j:
父亲在数组中的下标:(j-1)/2
Tips:这里由于除法取整,因此偶数不管是 j-1/2 还是 j-2/2 结果是一致的
四、堆
物理上: 数组
逻辑上: 完全二叉树
1.堆的概念
堆的概念: 一个完全二叉树

大堆: 任何一个父亲>=孩子
小堆: 任何一个父亲<=孩子
Tips:小堆和大堆不代表着升序和降序,堆只要求了父子之间的大小关系,但是兄弟之间(同一层之间)并没有确定大小关系
2.堆的定义
c
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
3.初始化
c
void HPInit(HP* php)
{
assert(php);
php->a=NULL;
php->size=php->capacity=0;
}
4.销毁
c
void HPDesTroy(HP* php)
{
assert(php);
free(php->a);
php->a=NULL;
php->size=php->capacity=0;
}
5.插入
(1)向上调整

- 先将元素插入到堆的末端,即最后一个孩子之后
- 插入之后如果堆的性质遭到破坏,将新插入节点顺着其双亲往上调整至合适位置即可
c
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;
}
}
Tips:这里的循环条件不建议用parent>=0,因为这里parent不会小于0
(2)交换
c
void Swap(HPDataType* p1,HPDataType* p2)
{
HPDataType tmp=*p1;
*p1=*p2;
*p2=tmp;
}
(3)插入数据
c
void HPPush(HP* php,HPDataType x)
{
assert(php);
//判断数组是否已满
if(php->size==php->capacity)
{
int newcapacity=php->capacity==0?4:2*php->capacity;
HPDataType* temp=(HPDataType*)realloc(php->a,newcapacity*sizeof(php->a));
if(temp==NULL)
{
perror("realloc fail\n");
return;
}
php->capacity=newcapacity;
php->a=temp;
}
php->a[php->size]=x;//在堆末尾插入数据
php->size++;//增加有效元素个数
AdjustUP(php->a,php->size-1);//要插入的数据位置为size-1
}
6.删除
这里的删除指的是删除堆顶的数据
不能只是单纯前移一位数组数据挪动覆盖删除,这样会导致父子关系混乱

- 将堆顶元素与堆中最后一个元素进行交换
- 删除堆中最后一个元素
- 将堆顶元素向下调整到满足堆特性为止
(1)向下调整

向下调整算法: 如果是小堆(大堆)的话,先比较该元素所在位置的左右子树的大小,取小(大)的那个与该元素进行比较,若小于(大于)该元素,则交换位置
到叶子节点截止: 当算出来左孩子的范围超出数组的范围
c
AdjustDown(HPDataType* a,int n,int parent)
{
//假设法,先假设左孩子小
int child=parent*2+1;
while(child<n)//如果child>=n,说明孩子不存在,调整到叶子节点了
{
//找出小的那个孩子
if(child+1<n&&a[child+1]<a[child])//如果右孩子小于左孩子,同时判断一下右孩子是否小于n,否则会有越界风险
{
++child;
}
//基于小堆情况
//如果孩子小于父亲
if(a[child]<a[parent])
{
Swap(&a[child],&a[parent]);
parent=child;//将元素下标改为其原孩子的下标
child=parent*2+1;//继续计算该元素当前位置的左孩子下标
}
else
break;//父亲小于孩子或者超出数组范围就break
}
}
(2)删除堆顶数据
c
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);//从根的位置向下调整,因此传0
}
时间复杂度最好情况是O(1),最坏情况是O(logN),效率很高
7.返回堆顶元素
c
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size>0);
return php->a[0];
}
8.判空
c
bool HPEmpty(HP* php)
{
assert(php);
return php->size==0;
}
9.堆排序
(1)建堆
❌降序建大堆:根节点不变,以剩下的数字重新作为堆找出次大的,会导致兄弟变父子、兄弟叔侄等关系混乱,需要重新建堆,代价太大
❌升序建小堆:根节点不变,以剩下的数字重新作为堆找出小的,也会导致关系混乱
✅降序建小堆:先首尾交换找出最小的,再向下调整,不断选出次小的
✅升序建大堆:先首尾交换找出最大的,再向下调整,不断选出次大的
a.向上调整建堆:模拟插入
c
for(int i=1;i<n;i++)//循环从1开始,默认i=0时就是个堆,然后不断插入向上调整
{
AdjustUp(a,i);
}

时间复杂度: O(N*logN)
特点: 结点数量多的层调整次数多,结点数量少的层调整次数少
b.向下调整建堆
如果左右子树都是大堆,可直接向下调整
如果不是,采取倒着向下调整的方法:由于叶子结点既可以看做大堆也可以看做小堆,因此我们从倒数第一个非叶子结点开始调
c
for(int i=(n-1-1)/2;i>=0;i--)//最后一个结点的父亲结点为起始调整结点
{
AdjustDown(a,n,i);
}

时间复杂度: O(N)
每一层的结点数: 等比数列
**每一层的结点的向下调整次数:**等差数列
c.时间复杂度
满二叉树:
F(h)=20+21+22+...+2(h-2)+2(h-1)
F(h)=2h-1
F(h)=2h-1=N
h= log 2 ( N + 1 ) \log_2(N+1) log2(N+1)
完全二叉树:
最坏情况:只有一个叶子节点
F(h)=20+21+22+...+2(h-2)+1
F(h)=2(h-1)-1+1
F(h)=2(h-1)-1+1=N
h= log 2 N \log_2N log2N+1
不论是向上调整还是向下调整,每次插入调整后都往后走一层,一共走高度次
这里我们忽略细节,可将二者的时间复杂度都看做O( log 2 N \log_2N log2N)
(2)排序
c
int end=n-1;
while(end>0)
{
Swap(&a[0],&a[end]);
AdjustDown(a,end,0);
--end;
}
时间复杂度: O(N*logN)
调整次数类似向上调整建堆
10.Pop K问题(N个数找最大的前K个)
方法1:
建一个N个数的大堆(O(N)),Pop K次(O(K*logN))
如果N远大于K:时间复杂度为O(N)
缺点: 当N很大时,空间消耗太大,分次解决效率太低
方法2:
用前K个数,建一个小堆(O(K)),剩下数据跟堆顶数据比较,如果比堆顶的数据大,就替代堆顶进堆(覆盖根位置,然后向下调整)(O(K*log(N-K)),最后小堆中的K个数据就是最大的前K个数
时间复杂度: O(N)
c
int k;
printf("请输入k:");
scanf("%d",&k);
int* kminheap=(int*)malloc(sizeof(int)*K)
if(kimheap==NULL)
{
perror("malloc fail");
return;
}
//打开文件
const char* file="data.txt";
FILE* fout=fopen(file,"r");
if(fout==NULL)
{
perror("fopen error");
return;
}
//读取文件中的前k个数
for(int i=0;i<k;i++)
{
fscanf(fout,"%d",&kimheap[i]);
}
//建有k个数的小堆
for(int i=(k-1-1)/2;i>=0;i++)
{
AdjustDown(kimheap,k,i);
}
//读取剩下的N-k个数
int x;
while(fscanf(fout,"%d",&x)>0)
{
if(x>kiminheap[0])//剩余数据大于堆顶数据
{
kminheap[0]=x;//该数据直接覆盖堆顶数据,代替堆顶进堆
AdjustDown(kminheap,k,0);//向下调整
}
}
//打印输出最大前k个数
printf("最大前k个数:");
for(int i=0;i<k;i++)
{
printf("%d ",kminheap[i]);
}
printf("\n");
五、完整代码
Heap.h
c
#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* php);//初始化
void HPDesTroy(HP* php);//销毁
void Swap(HPDataType* p1, HPDataType* p2);//交换
void AdjustUP(HPDataType* a, int child);//向上调整
void HPPush(HP* php, HPDataType x);//插入数据
AdjustDown(HPDataType* a, int n, int parent);//向下调整
void HPPop(HP* php);//删除堆顶数据
HPDataType HPTop(HP* php);//返回堆顶元素
bool HPEmpty(HP* php);//判空
Heap.c
c
#include"Heap.h"
//初始化
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
//销毁
void HPDesTroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = 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* php, HPDataType x)
{
assert(php);
//判断数组是否已满
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* temp = (HPDataType*)realloc(php->a, newcapacity * sizeof(php->a));
if (temp == NULL)
{
perror("realloc fail\n");
return;
}
php->capacity = newcapacity;
php->a = temp;
}
php->a[php->size] = x;//在堆末尾插入数据
php->size++;//增加有效元素个数
AdjustUP(php->a, php->size - 1);//要插入的数据位置为size-1
}
//向下调整
AdjustDown(HPDataType* a, int n, int parent)
{
//假设法,先假设左孩子小
int child = parent * 2 + 1;
while (child < n)//如果child>=n,说明孩子不存在,调整到叶子节点了
{
//找出小的那个孩子
if (child + 1 < n && a[child + 1] < a[child])//如果右孩子小于左孩子,同时判断一下右孩子是否小于n,否则会有越界风险
{
++child;
}
//基于小堆情况
//如果孩子小于父亲
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;//将元素下标改为其原孩子的下标
child = parent * 2 + 1;//继续计算该元素当前位置的左孩子下标
}
else
break;//父亲小于孩子或者超出数组范围就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);//从根的位置向下调整,因此传0
}
//返回堆顶元素
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
//判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
test.c
以下可用作测试参考
c
#include"Heap.h"
void TestHeap1()
{
int a[] = { 4,6,5,1,8,3,2,7,9 };
HP hp;
HPInit(&hp);
//建堆
for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
{
HPPush(&hp, a[i]);//一个一个插入,然后向上调整
}
//Pop的应用意义
//从小到大的打印印堆中所有数据
while (!HPEmpty(&hp))
{
printf("%d ", HPTop(&hp));
//若想排序:a[i++]=HPTop(&hp); 将数据从小到大放入数组
HPPop(&hp);
}
//找出最大的前k个数据
int k = 0;
scanf("%d", &k);
while (k--)
{
printf("%d ", HPTop(&hp));
HPPop(&hp);
}
printf("\n");
}
void HeapSort(int* a, int n)
{
//降序建小堆
//升序建大堆
//向上调整建堆
/*for(int i=1;i<n;i++)//循环从1开始,默认i=0时就是个堆,然后不断插入向上调整
{
AdjustUp(a,i);
}*/
//向下调整建堆
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;
}
}
//建堆测试
void TestHeap()
{
int a[] = { 1,5,8,6,9,3,7 };
HeapSort(a, sizeof(a) / sizeof(int));
}
int main()
{
TestHeap();
TestHeap1();
return 0;
}