1.1定义
1.2基本术语
1.3二叉树定义
特点:
①每个结点最多有两个孩子(二叉树中不存在度大于2的结点)。
②子树有左右之分,次序不能颠倒。
③二叉树可以是空集合,跟可以有空的左子树或者空的右子树。
注:二叉树不是树的特殊情况,二叉树的子树要区分左子树和右子树,而树无需区分。
例子:
具有三个节点的二叉树有五种不同形态。
树有两种形态
1.4二叉树的5种形态
【案例1】数据压缩问题
将数据文件转换成0、1组成的二进制串,称之为编码。这些编码可以用哈夫曼树来实现。
如:
【案例2】利用二叉树求解表达式的值
性质3:利用边数推理得来。
性质4表明了完全二叉树节点数n与完全二叉树深度k之间的关系
性质5:表明了完全二叉树中双亲结点编号与孩子结点编号之间的关系
定义:一棵二叉树拥有最多结点为满二叉树。
定义:深度为k的具有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号为1~n的结点对应时,成为完全二叉树。
注:根据定义满二叉树是完全二叉树。
例:
实现描述:按满二叉树的结点层次编号,依次存放二叉树中数据元素。
【类型定义】
//二叉树顺序存储表示
#define MAXTSIZE 100
Typedef TElemType SqBiTree[MAXSTIZE];
SqBiTree bt;
例(不是满二叉树时):
顺序存储缺点:
特点:结点间关系蕴含在存储位置,适于满二叉树和完全二叉树。
二叉树链式存储的结点,包含了两个后继:一个左孩子和一个右孩子,还有一个前驱:一个双亲。
1.二叉链表
存储方式需要经常操作后继元素,可以定义一个结点包含:左孩子(lchild),右孩子(rchild)和数据元素本身(data)。
【定义实现】
typdef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild;//左右孩子指针
}BiNode,*BiTree;
二叉树的链式存储示意图
由于每个结点都有两个指针域(左孩子跟右孩子),所以当有n个结点时,必定有2n个链域。这里除了根节点,其余结点都会有一个相应的链连接到双亲结点,所以一共有n-1条链 ,那么空指针的个数为 2n-(n-1) 个。
在n个结点的二叉树链表中,有 n+1 个空指针。
2.三叉链表
存储方式需要经常用到前驱元素,可以定义一个结点包含:左孩子(lchild),右孩子(rchild)和数据元素本身(data)以及父亲结点(parent)。
【定义】
typedef struct TriTree{
TElemType data;
struct TriNode *lchild,*rchild,*parent;
}TriNode,*TriTree;
注:“ 访问 ”定义很广泛,可以是对接点的各种处理(如:输出接点信息,修改接点数据值等),但要求这种访问不破坏原来的数据结构。
二叉树每个结点可归结为三个组成部分:根节点、左子树、右子树
设:L—左子树,D—根结点,R—右子树。
则遍历整个二叉树方案:DLR、LDR、LRD、DRL、RDL、RLD。
若规定先左后右,则只有三种情况:DLR—根左右(先序遍历)、LDR—左根右(中序遍历)、LRD—左右根(后序遍历)。
注:根据根的遍历优先,分为先序遍历、中序遍历、后续遍历。
【先序遍历例子】
【中序遍历】
【后序遍历】
【例题】
先序:A B D G C E H F
中序:D G B A E H C F
后序:G D B H E F C A
【例题2】用二叉树表示算术表达式
先序—前缀表示(波兰式): - + a × b - c d / e f
中序—中缀表示:a + b × c - d - e / f
后序—后缀表示(逆波兰式):a b c d - × + e f / -
【例】已知二叉树的先序和中序序列,构造出相应的二叉树
先序:A B C D E F G H I J
中序:C D B F E A I H G J
【解析】
由于先序是根左右序列,中序是左根右序列。所以由上束可以推出,A为根节点,C D B F E为A的左子树全部元素, I H G J 为A的右子树全部元素。
接着推出 A左子树 CDBFE,根据先序与中序序列定义,可以推出B位根节点,CD为B的左子树全部元素,FE为B的右子树全部元素。
依照这个方法分别对每个元素团细分为根节点、左子树右子树,分解到每个结点只有一个元素时,结果如下
1.先序遍历
【算法解析】
【算法实现】
//先序
Status PreOrderTraverse(BiTree T){
if(T==NULL) returnOK;//为空二叉树
else{
visit(T);//访问根节点,例如输出根节点:printf("%d\t",t->data);
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild); //递归遍历右子树
}
}
【算法例子】
void pre(BiTree T){
if(T=NULL)
return OK;
else{
printf("%d\t",T->data); //打印
pre(T->lchild); //递归遍历左子树
pre(T->rchild); //递归遍历右子树
}
}
2.中序遍历
相比于先序遍历,中序遍历仅需调换访问根的次序到左子树右子树中间。
【算法实现】
//中序
Status InOrderTraverse(BiTree T){
if(T==NULL) return OK;//已是空树
else{
InOrderTraverse(T->lchild);//递归遍历左子树
visit(T);//访问根结点
InOrderTraverse(T->rchild);//递归遍历右子树
}
}
3.后序遍历
相比于先序遍历,中序遍历仅需调换访问根的次序到左子树右子树最后。
【算法实现】
//后序
Status PostOrderTraverse(BiTree T){
if(T==NULL) return OK;//已是空树
else{
InOrderTraverse(T->lchild);//递归遍历左子树
InOrderTraverse(T->rchild);//递归遍历右子树
visit(T);//访问根结点
}
}
4.遍历分析
每个结点只访问一次。时间复杂度:O(n)。
在递归过程中,未访问结点需要临时存放到栈,所以栈占用最大辅助空间为空间复杂度。空间复杂度:O(n)。
1.中序遍历非递归算法
【算法思想】
①建立一个栈
②根结点进栈,遍历左子树。
③根结点出栈,输出根节点,遍历右子树。
【算法实现】
Status InOrderTraverse(BiTree T){
BiTree p;
InitStack(S);
p=T;
while(p || !StackEmpty(S)){
if(p){
Push(S,p);
p=p->lchild;
}else{
Pop(S,q);
printf("%c" ,q->data);
p=q->rchild;
}
}//while
return OK;
}
层次遍历:从根节点开始,从上到下每一个深度顺序访问,每个深度按照从左到右访问每个结点。每个结点只访问一次。
【算法思想】
使用一个队列:
①将根结点进队。
②队不空时循环:从队列中出列一个结点*p,访问它;
—若它有左孩子结点,将左孩子结点进队;
—若它有右孩子结点,将右孩子结点进队。
【算法实现】
//队列定义
typedef struct{
BTNode data[MaxSize]; //存放队中元素
int front,rear; //队头和队尾
}SqQueue; //顺序循环队列类型
//二叉树层次遍历算法
void LevelOrder(BTNode *b){
BTNode *p;
SqQueue *qu; //定义一个队列
InitQueue(qu);//初始化队列
enQueue(qu,b); //根节点入队
while(!QueueEmpty(qu)){//若队列不为空
deQueue(qu,p); //出队结点p
printf("%c",p->data); //访问p结点
if(p->lchild !=NULL) //先判断左结点是否为空
enQueue(qu,p->lchild);
if(p->rchild !=NULL) //再判断有结点是否为空
enQueue(qu,p->rchild);
}
}
1.二叉树建立
这里用“ # "充当空结点字符。则对上图先序遍历得到字符串:ABC##DE#G##F###
【算法实现】
Status CreateBiTree(BiTree &T){
scanf(&ch); //键盘输出字符
if(ch == "#") T==NULL;
else{
T=new BiTNode;
if(!T){//获取空间失败
exit(OVERFLOW);
}
T->data = ch; //生成当前根结点
CreateBiTree(T->lchild);//构造左子树
CreateBiTree(T->rchild);//构造右子树
}
return OK;
}
2.复制二叉树
【算法描述】
①如果是空树,递归结束;
②否则,申请新结点,复制根结点
—先递归复制左子树
—再递归复制右子树
int Copy(BiTree T, BiTree &NewT){
if(T==NULL){
NewT = NULL;
return 0; //结束
}else{
NewT=new BiTNode;
NewT->data = T->data;//复制值
Copy(T->lchild,NewT->lchild);//复制左子树
Copy(T->rchild,NewT->rchild);//复制右子树
}
}
3.计算二叉树深度
【算法描述】
①如果是空树,则深度为0;
②否则,递归计算左子树深度记录为m,递归计算右子树的深度记为n,取n与m较大者+1。
【算法实现】
int Depth(BiTree T){
if(T=NULL)
return 0;//如果树为空返回0
else{
m=Depth(T->lchild);
n=Depth(T->rchild);
if(m>n) return(m+1);
else return(n+1);
}
}
4.计算二叉树结点总个数
【算法描述】
①如果是空树,结点个数为0。
②否则,结点个数=左子树的结点个数+右子树的节点个数+1。
【算法实现】
int NodeCount(BiTree T){
if(T ==NULL)
return 0;
else{
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
}
5.计算二叉树叶子结点总数
【算法描述】
①如果是空树,则叶子结点为0。
②否则,为左子树叶子结点个数+右子树叶子结点个数。
int LeadCount(BiTree T){
if(T==NULL)
return 0;
if(T->lchild == NULL & T->rchild == NULL)
return 1;
else
return LeafCount(T->lchild)+LeafCount(T->rchild);
}
空结点利用:
①如果某个结点左孩子为空,则将空的左孩子指针改为指向其前驱。
②如果某个结点右孩子为空,则将空的右孩子指针改为指向其后继。
【例】
为了区分lchild和rchild指针是指向孩子指针,还是前驱或后继指针,对二叉链表中增设两个标志域,ltag和rtag,且约定:
—ltag=0:lchild指向该节点左孩子;ltag=1:lchild指向该节点前驱。
—rtag=0:rchild指向该节点右孩子;rtag=1:rchild指向该节点后继。
节点结构如图:
【定义实现】
typedef struct BiThrNode{
int data;
int ltag,rtag;
struct BiThrNode *lchild,*rchild;
}BiThrNode,*BiThrTree;
如先序线索二叉树:
【练习】
由于这里H的前驱与G的后继为空,这里增设了一个头结点:ltag=0,lchild指向根节点;rtag=1,rchild指向遍历序列最后一个结点。并且遍历中第一个结点的lchild和最后一个节点的rchild都指向头结点。
【定义】结构数组,存放树的结点,每个结点两个域:
—数据域:存放结点本身信息。
—双亲域:指示本结点的双亲结点在数组中的位置。
【例子】
【特点】找双亲容易,找孩子难。
【实现】
//结点结构
typedef struct PTNode{
TElemType data; //数据域
int parent; //双亲域
}PTNode;
//树结构
#defind MAX_TREE_SIZE 100
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int r,n;//记录根结点位置和结点个数
}PTree;
把每个结点的孩子结点排列起来,看成一个线性表,用单链表存储。则n个结点有n个孩子链表。n个头指针又组成一个线性表,用顺序表存储。
【例】
【实现】
//孩子节点
typedef struct CTNode{
int child;
struct CTNode *next;
}*ChildPtr;
//双亲节点
typedef struct{
TElemType data;
ChildPtr firstchild; //孩子链表头指针
}CTBox;
//树结构
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r; //结点树与结点位置
}CTree;
【特点】找孩子容易,找双亲难。可以在双亲结点中再增加一个成员,指向这个结点的双亲。
【示意图】带双亲的孩子链表
【定义】用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向第一个孩子结点和下一个兄弟结点
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
【例子】
①加线:在兄弟之间加一连线。
②抹线:对每个结点,除了其左孩子外,去除其余孩子之间的关系。
③旋转:以树根结点为轴心,整树顺时针转45°。
【例】
①加线:若p结点是某个双亲结点的左孩子,则将p结点的右子树的右子孙都与p的双亲结点链接起来。
②抹线:抹掉原二叉树中双亲域右孩子之间的连线。
③调整:将结点按层次排列,形成树结构。
【例】
①将各棵树分别转换成二叉树
②将每棵树的根结点用线相连
③以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构。
①抹线:将二叉树中根结点与其右孩子连线,以及沿右分支搜索到的所有右孩子间连线全部抹掉,是之变成孤立的二叉树。
②还原:将孤立的二叉树还原成树。
—先根:若树不为空,则先访问根结点,然后依次先根遍历各棵子树。
—后根:若树不为空,则先依次后根遍历各棵子树,然后访问根结点。
—按层次遍历:若树不为空,则自上而下自左自右访问树中每个结点。
【例】
将森林看成三个部分构成:第一棵树的根结点;第一棵树子树森林;其他树构成的森林。
—先序:先访问森林第一棵树的结点,先序遍历森林中第一棵树的子树森林,先序遍历其他树构成森林。
—中序:中序遍历森林中第一棵树的子树森林,再访问森林第一棵树的结点,中序遍历其他树构成森林。
【例子】
当每次输入量很大时,若学生成绩数据共10000个,比较总次数为10000×(1×5%+2×15%+3×40%+4×10%)=31500次。
若将判别树进行修改:
此时比较总次数为22000次。
综上,不同判别树效率是不一样的。而效率最高的判别树称为" 哈夫曼树 “,也称” 最优二叉树 "。
【例】
二叉树中若结点数目相同,完全二叉树是路径长度最短的二叉树。
【例】有4个结点a,b,c,d,权值分别为7,5,2,4,构造以此4个结点为叶子结点的二叉树。
则带权路径长度:WPL=7×2+5×2+2×2+4×2=36。
则带权路径长度:WPL=4×2+7×3+5×3+2×1=46。
最优树:即带权路径长度(WPL)最短的树。
注:“ 带权路径长度最短 ” 是在 “ 度相同 ”的树中比较而得出的,因此优最优二叉树、最优三叉树等。
哈夫曼树:最优二叉树,即带权路径长度(WPL)最短的二叉树。
【例】
满二叉树不一定是哈夫曼树。具有相同带权结点的哈夫曼树不唯一。
【特点】哈夫曼树中权值越大的叶子离根越近。
贪心算法:构造哈夫曼树首先选择权值小的叶子结点。
【思路】
①根据给定的权值,将每个结点单独称为一个森林。
②选用最小的两个结点组成新树,删除这两棵树,同时二叉树加入森林中。
③重复②,直至森林中只有一棵树为止,这棵树称为哈夫曼树。
【例】有4个结点a ,b ,c , d,权值分别为7 ,5 , 2 ,4,构造哈夫曼树
哈夫曼树的结点只有度为0或2,没有度为1的结点。
包含n个叶子结点的哈夫曼树中共有2n-1个结,点。
【例】有5个结点a ,b ,c ,d ,e,权值分别为7,5,5,2,4,构造哈夫曼树
1.结点类型定义
typedef struct{
int weight;
int parent,lch,rch;
}HTNode,*HuffmanTree;
2.构造算法
【示意图】
【实现】
void CreatHuffmanTree(HuffmanTree HT,int n){//构造哈夫曼树
if(n<=1) return;
m=2*n-1; //一共2n-1个结点
HT=new HTNode[m+1]; //由于0单元未用,所以需要+1。HT[m]表示根结点
for(i=1;i<=m;++i){//初始化表
HT[i].lch=0;
HT[i].rch=0;
HT[i].parrnt=0;
}
for(i=1;i<=n;++i)
scanf("%c",&HT[i].weight);//输出前n个元素的权值
//初始化结束,开始建立哈夫曼树
for(i=n+1;i<=m;i++){
Select(HT,i-1,s1,s2);//在HT中选择两个双亲域为0,且权值最小的结点,并返回他们在HT中的序号s1和s2。
HT[s1].parent=i;//将两个最小结点结合,双亲结点指向当前i结点。即删除s1,s2组成新树
HT[s2].parent=i;
HT[i].lch=s1;//双亲结点的左孩子右孩子设置
HT[i].rch=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;//i的权值为左右孩子权值之和
}
}
例如:设置等长编码A—00,B—01,C—10,D—11。
则:ABACCDA—00 01 00 10 10 11 00
倘若将编码设计长度不等的二进制编码,即让待传送字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。
例如:设置不等长编码A—0,B—00,C—1,D—01。
则:ABACCDA—0 00 0 1 1 01 0 。这里会发生重码,导致从编码转换为字符时发生错误。
注:所以在设计长度不等的编码时,必须使任一字符编码都不是另一个字符的编码的前缀。
(1)统计字符集中每个字符在电文中出现的平均概率。
(2)利用哈夫曼树的特点:权越大的叶子离根越近,将每个字符的概率值作为权值,构成哈夫曼树。则概率越大的结点,路径越短,编码越短。
(3)把哈夫曼树的每个分支标上0或1:结点左分支标0,右分支标1。把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符编码。
【例】要传输的字符集D={C , A , S , T , ; }其对应的出现频率w={2 , 4 , 2 , 3 , 3}
构造哈夫曼树并标左右分支后得
则T—00,; —01,A—10,C—110,S—111。这些为哈夫曼编码。
例如电文为CAS;CAT;SAT;AT,其编码为110 10 111 01 110 10 00 01 111 10 00 01 10 00
反之编码为1101000,得出CAT
s1].weight+HT[s2].weight;//i的权值为左右孩子权值之和
}
}
### 10.4哈夫曼编码
* 在远程通讯中,要将待传字符转换成由二进制的字符串:
例如:设置等长编码A—00,B—01,C—10,D—11。
则:ABACCDA—00 01 00 10 10 11 00
倘若将编码设计长度不等的二进制编码,即让待传送字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。
例如:设置不等长编码A—0,B—00,C—1,D—01。
则:ABACCDA—0 00 0 1 1 01 0 。这里会发生重码,导致从编码转换为字符时发生错误。
**注**:所以在设计长度不等的编码时,必须使任一字符编码都不是另一个字符的编码的前缀。
* 使用哈夫曼编码可以避免这个问题:
(1)统计字符集中每个字符在电文中出现的平均概率。
(2)利用哈夫曼树的特点:权越大的叶子离根越近,将每个字符的概率值作为权值,构成哈夫曼树。则概率越大的结点,路径越短,编码越短。
(3)把哈夫曼树的每个分支标上0或1:结点左分支标0,右分支标1。把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符编码。
【例】要传输的字符集D={C , A , S , T , ; }其对应的出现频率w={2 , 4 , 2 , 3 , 3}
构造哈夫曼树并标左右分支后得
则T—00,; —01,A—10,C—110,S—111。这些为哈夫曼编码。
例如电文为CAS;CAT;SAT;AT,其编码为110 10 111 01 110 10 00 01 111 10 00 01 10 00
反之编码为1101000,得出CAT
学习视频:数据结构——王卓;
参考文献:数据机构C语言版第2班——严蔚敏