目录
树和森林
哈夫曼树
常用点的深度讲解
简单认识几种二叉树和它们的性质
什么是二叉树?
- 就是有一棵树,它的节点最多有两个分支,可以没有分支,可以有一个,也可以有两个。如果它有两个分支,那么它的左右分支是有顺序的,左边的就叫左子树,右边的就叫右子树,左右不能颠倒。如果它只有一个分支,那你也要说清楚这个分支到底是左子树还是右子树。
- 二叉树必须有根吗?不一定,二叉树可以没有根,也就是一个节点都没有,是被人连根拔起的树。
- 二叉树好像和树差不多,就是比一般的树特殊,那二叉树是树的特殊情况吗?不是!二叉树不是树的特殊情况,他们是两个概念。因为二叉树得说明左右子树,哪怕只有一个分支。但是一般的树就不区分这个,只有一个树枝就不区分左右了。
二叉树有哪些性质?
1、第 i 层最多有2^(i - 1)个节点。(i >= 1)
2、深度为 k 的二叉树最多有2^k - 1个节点。(k >= 1)
3、叶子数为n0,度为2的节点个数为n2,那么n0 = n2 + 1。
什么是满二叉树?
- 就是这个二叉树是饱和的,每一层有最多的节点,它的叶子都在最底层,整棵树有k层,就有2^k - 1个节点。
什么是完全二叉树?
- 你可以想象这里有一棵满二叉树,你觉得它枝叶太多,想剪掉一部分。那么你从这棵树的右下角开始剪:先剪最下面的一层,从右到左,剪完一层,觉得不够,再剪上一层,也是从右到左...这样你总是能保持这棵树是完全二叉树。
- 它用数学语言描述就是:对任一结点,如果其右子树的最大层次为 L,则其左子树的最大层次必为 L 或 L + 1。
完全二叉树有什么性质?
1、具有n个节点的完全二叉树的深度为logn + 1。
2 、假设完全二叉树有n个节点,每个节点按序编号,对于任一节点 i
1)i = 1,则节点是根。i > 1,该节点的双亲节点是[i / 2] ([ ]代表向下取整)
2)2i > n,节点i 是叶子节点。一个节点如果有左孩子,左孩子节点为2i。
3)2i + 1 > n,节点 i 没有右孩子。 一个节点如果右有孩子,有孩子节点为 2i + 1。
二叉树的存储和遍历
二叉树怎么存进计算机呢?
- 在计算机中存储某种逻辑结构一般采用顺序结构或者链式结构。我们先试试用顺序结构存储二叉树。以数组来模拟顺序存储,二叉树的节点是被我们按顺序编号的,那就按编号顺序将节点存储在数组里。但是这样存储的缺点也显而易见。
- 我们进而采用链式存储,每个节点的结构为:左孩子节点地址 - 数据 - 右孩子孩子节点地址
typedef struct BiTNode { // 结点结构
TElemType data;
struct BiTNode *lchild, *rchild; // 左右孩子指针
} BiTNode, *BiTree;
二叉树怎么遍历?
- 三种方式:先序遍历、中序遍历、后序遍历。
- 先序:就是每个节点都按照数据、左孩子节点地址、右孩子节点的顺序遍历。中序:每个节点都按照左孩子节点地址、数据、右孩子节点的顺序遍历。后序:就是每个节点都按照左孩子节点地址、右孩子节点、数据的顺序遍历。
//用递归的形式
//先序:
void preorder (BiTNode *root) {
if (root!=NULL) {
printf("%d", root->data); //访问根结点
preorder(root->Lchild); //先序遍历根的左子树
preorder(root->Rchild); //先序遍历根的右子树
}//if
}//preorder
//中序:
void preorder (BiTNode *root) {
if (root!=NULL) {
preorder(root->Lchild); //先序遍历根的左子树
printf("%d", root->data); //访问根结点
preorder(root->Rchild); //先序遍历根的右子树
}//if
}//preorder
//后序:
void preorder (BiTNode *root) {
if (root!=NULL) {
preorder(root->Lchild); //先序遍历根的左子树
preorder(root->Rchild); //先序遍历根的右子树
printf("%d", root->data); //访问根结点
}//if
}//preorder
怎么通过遍历顺序确定一棵二叉树?(深刻理解递归遍历)
- 给出先序遍历和中序遍历的一串数字或字母让你确定一棵二叉树,例如先序:ABCDEFGHI,后序:BCAEDGHFI,确定一棵二叉树。
- 要想推出二叉树结构,就要理解遍历中的归律。我在尝试很多种情况后发现几个有用的规律:
二叉树的创建
怎么方便的创建一个二叉树呢?
- 为了避免手动的一个一个创建节点,那就必须用到循环或递归的方式创建节点。下面我们介绍用递归的方式创建节点。
- 思路:获取要给节点赋的值;给出递归终点条件;创建节点并赋值;创建左节点(递归);创建右节点(递归)。
BiTNode *CreateBiTree() // 创建任意二叉树(先序)
{
BiTNode *T;
int x;
printf("Enter data(-1 for no data):"); // 输入-1表示该节点为空
scanf("%d", &x);
if (x == -1) // 递归终止条件
return NULL;
T = (BiTree)malloc(sizeof(BiTNode)); // 创建节点并赋值
T->data = x;
printf("Enter left child of %d:", x); // 创建左子树
T->Lchild = CreateBiTree();
printf("Enter right child of %d:", x); // 创建右子树
T->Rchild = CreateBiTree();
return T;
}//CreateBiTree
由遍历引申出的问题
怎么确定二叉树的叶子节点个数?
- 遍历中可以访问到每个节点,我们在其中加入判断条件,当节点没有孩子就是叶子节点,计数器加一。
int count = 0; // 全局变量,充当计数器
void CountLeaf(BiTNode *T) // 求叶子节点个数
{
if (T)
{
if ((!T->Lchild) && (!T->Rchild))
count++;
CountLeaf(T->Lchild);
CountLeaf(T->Rchild);
}//if
}//CountLeaf
怎么确定二叉树的深度?
- 递归遍历左子树得到左子树的深度,再递归遍历右子树得到右子树的深度。取最大值加一,就是某个节点的深度。
int leftDepth = 0, rightDepth = 0; // 设置两个全局变量
int BiTreeDepth(BiTNode *T)
{
if (!T) // 节点为空,depth为0
return 0;
leftDepth = BiTreeDepth(T->Lchild); // 遍历左子树
rightDepth = BiTreeDepth(T->Rchild); // 遍历右子树
depth = max(leftDepth, rightDepth) + 1; // 取最大值加一
return depth;
}//BiTreeDepth
线索二叉树
线索二叉树怎么来的?
- 为了提高找某一节点的效率产生了线索二叉树。普通的二叉树在寻找某个节点需要从父到子逐个遍历。要是能直接找到一个节点的父节点和它的兄弟节点就会方便很多。线索二叉树就是这样的。
线索二叉树的存储结构?
typedef enum PointerTag{ Link, Thread } PointerTag;
// Link == 0 代表指针 Thread == 1 代表线索
typedef struct BiThrNode{
TElemType data;
struct BiThrNode *lchild, *rchild; // 左右指针
PointerTag LTag, RTag; // 左右标志
}BiThrNode, *BiThrTree;
- 存储结构相比于原来的二叉树多了两个枚举类型的值。这两个值就是为了充分利用节点空间而设计的。一个有左孩子没有右孩子的节点,它的右孩子指针域是空的。为了充分利用,我们让这个空的右孩子指针域指向该节点的后继。后继又是什么呢?假设我们把二叉树的中序遍历结果写出为acdgh,c的后继就是d,d的后继就是g。
- 所以,在线索二叉树中,我们规定:节点有孩子,对应的指针域就指向它的孩子,标志域写为0;若是左孩子指针域有空闲,那么这个空间就指向它的前驱,标志域写为1;相对应的,若是右孩子指针域有空闲,那么这个空间就指向它的后继,标志域写为1。
- 下面二叉树的中序遍历的结果为:a + b × c - d - e / f,那么节点 a 的左孩子指针域和 f 的右孩子指针域为空,应该让其空闲区域指向前驱后继,但是a没有前驱,f没有后继,那么我们新建一个节点T,让a的左域和f的右域去指向T。同时节点T的右域指向f,左域指向二叉树的根节点。这样,一个完整的“环”就构成了。这构成的就是中序线索二叉树。类似的还有先序线索二叉树和后序线索而二叉树,是根据何种的遍历序列决定的。对于先序或后序线索二叉树,可能没有T节点。请大家自行尝试,即可发现规律。
怎么遍历线索二叉树?
- 中序找后继法
Status Visit(TElemType e)
{
if(!e)
return ERROR;
printf("%c ",e);
return OK;
}
// 中序遍历二叉线索树
Status InOrderTraverse_Thr(BiThrTree T,int (* Visit)(TElemType e))
{ //T指向头结点,头结点的左链lchild指向根节点,可参见线索化算法。
//中序遍历二叉线索树T的非递归算法,对每个数据元素调用函数Visit。
BiThrTree p;
p = T->lchild; //p指向根节点
while (p != T) {//空树或遍历结束时,p == T
while (p->LTag == Link)
p = p->lchild;
if(!Visit(p->data)) //访问左子树为空的结点
return ERROR;
while (p->RTag == Thread && p->rchild != T)
{
p = p->rchild;
Visit(p->data); //访问后继结点 这里为啥不判定失败?
}
p = p->rchild;
}
return OK;
}//InOrderTraverse_Thr
怎么创建线索二叉树?
char arr3[] = {'-','+','a','\0','\0','*','b','\0','\0','-','c','\0','\0','d','\0','\0','/','e','\0','\0','f','\0','\0'};
int arr_i = 0;
// 按照先序遍历创建线索二叉树
Status CreateBiTree(BiThrTree *T) {
char ch;
ch = arr3[arr_i++];
if(ch == '\0') {
(*T) = NULL;
}else{
if(!((*T) = (BiThrNode *)malloc(sizeof(BiThrNode))))
exit(OVERFLOW);
(*T)->data = ch; //生成根节点
(*T)->LTag = Link; //生成时给线索标志域赋初值
(*T)->RTag = Link; //默认都是Link
CreateBiTree(&((*T)->lchild));//构造左子树
CreateBiTree(&((*T)->rchild));//构造右子树
}
return OK;
}
怎么将普通的二叉树线索化?
// 线索化左子树和右子树
// 此处也可以把pre设置为全局变量,那就将InOrderThreading函数中的pre定义去掉
void InThreading(BiThrTree p,BiThrTree *pre)
{
if(p){
InThreading(p->lchild, pre); //左子树线索化
if(!p->lchild) { //若p指向结点没有左孩子,线索化前驱
p->LTag = Thread;
p->lchild = (*pre); //前驱线索化
}
if(!(*pre)->rchild){ //若p指向结点没有右孩子,线索化后继
(*pre)->RTag = Thread;
(*pre)->rchild = p; //后继线索化
}
(*pre) = p; //保持pre指向p的前驱
InThreading(p->rchild, pre); //右子树线索化
}
}
// 中序线索化
Status InOrderThreading(BiThrTree *Thrt,BiThrTree T)
{ // 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点。
BiThrTree pre = NULL;
if(!((*Thrt) = (BiThrNode *)malloc(sizeof(BiThrNode))))
exit(OVERFLOW);
(*Thrt)->LTag = Link; (*Thrt)->RTag = Thread; //建头结点
(*Thrt)->data = '#';
(*Thrt)->rchild = (*Thrt); //右指针回指
if(!T) {
(*Thrt)->lchild = (*Thrt); //若二叉树为空,则左指针回指(就是指向自己)
}else {
(*Thrt)->lchild = T;
pre = (*Thrt);
InThreading(T, &pre); //中序遍历进行中序线索化
pre->rchild = (*Thrt); pre->RTag = Thread; //最后一个结点线索化
(*Thrt)->rchild = pre;
}
return OK;
}
树和森林
树的存储结构
- 双亲表示法
- 孩子表示法
- 多重链表
- 孩子链表
- 孩子兄弟表示法
树与二叉树的转换
- 将树转换成二叉树
- 将二叉树转换成树
- 将森林转换成二叉树
- 将二叉树转换成森林
哈夫曼树
什么是哈夫曼树?
基本概念
哈夫曼树构造方法
哈夫曼编码译码