树的基本概念
树的定义:树是一种非线性的数据结构,是由一个根和若干个不相交的子树构成的,每一棵子树也是一棵树,即树的定义是递归的,树的结点数可以为0
树的基本术语:
结点:包含数据元素和指向子树的分支
结点的度:结点拥有的子树或者说叫分支的个数
树的度:书中各结点度的最大值
叶子结点:又叫做终端结点,指的是度为零的结点(没有分支)
非终端结点:又叫做分支结点,度不为零的结点
孩子:结点的子树的根
双亲:与孩子的定义相对应,结点的孩子的双亲就是该结点
兄弟:同一个双亲的孩子互为兄弟
祖先:从根到某结点的路径上的所有结点都是这个结点的祖先
子孙:以某结点为根的子树的全部结点,都是该结点的子孙
层次:从根开始,根为第一层,根的孩子为第二层,以此类推
树的高度:树中结点的的最大层次
结点的深度和高度:
1)结点的深度是从根结点到该结点路径上的结点个数
2)从某结点往下走可能到达多个叶子结点,对应了多条通往这些叶子结点的路径,其中最长的那条路径上结点的个数即为该结点在树中的高度
3)结节点的高度为树的高度
堂兄弟:双亲在同一层的结点互为堂兄弟
有序树:树中节点的子树从左到右是有次序的,不能交换,这样子叫做有序树
无需树:树中结点的子树没有顺序,可以任意交换
丰满树:丰满树即为理想平衡树,要求除了最底层外,其余层都是满的
森林:若干棵不相交的树的集合
树的存储结构
1、顺序存储结构
2、链式存储结构
二叉树
二叉树的定义:
-
每个结点最多只有两棵子树,即二叉树中结点的度只能为0,1,2,
-
子树有左右顺序之分,不能颠倒
满二叉树:如果所有的分支结点都有左孩子和右孩子,并且叶子结点都集中在二叉树的最下层
完全二叉树:树上所有结点满足二进制结构的二叉树,可以观察得到,所有的儿子结点的双亲都为儿子结点除以2得到的树,一棵完全二叉树可以由满二叉树按照“从右至左,从下至上”的顺序删除得到
二叉树的主要性质:
1、非空二叉树的叶子节点数等于双分支节点数加1
证明:设二叉树叶子节点数为\(n_0\),单分支节点数为\(n_1\),双分支结点数为\(n_2\),总结点数为\(n_0+n_1+n_2\)。在一棵二叉树中,所有结点的分支数等于单分支节点数加上双分支结点数的两倍,即总的分支数为\(n_0+n_1+n_2\)。由于二叉树中除根节点之外,每个结点都有惟一的一个分支指向它,因此二叉树中有总分支数=总结点数-1
由此可得:\(n_0+n_1+n_2-1=n_1+2n_2\)
化简得:\(n_0=n_2+1\)
2、二叉树的第\(i\)层上最多有\(2^{i-1}(i\geq1)\)个节点
3、高度(或深度)为k的二叉树最多有\(2^{k}-1(k\geq1)\)个结点。换句话说,满二叉树的前k层结点数量为\(2^k-1\)
4、卡特兰函数Catalan():给定n个结点,能构成\(h(n)\)种不同的二叉树, \(h(n)=\frac{C_{2n}^{n}}{n+1}\)
5、具有 \(n(n\geq1)\) 个结点的完全二叉树的高度(深度)为 \(\lfloor log_{2}n \rfloor+1\)
线索二叉树:n个结点的二叉树有n+1个空链域,将原来的空指针利用起来,指向结点的前驱或者后驱,对于不同的遍历顺序,把原来的树形结构变成线性结构,更方便多次遍历
ltag和rtag:
- 如果ltag=0,则表示lchild为指针,指向结点的左孩子;如果=1,则表示lchild为线索,指向结点的直接前驱
- 如果rtag=0,则表示rchild为指针,指向结点的右孩子;如果=1,则表示rchild为线索,指向结点的直接后驱
#include<bits/stdc++.h>
using namespace std;
const int maxSize=1e5+50;
//实际上线索二叉树就是将原来的二叉树变成了链表,方便多次遍历或者查询
typedef struct TBTnode{
char data;
//对于线索二叉树来说,如果ltag为0表示lchild是左儿子,否则是自己的前驱,rtag为0表示rchild是右儿子,否则是后驱
int ltag,rtag;
struct TBTnode *lchild;
struct TBTnode *rchild;
}TBTnode;
void Visit(TBTnode* p){
}
//二叉树的中序遍历构建线索二叉树
//代码中的pre意思上一个遍历过的结点
void InThread(TBTnode *p,TBTnode *&pre){
if(p!=NULL){
InThread(p->lchild,p);
if(p->lchild!=NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
p=p->rchild;
InThread(p,pre);
}
}
void createInThread(TBTnode *root){
TBTnode *pre=NULL;
if(root!=NULL){
InThread(root,pre);
//给最开始的头指针补充信息
pre->rchild=NULL;
pre->rtag=1;
}
}
//遍历中序线索二叉树
TBTnode *First(TBTnode *p){
while(p->ltag==0){
p=p->lchild;
}
return p;
}
TBTnode *Next(TBTnode *p){
if(p->rtag==0) return First(p->rchild);
else return p->rchild;
}
//由此得到中序线索二叉树上执行中序遍历的方法
void Inorder(TBTnode *root){
for(TBTnode *p=First(root);p!=NULL;p=next(p)) Visit(p);
}
//先序线索二叉树遍历
void PreThread(TBTnode *p,TBTnode *&pre){
if(p!=NULL){
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
if(p->ltag==0) PreThread(p->lchild,pre);
if(p->rtag==0) PreThread(p->rchild,pre);
}
}
void preorder(TBTnode *root){
if(root!=NULL){
TBTnode *p=root;
while(p!=NULL){
while(p->ltag==0){
Visit(p);
p=p->lchild;
}
visit(p);
p=p->rchild;
}
}
}
int main(){
return 0;
}
树和森林与二叉树的互相转换
树转换成二叉树:
- 将用一个结点的各孩子结点用线串起来
- 每个结点的第一个孩子作为这个结点的左结点,多余的结点作为左结点的右节点,也就是说每一个结点的右结点都是这个结点在树上的兄弟结点
二叉树转换成树,反过来操作就行了
森林转换成二叉树:
- 每一颗单独的树构建同上
- 每一个根结点的右结点是后面一棵森林的根结点
树和森林的遍历:
- 分为先序遍历和后序遍历,区别在于根结点是在前还是在后
- 注意森林的先序遍历是先遍历第一棵树,然后再遍历下一棵,后续同样
树与二叉树的应用
赫夫曼树和赫夫曼编码
基本概念:
- 路径:路径是从树中一个结点到另一个结点的分支所构成的路线
- 路径长度: 指的是路径上的分支数目
- 树的路径长度:根到每个结点的路径长度之和
- 带权路径长度:结点具有权值,权值乘以路径长度就是带权路径长度
- 树的带权路径长度:带权路径长度之和
赫夫曼树的构造方法:
给定n个权值,用这个n个权值来构造赫夫曼树的算法:
- 从n个结点选出最小的两个结点作为左右子树,中间结点为两个结点权值之和,将新的结点加入点集
- 重复进行操作,直到只剩下一棵树
赫夫曼树的特点
- 权值越大的结点距离根节点越近
- 树中没有度为1的结点。这类树被叫做正则二叉树
- 树的带权路径长度最短
赫夫曼编码:
以字符为叶子结点,其出现次数为权值,构造一棵赫夫曼树。对赫夫曼树的每个结点的左右分支进行编号,左0右1,则从根到每个结点的路径上的数字序列即为每个字符的编码。
赫夫曼编码当中任意字符的编码串都不是另一字符的编码串前缀,所以解码时候就不会出现歧义
解码过程:每次从当前起点出发,沿着数字在得出的赫夫曼树走,每次走到叶子结点的时候就得到了一个字符,之后再从下一个数字出发得到下一个字符
赫夫曼n叉树
对于结点大于等于2的待处理序列都可以得到赫夫曼二叉树,但是如果不能直接构造n叉树,那么就要补足0