二叉树
二叉树
二叉树的概念
二叉树是n(n≥0)个结点的有限集
或者是空集(n= O),或者由一个根结点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成
- 二叉树结构最简单、规律性最强
- 所有树都能转为唯一对应的二叉树,具有一般性,解决了树的存储结构及其运算中存在的复杂性
特点:
- 每个结点最多含有两个孩子(二叉树中不存在度大于2的结点)
- 子树有左右之分,次序不能颠倒
- 二叉树可以是空集合,根可以有空的左子树或空的右子树
二叉树不是树的特殊情况,是两个概念 :
二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分
(二叉树的每个结点位置或者说次序都是固定的,可以是空,但不可以说没位置)
树当结点只有一个孩子时,就无需区分是左还是右的次序
(树的结点位置相对于其他结点来说的,没有别的结点时就无所谓左右)
例:具有三个结点的二叉树可能有几种不同形态?普通树呢?
-
二叉树具有五种形态:2*2+1
-
树有两种形态:1+1
二叉树的五种基本形态:
- 空二叉树
- 根和空的左右子树
- 根和左子树
- 根和右子树
- 根和左右子树
二叉树的性质
-
在二叉树的第i层最多有2^(i-1)个结点;最少有1个结点
-
深度为k的二叉树最多有2^k-1个结点;最少有k个结点
-
对于任何一个二叉树T,如果其叶子树为n0,度为2的结点数为n2,则n0=n2+1
总边数为总结点数-1(根结点往上没有边)
例:
特殊形式二叉树
顺序存储方式下可以复原
满二叉树
深度为k的二叉树仅有2^k-1个结点
编号规则:从上到下,从左至右
-
每层都满
-
叶子结点在最底层
完全二叉树
深度为k具有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号1~n结点位置一一对应
例:
在满二叉树中,从最后一个结点开始,连续去掉任意个结点即是一个完全二叉树
因此,满二叉树一定是完全二叉树
特点:
- 叶子结点只可能分布在层次最大的两层上
- 对任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1
- 具有n个结点的完全二叉树深度为⌊log2 n⌋+1
- 对任一结点i,双亲结点编号是⌊i/2⌋,左孩子结点编号是2i,右孩子结点编号是2i+1
二叉树的抽象数据类型定义
ADT Binary Tree{
数据对象D:具有相同特性的数据元素集合
数据关系R:若D=∅,则R=∅ ;
若D≠∅,则R={H};H是如下二元关系:
root唯一 //关于根的说明
Dj∩Dk= ∅ //关于子树不相交的说明
...... //关于数据元素的说明
...... //关于左子树和右子树的说明
基本操作P:
CreateBiTree(&T,definition)
初始条件:definition给出二叉树T的定义
操作结果:按definition构造二叉树T
PreOrderTraverse(T)
初始条件:二叉树T存在
结果:先序遍历T,对每个结点访问一次
lnOrderTraverse(T)
初始条件:二叉树T存在
操作结果:中序遍历T,对每个结点访问一次
PostOrderTraverse(T)
初始条件:二叉树T存在
操作结果:后序遍历T,对每个结点访问一次
}ADT Binary Tree
二叉树的应用:
- 数据压缩:将数据文件转化为二进制形式
- 求解表达式的值
二叉树的顺序存储结构
按满二叉树的结点编号作为数组下标;适合于存储满二叉树和完全二叉树
缺点:深度为k且仅有k个结点的单支树需要长度为2^k-1的一维数组
定义
#define MAXSIZE 100
typedef int SqBiTree[MAXSIZE];
SqBiTree bt;
二叉树的链式存储结构
二叉链表
lchild+data+rchild
在n个结点的二叉链表中,有n+1个空指针域(2n-(n-1))
定义
typedef struct Binode{
int data;
struct Binode *lchild,*rchild;
}BiNode,*BiTree;
三叉链表
lchild+data+parent+rchild
定义
typedef struct Tritnode{
int data;
struct Tritnode *lchild,*parent,*rchild;
}TritNode,*TriTree;
二叉树的遍历
遍历/周游:顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问(不破坏原来的数据结构)一次,且仅被访问一次,其目的是得到树中所有结点的线性排列
依次遍历二叉树的三个组成部分(L左子树、D根节点、R右子树)就是遍历了整个二叉树
共有6种:DLR、LDR、LRD、DRL、RDL、RLD(前三种与后三种是一样的)
若规定先左后右 (递归操作)
-
DLR:先(根)序遍历:根左右
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
-
LDR:中(根)序遍历:左根右
-
中序遍历左子树
-
访问根节点
-
中序遍历右子树
-
-
LRD:后(根)序遍历:左右根
-
后序遍历左子树
-
后序遍历右子树
-
访问根节点
-
例:
先序遍历:ABELDHMIJ
中序遍历:ELBAMHIDJ
后序遍历:LEBMIHJDA
例:
先序遍历:ABDGCEHF
中序遍历:DGBAEHCF
后序遍历:GDBHEFCA
例:
(表达式的前缀表示/波兰式)先序遍历:-+a*b-cd/ef
(表达式的中缀表示)中序遍历:a+b*c-d-e/f
(表达式的后缀表示/逆波兰式)后序遍历:abcd-*+ef/-
二叉树的重建
先序中序重建二叉树
- 若二叉树中各结点的值均不相同,则二叉树的先序、中序、后续都是唯一的
- 由二叉树的先序和中序,或由二叉树的后序和中序可以确定唯一一棵二叉树
先序:A B C D E F G H I J
中序:C D B F E A I H G J
先确定根,再确定左右子树
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct TreeNode{
char data;
struct TreeNode *lchild,*rchild;
}*Tree;
void PostOrder(Tree T){
if(T){
PostOrder(T->lchild);
PostOrder(T->rchild);
printf("%c",T->data);
}
}
void Rebuild(Tree *T,char *prestr,char *instr,int l1,int h1,int l2,int h2){
*T=(Tree)malloc(sizeof(Tree));
(*T)->data=prestr[l1];
int root;
for (root = 0; root <h2 ; root++) {
if(instr[root]==prestr[l1]) break;
}
if((root-l2)!=0){ //左子树是否为空
Rebuild(&(*T)->lchild,prestr,instr,l1+1,l1+root-l2,l2,root-1);
} else (*T)->lchild=NULL;
if((h2-root)!=0){ //右子树是否为空
Rebuild(&(*T)->rchild,prestr,instr,h1-h2+root+1,h1,root+1,h2);
} else (*T)->rchild=NULL;
}
int main(){
Tree T=NULL;
char prestr[30],instr[30];
scanf("%s %s",prestr,instr);
int len1=strlen(prestr);
int len2=strlen(instr);
Rebuild(&T,prestr,instr,0,len1-1,0,len2-1);
PostOrder(T);
}
后序中序重建二叉树
中序:B D C E A F H G
后序:D E C B H G F A
根据带有空叶子结点的先序序列重建二叉树
ab##cd#gf###e##
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct TreeNode{
char data;
struct TreeNode *lchild,*rchild;
}*Tree;
int k=0;
void PreRebuild(Tree *T,char *prestr){ //根据带空叶子结点的先序序列重建二叉树
if(prestr[k]=='#'){
k++;
*T=NULL;
}
else{
*T=(Tree)malloc(sizeof(Tree));
(*T)->data=prestr[k];
k++;
PreRebuild(&(*T)->lchild,prestr);
PreRebuild(&(*T)->rchild,prestr);
}
}
void PreOrder(Tree T){
if(T){
printf("%c ",T->data);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
int main(){
Tree T=NULL;
char prestr[30];
scanf("%s",prestr);
PreRebuild(&T,prestr);
printf("\n");
PreOrder(T);
}
算法实现
如果去掉输出语句,从递归角度看,三种算法是完全相同的,或者说这三种算法的访问路径是相同的,只是访问时机不同
从虚线的出发点到终点的路径上,每个结点经过三次
第一次经过时访问=先序遍历
第二次经过时访问=中序遍历
第三次经过时访问=后序遍历
-
时间效率O(n)(每个结点只访问一次)
-
空间效率O(n)(栈占用最大辅助空间)
先序遍历
void PreOrderTraverse(BiTree T){
if(T){
printf("%d\t",T->data);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
}
中序遍历
递归算法:
void InOrderTraverse(BiTree T){
if(T){
InOrderTraverse(T->lchild);
printf("%d\t",T->data);
InOrderTraverse(T->rchild);
}
}
非递归算法:
- 建立一个栈
- 根结点进栈,遍历左子树
- 根结点出栈,输出根结点,遍历右子树
#define MAXSIZE 100
typedef struct Binode{
int data;
struct Binode *lchild,*rchild;
}BiNode,*BiTree;
typedef struct {
int *top;
int *base;
int stacksize;
}SqStack;
void InitStack(SqStack *S){
S->base=malloc(sizeof(int)*MAXSIZE);
if(!S->base) printf("Error");
S->top=S->base;
S->stacksize=MAXSIZE;
}
int StackEmpty(SqStack S){
if(S.top==S.base) return 1;
else return 0;
}
void PreOrderTraverse(BiTree T){
if(T){
printf("%d\t",T->data);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
}
void Push(SqStack *S,BiTree p){
*(S->top)= p->data;
S->top++;
}
void Pop(SqStack *S,BiTree p){
S->top--;
p->data=*(S->top);
}
void InOrderTraverse(BiTree T){
BiTree p=T;
SqStack s;
InitStack(&s);
if(p||!StackEmpty(s)){
if(p){
Push(&s,p);
p=p->lchild;
} else{
Pop(&s,p);
printf("%d",p->data);
p=p->rchild;
}
}
}
后序遍历
void PostOrderTraverse(BiTree T){
if(T){
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
printf("%d\t",T->data);
}
}
二叉树的层次遍历
从根结点开始从上到下(逐层),从左至右依次访问每个结点
每个结点仅访问一次
例:
层次遍历结果:abfcdgeh
设计思路:
- 将根结点进队
- 队不空时循环:从队中出队一个结点*p,访问
- 若*p有左孩子,左孩子结点进队
- 若*p有右孩子,右孩子结点进队
#include <stdio.h>
#include <stdlib.h>
typedef struct TreeNode{
char data;
struct TreeNode *lchild,*rchild;
}*Tree;
typedef struct {
Tree Que[50];
int front;
int rear;
}Queue;
Queue Q;
void CreateTree(Tree *T){
char a;
fflush(stdin);
scanf("%c",&a);
if(a=='#') *T=NULL;
else{
*T=(Tree)malloc(sizeof(Tree));
(*T)->data=a;
CreateTree(&((*T)->lchild));
CreateTree(&((*T)->rchild));
}
}
void PreOrder(Tree T){
if(T){
printf("%c ",T->data);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
void IniQueue(){
Q.rear=0;
Q.front=0;
}
void EnQueue(Tree T){
Q.Que[++Q.rear]=T;
}
Tree DeQueue(){
return Q.Que[++Q.front];
}
int empty(){
return Q.rear==Q.front;
}
void LevelOrder(Tree T){
Tree p;
EnQueue(T);
while(!empty()){
p=DeQueue();
printf("%c ",p->data);
if(p->lchild) EnQueue(p->lchild);
if(p->rchild) EnQueue(p->rchild);
}
}
int main(){
Tree T=NULL;
CreateTree(&T);
PreOrder(T);
printf("\n");
IniQueue();
LevelOrder(T);
}
二叉树的建立
已知一个先序序列,所构建的二叉树不唯一
根据所补充空结点的位置,形成的二叉树可唯一
例:
ABC##DE#G##F###
int CreateTree(BiTree T){
char ch;
printf("Please input the ch:\n");
scanf("%c",&ch);
if(ch=='#') T=NULL;
else{
T=(BiTree)malloc(sizeof(BiNode)); //生成根结点
T->data=ch;
CreateTree(T->lchild); //构造左子树
CreateTree(T->rchild); //构造右子树
}
return 1;
}
return:返回上一层的调用
二叉树的建立需要使用二级指针
#include <stdio.h>
#include <stdlib.h>
typedef struct TreeNode{
char data;
struct TreeNode *lchild,*rchild;
}Tree;
void CreateTree(Tree **T){
char a;
fflush(stdin);
scanf("%c",&a);
if(a=='#') *T=NULL;
else{
*T=(Tree*)malloc(sizeof(Tree));
(*T)->data=a;
CreateTree(&((*T)->lchild));
CreateTree(&((*T)->rchild));
}
}
void PreOrder(Tree *T){
if(T){
printf("%c ",T->data);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
int main(){
Tree *T=NULL;
CreateTree(&T);
PreOrder(T);
}
复制二叉树
- 如果是空树,递归结束
- 否则,申请新的结点空间,复制根结点
- 递归复制左子树
- 递归复制右子树
int CopyTree(BiTree T){
if(T==NULL) return 0;
else{
T=(BiTree )malloc(sizeof(BiNode));
CopyTree(T->lchild);
CopyTree(T->rchild);
}
return 1;
}int CopyTree(BiTree T,BiTree NewT){
if(T==NULL) {
NewT=NULL;
return 0;
}
else{
NewT=(BiTree )malloc(sizeof(BiNode));
NewT->data=T->data;
CopyTree(T->lchild,NewT->lchild);
CopyTree(T->rchild,NewT->rchild);
}
return 1;
}
计算二叉树的深度
- 如果是空树则深度为0
- 否则,递归计算左子树的深度为m,递归计算右子树的深度为n,二叉树的深度为max{m,n}+1
int Depth(BiTree T){
int m,n;
if(T==NULL) return 0;
else{
m=Depth(T->lchild);
n=Depth(T->rchild);
if(m>n) return (m+1);
else return (n+1);
}
}
计算二叉树结点总数
- 如果是空树,结点为0
- 否则,结点个数为左子树结点个数+右子树结点个数+1
int NodeCount(BiTree T){
if(T==NULL) return 0;
else return (NodeCount(T->lchild)+NodeCount(T->rchild)+1);
}
计算二叉树叶子结点数
- 如果是空树,叶子结点数为0
- 否则,叶子结点个数为左子树叶子结点个数+右子树叶子结点个数
int LeafCount(BiTree T){
if(T==NULL) return 0;
else if (T->lchild==NULL&&T->rchild==NULL) return 1;
else return LeafCount(T->lchild)+LeafCount(T->rchild);
}
线索二叉树
利用二叉链表中空的指针域找到前驱和后继结点:(一个二叉链表中有n+1个空指针域)
若某个结点左孩子为空,则将空的左孩子指针域改为指向其前驱;若某个结点右孩子为空,则将空的右孩子指针域改为指向其后继 ,这种改变指向的指针称为线索
线索化:对二叉树按某种遍历次序使其变为线索二叉树的过程
为区分lchild与rchild的指向,增加ltag、rtag:
- ltag=0,lchild指向其左孩子
- ltag=1,lchild指向其前驱
- rtag=0,rchild指向其右孩子
- rtag=1,rchild指向其后继
结点结构:lchild+ltag+data+rtag+rchild
增设一个头结点:
ltag=0,lchild指向根结点
rtag=1,rchild指向遍历序列中的最后一个结点
遍历序列中的第一个结点的lchild域和最后一个结点的rchild域都指向头结点
线索二叉树的定义
typedef struct BiThrnode{
int data;
int ltag,rtag;
struct BiThrnode *lchild,*rchild;
}BiThrNode,BiThrTree;
二叉树排序树
顺序: 左<根<右
二叉排序树的插入
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct TreeNode{
int data;
struct TreeNode *lchild,*rchild;
}*Tree;
void CreateTree(Tree *T){
int a;
fflush(stdin);
scanf("%d",&a);
if(a==0) *T=NULL;
else{
*T=(Tree)malloc(sizeof(Tree));
(*T)->data=a;
CreateTree(&((*T)->lchild));
CreateTree(&((*T)->rchild));
}
}
void PreOrder(Tree T){
if(T){
printf("%d ",T->data);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
void OrderTree(Tree T,int n){ //二叉排序树的插入
while (T){
if((T)->data==n) return;
else if(((T)->data<n) && (T)->rchild){
T=(T)->rchild;
}
else if(((T)->data>n) && (T)->lchild){
T=(T)->lchild;
} else break;
}
Tree p=(Tree)malloc(sizeof(Tree));
if(n>(T)->data){
(T)->rchild=p;
}
if(n<(T)->data){
(T)->lchild=p;
}
p->data=n;
p->lchild=NULL;
p->rchild=NULL;
}
int main(){
Tree T=NULL;
CreateTree(&T);
PreOrder(T);
OrderTree(T,8);
printf("\n");
PreOrder(T);
}
哈夫曼树/最优二叉树
- 结点的路径长度:两结点间路径上的分支数
- 树的路径长度:根结点到每个结点的路径长度之和,记为:TL
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树;路径长度最短的不一定是完全二叉树
- 权:将树中结点赋予一个有某种含义的数值,这个数值称为该结点的权
- 结点的带权路径长度:从根结点到该结点直接路径长度与该点权的乘积
- 树的带权路径长度:树中所有叶子结点带权路径长度之和,记为:WPL
哈夫曼树是带权路径长度(WPL)最小的二叉树 (度相同的树相比较)
-
满二叉树不一定是哈夫曼树
-
哈夫曼树权值越大的结点离根越近
-
具有相同带权结点的哈夫曼树不唯一
例:
构造哈夫曼树
哈夫曼树权值越大的结点离根越近
贪心算法:构造哈夫曼树时首先选择权值小的叶子结点
哈夫曼算法:
- 构造森林全是根
- 选用两小造新树:新二叉树根结点权值是左右子树根结点权值之和
- 删除两小添新树
- 重复2,3直至森林中只剩一棵树
例:
- 哈夫曼树中只有度为0(n个)和度为2(n-1个)的结点,没有度为1的结点
- 包含n个叶子结点的哈夫曼树共有2n-1个结点
哈夫曼树构造的实现
采用顺序存储结构——一维结构数组(不使用0下标,数组大小为2n)
typedef struct {
int weight;
int parent,rc,lc;
}HTNode,*HuffmanTree;
void Select(HuffmanTree HT,int n ,int *s1,int *s2){
*s1=*s2=0;
int min1=100,min2=100;
for (int i = 1; i <=n ; i++) {
if(HT[i].weight<min1){
min2=min1;
min1=HT[i].weight;
*s2=*s1;
*s1=i;
} else if (HT[i].weight<min2){
min2=HT[i].weight;
*s2=i;
}
}
}
void CreateHuffmanTree(HuffmanTree HT,int n){
int m,s1,s2;
if(n<=1) return;
m=2*n-1;
HT=malloc(m+1);
for (int i = 1; i <=m ; i++) { //初始化
HT[i].lc=0;
HT[i].rc=0;
HT[i].parent=0;
}
printf("Please input the weight:\n");
for (int j = 1; j <=n ; j++) {
scanf("%d",&HT->weight);
}
for (int k = n+1; k <=m ; k++) {
Select(HT,k-1, &s1, &s2);
HT[s1].parent=k;
HT[s2].parent=k;
HT[k].lc=s1;
HT[k].rc=s2;
HT[k].weight=HT[s1].weight+HT[s2].weight;
}
}
哈夫曼编码
若在通讯时将编码设计成不等长的二进制编码,即让待串字符串出现频率较多的字符采用尽可能短的编码
为避免重码:必须使任意字符的编码都不是另一字符编码的前缀——前缀编码
哈夫曼编码:前缀编码(字符都是叶子结点)+总长最短/最优前缀码(带权路径长度最短)
步骤:
- 统计字符集中每个字符在电文中出现的平均概率
- 将每个字符的概率值作为权值,构造哈夫曼树
- 在哈夫曼树的每个结点的左分支标0,右分支标1;把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
例:
哈夫曼编码的实现
从叶子结点出发往上找双亲结点,如果该结点是左孩子则标注0,右孩子标注1,直至往上到根结点(双亲是否为0)
翻转所得编码即为哈夫曼编码
#define MAXSIZE 100
typedef struct {
int weight;
int parent,rc,lc;
}HTNode,*HuffmanTree;
typedef struct {
char bit[MAXSIZE];
int start;
char ch;
}HCode;
void Select(HuffmanTree HT,int n ,int *s1,int *s2){
*s1=*s2=0;
int min1=100,min2=100;
for (int i = 1; i <=n ; i++) {
if(HT[i].weight<min1){
min2=min1;
min1=HT[i].weight;
*s2=*s1;
*s1=i;
} else if (HT[i].weight<min2){
min2=HT[i].weight;
*s2=i;
}
}
}
void CreateHuffmanTree(HuffmanTree HT,int n){
int m,s1,s2;
if(n<=1) return;
m=2*n-1;
HT=malloc(m+1);
for (int i = 1; i <=m ; i++) { //初始化
HT[i].lc=0;
HT[i].rc=0;
HT[i].parent=0;
}
printf("Please input the weight:\n");
for (int j = 1; j <=n ; j++) {
scanf("%d",&HT->weight);
}
for (int k = n+1; k <=m ; k++) {
Select(HT,k-1, &s1, &s2);
HT[s1].parent=k;
HT[s2].parent=k;
HT[k].lc=s1;
HT[k].rc=s2;
HT[k].weight=HT[s1].weight+HT[s2].weight;
}
}
void CreateHuffmanCode(HuffmanTree HT,HCode code[],int n){
int c,f;
HCode cd;
for (int i = 1; i <=n ; ++i) {
cd.start=n-1;
c=i;
f=HT[i].parent;
while (f!=0){
cd.start--;
if(HT[f].lc==c){
cd.bit[cd.start]='0';
}
else{
cd.bit[cd.start]='1';
}
c=f;
f=HT[f].parent;
}
code[i]=cd;
}
}
哈夫曼编码应用
数据的编码(数据压缩)与解码
编码:
- 输入各字符及其权值
- 构造哈夫曼树——HT[i]
- 进行哈夫曼编码—HC[i]
- 查HC[i],得到各字符的哈夫曼编码
解码:
- 构造哈夫曼树
- 依次读入二进制码
- 读入0,则走向左孩子;读入1,则走向右孩子
- 一旦到达某叶子时,即可译出字符
- 再从根出发继续译码,指导结束