文章目录
- [一. 树](#一. 树)
-
- [1. 树的概念](#1. 树的概念)
- 2.树的表示
- [二. 二叉树](#二. 二叉树)
-
- [2.1 概念](#2.1 概念)
- [2.2 满二叉树](#2.2 满二叉树)
- [2.3 完全二叉树](#2.3 完全二叉树)
- [2.4 二叉树的性质](#2.4 二叉树的性质)
- [2.5 二叉树的存储结构](#2.5 二叉树的存储结构)
- [2.5.1 顺序结构](#2.5.1 顺序结构)
- [2.5.2 链式结构](#2.5.2 链式结构)
- [三. 实现顺序结构二叉树](#三. 实现顺序结构二叉树)
一. 树
1. 树的概念
树是一种非线性的数据结构 ,它是由 n(n>=0) 个有限结点组成一个具有层次关系的集合 。把它叫做树是因为它看起来像一棵倒挂的树 (它是根朝上,而叶朝下的)
- 有一个特殊的结点,称为根结点,根结点没有前驱结点。
- 除根结点外,其余结点被分成 M (M>0) 个互不相交的集合 T1、T2、......、Tm。其中每一个集合 又是一棵结构与树类似的子树 。每棵子树的根结点有且只有一个前驱,可以有 ≥0 个后继。因此,树是递归定义的。
- 子树是不相交的。(相交的话是图)
- 除根结点外,每个结点有且只有一个父结点。
- 一棵N个结点的树有N-1条边。(忽略图中的箭头)
父结点/双亲结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;
子结点/孩子结点:一个结点含有的子树的根结点称为该结点的子结点;
兄弟结点:具有相同父结点的结点互称为兄弟结点(亲兄弟);
结点的度:这个结点有几个孩子,他的度就是多少;
树的度:一棵树中,最大的结点的度 称为树的度;
叶子结点/终端结点:度为 0 的结点;
分支结点/非终端结点:度不为 0 的结点;
结点的层次:从根开始定义起,根为第 1 层,根的子结点为第 2 层,以此类推;
树的高度或深度:树中结点的最大层次;
结点的祖先:(从根到该结点)所经分支上的所有结点;
路径:一条从树中任意结点出发,沿父结点-子结点连接,达到任意结点的序列;
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。
森林:由 m(m>0) 棵互不相交的树的集合称为森林;
2.树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系。
孩子兄弟表示法:
cpp
struct TreeNode
{
struct Node* child; // 左边开始的第⼀个孩⼦结点
struct Node* brother; // 指向其右边的下⼀个兄弟结点
int data; // 结点中的数据域
};
二. 二叉树
2.1 概念
二叉树是树的一种,二叉树是在树的基础上添加了一些规则。
在树形结构中,我们最常用的就是二叉树,一棵二叉树是结点的一个有限集合(集合是什么呢?集合由一个根结点 加上两棵别称为左子树 和右子树的二叉树组成或者为空)
二叉树的特点:
1.二叉树不存在度大于 2 的结点(度是孩子结点的个数)
2.二叉树的子树有左右之分,次序不能颠倒 ,因此二叉树是有序树
对于任意的二叉树都是由以下几种情况复合而成的
2.2 满二叉树
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。(层数是k,则该层的结点数是2^(k-1)^个
结点总数是每一层的加起来(是等比数列),结果是2^k^-1
2.3 完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。满二叉树是特殊的完全二叉树,当完全二叉树的最后一层的结点数是最大结点数,即2^k-1^,则为满二叉树。
对于深度(层数)为 K 的,有 n 个结点的二叉树,当且仅当其每一个结点都与 深度为K的满二叉树中编号从 1 至 n 的结点一一对应时,称之为完全二叉树。
再解释一下完全二叉树:假设二叉树的层次是k,除了最后一层(第k层),其余每层的结点个数都达到了最大结点数,且第k层的结点的顺序是从左到右,这种二叉树叫做 "完全二叉树"。
2.4 二叉树的性质
由满二叉树推理而得的二叉树的性质:
若规定根结点的层数为1
- 非空二叉树的第i层最多有2^i-1^个结点
- 深度为h的非空二叉树的最大结点数是2^h^-1
- 具有 n 个结点的满二叉树的深度
2.5 二叉树的存储结构
2.5.1 顺序结构
顺序结构存储就是使用数组来存储 ,⼀般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费
我们不能直接存右图的A,B,C,D,E,F,这样会打乱顺序,之后没法通过公式找到某个结点。
2.5.2 链式结构
二叉树的链式存储结构:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。
链表中每个结点由三个域组成:数据域和左右指针域(左右指针:该结点的左孩子和右孩子所在的链结点的存储地址)
链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。后面课程学到高阶数据结构如红黑树等会用到三叉链。
三. 实现顺序结构二叉树
3.1 堆的概念和结构
在二叉树里面,还有一个特殊的二叉树:堆
- 堆:通常使用顺序结构来存储数据
- 堆:具有⼆叉树的特性的同时,还具备其他的特性。
堆的性质:
- 堆中某个结点的值≤或≥根结点。(分为大小堆)
- 堆总是一颗完全二叉树。(从左到右的顺序)
小堆(小根堆):根结点的值最小的堆-------->叫做小根堆(小堆的堆顶值是min的)
大堆(大根堆):根结点的值最大的堆-------->叫做大根堆(大堆的堆顶值是max的)
3.2 堆的性质
对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为 i 的结点有:
- 若i=0,则 i 为根结点的序号(没有父结点)
- 若i > 0,i 这个位置的结点的父结点序号是:(i-1)/2;左孩子结点的序号:2i +1;右孩子结点序号:2i +2
- 左孩子序号<结点个数(2i+1<n),则有左孩子;右孩子序号<结点个数(2i+2<n),则有右孩子;
- 左孩子序号>=结点个数(2i+1>=n),则没有左孩子;右孩子序号>=结点个数(2i+1>=n),则没有右孩子;
10是根结点,序号是0,只有孩子结点。i=0,左孩子结点:0*2+1=1,右孩子结点:0 *2+2=2.
3.3 堆的实现
堆底层结构为数组
堆的初始化和销毁
cpp
//Heap.h
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
//定义堆的结构---数组
typedef int HPDataty;
typedef struct Heap
{
HPDataty* arr;
size_t size; //有效个数
size_t capacity; //空间大小
}HP;
//堆的初始化
void HPInit(HP* php);
void HPDestroy(HP* php);
cpp
//Heap.cpp
#include"Heap.h"
void HPInit(HP* php)
{
assert(php);
/*HPDataty* arr1=(HPDataty*)malloc(sizeof(HPDataty));
if (arr1 == NULL)
{
perror("malloc fail!");
exit(1);
}*/
php->arr = NULL;
php->size = php->capacity = 0;
}
void HPDestroy(HP* php)
{
assert(php);
if (php->arr != NULL)
{
free(php->arr);
}
php->arr = NULL;
php->size = php->capacity = 0;
}
向堆里插入数据
cpp
void HPPush(HP* php, HPDataty x)
{
assert(php);
//判断空间是否足够
if (php->size == php->capacity)
{
//扩容
size_t newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataty* tmp = (HPDataty*)realloc(php->arr, newcapacity * sizeof(HPDataty));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
php->arr = tmp;
php->capacity = newcapacity;
}
//空间足够,添加数据
php->arr[php->size] = x;
php->size++;
}
重点:插入数据之后,并不代表这个数据就是堆了。堆它还有其它要求呢,根结点的值最大/最小;父结点的值≥或≤孩子结点。
以小堆为例:
新插入的数据肯定是孩子结点,将arr[孩子]和arr[父结点]比较。
堆的向上调整算法
从最后一个插入的数据arr[size-1]
开始,最后一个数据只可能是孩子结点,将children=size-1。
将这个孩子结点和它的父结点arr[(children-1)/2]比较,如果孩子结点<父结点,将其交换。循环往上走,直到父结点=0.
cpp
void Swap(int* x, int* y)
{
int t = *x;
*x = *y;
*y = t;
}
void AdjustUp(HPDataty* arr,int child)
{
int parent = (child - 1) / 2;
while (child>0) //当孩子结点是0时,那就是已经走到根结点了,父结点已,越界无需再向上比较
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child- 1) / 2;
}
else
{
break;
}
}
}
void HPPush(HP* php, HPDataty x)
{
assert(php);
//判断空间是否足够
if (php->size == php->capacity)
{
//扩容
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataty* tmp = (HPDataty*)realloc(php->arr, newcapacity * sizeof(HPDataty));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
php->arr = tmp;
php->capacity = newcapacity;
}
//空间足够,添加数据
php->arr[php->size] = x;
php->size++;
AdjustUp(php->arr,php->size-1);
}
void Print(HP* php)
{
int i = 0;
while (i < php->size)
{
printf("%d ", php->arr[i++]);
}
}
删除堆里的数据
删除堆里的数据,删除的是堆顶的数据。
但是如果直接将堆顶的数据删除,接下来的堆就没有堆顶了。
我们可以将根结点和最后一个结点交换,然后将最后一个结点删除掉,那接下来的堆就不是有效的,我们需要将数据再次排序调整----->向下调整算法。
向下调整算法
要将根结点和孩子结点比较,【比较左右孩子结点的值,找出那两个孩子中 最小的孩子结点】,将那个小的孩子结点和父结点比较,若是父结点比 (那个小的孩子结点) 大,则将它们两个交换,那么此时的根结点放的就是最小的值了。循环往下...
接下来的一个重点是:那这个循环什么时候停止呢?
取堆顶数据
cpp
HPDataty HPTop(HP* php)
{
assert(php && php->size);
return php->arr[0];
}
判空
cpp
bool HPEmpty(HP* php)
{
assert(php);
return php->size==0;
//如果size不等于0,堆不是空的,那么返回假。 !HPEmpty就是真
}
可以将判空用于读取堆里的所有数据。
先读取堆顶数据,然后将堆顶数据删除,下一次又可以读堆顶数据。那循环什么时候停止呢?当数据是空的时候,这时候判断条件就可以用判空那个函数了。
cpp
while (!HPEmpty(&hp))
{
printf("%d ", HPTop(&hp));
HPPop(&hp);
}