第四章 树和二叉树
一、二叉树
1、二叉树的基本概念(逻辑结构)
- 二叉树的定义
- 二叉树是n(n>=0)个结点的有限集合。n=0时,二叉树为空树;n>0时,由根结点和两个互不相交的被称为根的左子树和右子数组成。左子树和右子树也分别是一棵二叉树
- 五种基本形态
- 二叉树的分类
- 1、满二叉树:一棵高度为h,且含有2^h-1个结点的二叉树(编号顺序从上至下,从左至右)
对于编号为i的结点,若存在,其双亲的编号为i / 2(取下界);左孩子为2i,右孩子为2i+1 - 2、完全二叉树:一棵高度为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号1~n的结点一一对应。(编号顺序从上至下,从左至右)
- 3、二叉排序树:一棵二叉树若非空,则对任意结点若存在左子树或右子数,其左子树上所有结点的关键字均小于该结点,右子树上所有结点的关键字均大于该结点
- 4、平衡二叉树:树上任意结点的左子树和右子树的深度之差不超过1
- 1、满二叉树:一棵高度为h,且含有2^h-1个结点的二叉树(编号顺序从上至下,从左至右)
- 二叉树的性质
- 1、非空二叉树上的叶子结点数等于度为2的结点数+1,即n0 = n2 + 1;n = n0 + n1 + n2,,,
- 2、在非空二叉树的第k层上最多有2 ^ (k − 1) 个结点
- 3、高度为h的二叉树最多有 2^h − 1个结点
- 4、具有n个节点的完全二叉树的高度k = ⌊log.2. n⌋ + 1
- 5、对于含n个结点的完全二叉树中编号为i(1≤i≤n)的节点:
- 如果i=1,则i结点是这棵完全二叉树的根,没有双亲,否则其双亲的编号为 ⌊i / 2⌋
- 如果2i>n,则i节点没有左孩子,否则其左孩子的编号为2i
- 如果2i+1>n,则i节点没有右孩子,否则其有孩子的编号为2i+1
- 二叉树的存储结构
- 顺序存储:用一组连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素。
- 在完全二叉树中依次编号,对于结点i,若存在左孩子,则编号为2i;若存在右孩子,则编号为2i+1。结点与其数组下标对应
- 对于非完全二叉树,可以添加一个不存在的空结点将其补为完全二叉树,在数组中用0表示
- 顺序存储最坏情况下会非常浪费存储空间,比较适合完全二叉树
- 链式存储:用链表来存放一棵二叉树,二叉树中每个结点用链表的一个链结点来存储。含有n个结点的二叉链表中,有n+1个空链域
- 1、二叉树结点结构
- 2、二叉树链式存储结构示意图
- 3、二叉树链式存储结构
typedef struct BiTNode{ ElemType data;//数据域 struct BiTNode *lchild,*rchild;//左孩子指针与右孩子指针 }BiTNode,*BiTree;
- 1、二叉树结点结构
- 顺序存储:用一组连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素。
2、二叉树的遍历
按某条搜索路径访问树中的每个结点,树的每个结点均被访问一次,而且只访问一次。
A为根结点、B为左子树、C为右子树;先序遍历是ABC(根节点排最先,然后同级先左后右);中序遍历是BAC(先左后根最后右);后序遍历是BCA(先左后右最后根)。
-
先序遍历
若二叉树非空:访问根结点、先序遍历左子树、先序遍历右子树
递归算法:void PreOrder(BiTree T){ if(T!=NULL){ visit(T);//访问根结点 PreOrder(T->lchild);//先序遍历左子树 PreOrder(T->rchild);//先序遍历右子树 } }
-
中序遍历
若二叉树非空:中序遍历左子树、访问根结点、中序遍历右子树- 递归算法:
void InOrder(BiTree T){ if(T!=NULL){ InOrder(T->lchild);//中序遍历左子树 visit(T);//访问根结点 InOrder(T->rchild);//中序遍历右子树 } }
- 中序遍历非递归算法(借助栈)
1、初始时,依次扫描根结点的所有左侧结点并将它们一一进栈
2、出栈一个结点,访问它
3、扫描该结点的右孩子结点并将其进栈
4、依次扫描右孩子结点的所有左侧结点并将它们一一进栈
5、反复该过程直至栈空为止void InOrder(BiTree T){ InitStack(S); BiTree p = T; while(p || IsEmpty(S)){ if(p){ Push(S,p); p = p->lchild; }else{ Pop(S,p); visit(p); p = p->rchild; } } }
- 递归算法:
-
后序遍历
若二叉树非空:后序遍历左子树、后序遍历右子树、访问根结点
递归算法:void PostOrder(BiTree T){ if(T!=NULL){ PostOrder(T->lchild);//后序遍历左子树 PostOrder(T->rchild);//后序遍历右子树 visit(T);//访问根结点 } }
-
层次遍历
1、初始将根入队并访问根结点,然后出队
2、若有左子树,则将左子树的根入队
3、若有右子数,则将右子树 根入队
4、然后出队,访问根结点
5、反复该过程直至栈空为止void levelOrder(BiTree T){ InitQueue(Q); BiTree p; EnQueue(Q,T); while(!isEmpty(Q)){ DeQueue(Q,p); visit(p); if(p->lchild!=NULL){ EnQueue(Q,p->lchild); } if(p->rchild!=NULL){ EnQueue(Q,p->rchild); } } }
-
由遍历序列构造二叉树
先(后)序遍历序列和中序遍历序列可以确定一棵二叉树
1、在先序序列中,第一个节点是根结点
2、根结点将东旭遍历序列划分为两个部分
3、然后在先序序列中确定两部分的结点,并且两部分的第一个结点分别为左子树的根和右子树的根
4、在子树中递归重复该过程,便能唯一确定一棵二叉树 -
线索二叉树
参考
线索化:若无左子树,则将左指针指向其前驱结点;若无右子树,则将其指针指向其后继结点
线索:利用原来的空链域存放指针,指向树中其他结点。
-
ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;
-
rtag为0时指向该结点的右孩子,为1时指向该结点的后继;
typedef struct ThreadNode{ ElemType data; struct ThreadNode *lchild,*rchild; int ltag,rtag; }ThreadNode,*ThreadTree;
这种结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表
-
中序线索二叉树线索化
void InThread(ThreadTree &p,ThreadTree &pre){//p为线索二叉树的根结点,pre为对应结点的前驱结点 if(p!=NULL){ InThread(p->lchild,pre);//递归线索化的左子树,传入参数为左子树的根结点以及前驱结点 if(p->lchild==NULL){ p->lchild = pre; p->ltag = 1; } if(pre!=NULL&&pre->rchild==NULL){//前驱结点的右指针 pre->rchild = p; pre->tag = 1; } pre = p; InThread(p->rchild,pre);//递归调用该函数实现右子树的线索化 } } void CreatInThread(ThreadTree T){//T为根结点 ThreadTree pre = NULL; if(T!=NULL){ InThread(T,pre);//调用函数实现线索化 pre->rchild = NULL; pre->rtag = 1; } }
-
中序线索二叉树遍历
ThreadNode *Firstnode(ThreadNode *p){//寻找第一个节点(最左侧) while(p->ltag == 0) p = p->lchild; return p; } ThreadNode *Nextnode(ThreadNode *p){//寻找后继结点 if(p->rtag == 1){ return Fistnode(p->rchild); }else{ return p->rchild; } } void Inorder(ThreadNode *T){ for(ThreadNode *p = Firstnode(T);p!=NULL;p=Nextnode(p)) visit(p); }
-
3、二叉树的应用
- 二叉树排序(Binary Sort Tree)
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
中序遍历序列:左子树结点值<根结点值<右子树结点值- 构造二叉排序树:读入一个元素并建立结点,若二叉树为空将其作为根结点;若二叉树不为空,当值小于根结点时,插入左子树;当值大于根结点时,插入右子树;当值相等时不进行插入
void Creat_BST(BiTree &T,KeyType str[],int n){ T = NULL; int i = 0; while(i<n){ BST_Insert(T,str[i]); i++; } }
- 查找:二叉树非空时,查找根结点,若相等,则查找成功;若不等,则当结点小于根结点值时,查找左子树;当大于根结点的值时,查找右子树。当查找到叶子结点仍没查找到相应的值,则查找失败。
时间复杂度:O(h)
非递归算法:BSTNode *BST_Search(BiTree T,ElemType key,BSTNode *&p){//查找的二叉排序树、查找关键字、保存查找到结点的双亲结点,指针引用类型的变量是 p=NULL; while(T!=NULL&&key!=T->data){ p = T; if(key < T->data) T = T->lchild; else T = T->rchild; } return T; }
- 插入:若二叉排序树为空,则直接插入结点;若二叉排序树非空,当值小于根结点,插入左子树;当值大于根结点时,插入右子树;当值等于根结点时不进行插入
int BST_Insert(BiTree &T,KeyType k){ if(T==NULL);{ T = (BiTree)malloc(sizeof(BSTNode)); T->key = k; T->lchild = T->rchild = NULL; return 1; } else if(k == T->key) return 0;//插入失败 else if(k < T->key) return BST_Insert(T->lchild,k); else return BST_Insert(T->rchild,k); }
- 删除
- 1、若被删除结点z是叶子结点,则直接删除
- 2、若被删除结点z只有一颗子树,则让z的子树成为z父结点的子树,代替z结点
- 3、若被删除结点z有两颗子树,则让z的中序序列直接后继代替z,并删去直接后继结点
- 查找效率
平均查找长度(ASL)取决于树的高度
{2,1,4,3}
ASL=(1+2*2+3)/ 4 = 2
最好时间复杂度:O(㏒2 n)
{1,2,3,4}
ASL=(1+2+3+4)/ 4 = 2.5
最坏时间复杂度:O(n)
- 构造二叉排序树:读入一个元素并建立结点,若二叉树为空将其作为根结点;若二叉树不为空,当值小于根结点时,插入左子树;当值大于根结点时,插入右子树;当值相等时不进行插入
- 平衡二叉树
AVL,任意结点的平衡因子的绝对值不超过1(左子树高度-右子树高度)
高度为h的最小平衡二叉树的结点数 N h = N (h-1) + N (h-2) + 1
N 0 = 0;N 1 = 1;- 平衡二叉树的判断
利用递归的后序遍历过程:
1)、判断左子树是一棵平衡二叉树
2)、判断右子树是一棵平衡二叉树
3)、判断以该结点为根的二叉树为平衡二叉树
判断条件:若左子树和右子树均为平衡二叉树,且左子树与右子树高度差的绝对值小于等于1,则平衡void Judge_AVL(BiTree bt,int &balance,int &h){ int bl = 0,br = 0, hl = 0,hr = 0;//左子树的平衡性、右子树的平衡性、左子树的高度、右子树的高度 if(bt = NULL){//空树 h = 0;//高度为0 balance = 1;//平衡 }else if(bt->lchild == NULL && bt->rchild == NULL){//只有一个根结点 h = 1;//高度为1 balance = 1;//平衡 }else{ Judge_AVL(bt->lchild,bl,hl); Judge_AVL(bt->rchild,br,hr); if(hl>hr) h = hl + 1; else h = hr + 1; if(abs(hl - hr)<2 && bl == 1 && br == 1)//高度差的绝对值小于2并且左右子树皆平衡 balance = 1;//满足则平衡 else balance = 0; } }
- 平衡二叉树的插入:先插入再调整(每次调整最小不平衡子树)
- LL平衡旋转(右单旋转)
- RR平衡旋转(左单旋转)
- LR平衡旋转(先左后右双旋转)
- RL平衡旋转(先右后左双旋转)
- LL平衡旋转(右单旋转)
- 平衡二叉树的判断
- 哈弗曼树及哈弗曼编码
- 树的带权路径长度:WPL,树中所有叶节点的带权路径长度之和
- 哈弗曼树:也称最优二叉树,含有n个带权叶子结点带权路径长度最小的二叉树。哈弗曼树并不唯一,所以每个字符对应的哈弗曼编码也不唯一,但带权路径长度相同且最优
- 哈弗曼树的性质:
- 1)每个初始结点都会成为叶子结点,双支结点都为新生成的结点
- 2)权值越大离根结点越近,反之权值越小离根结点越远
- 3)哈弗曼树中没有节点的度为1
- 4)n个叶子结点的哈弗曼树的结点总数为2n-1,其中度为2的结点数为n-1
- 编码
- 前缀编码: 没有一个编码是另一个编码的前缀
- 树的带权路径长度:WPL,树中所有叶节点的带权路径长度之和
二、树和森林
1、树的基本概念(逻辑结构)
- 树是n(n>=0)个结点的有限集合,n=0时,称为空树。n个结点的树中只有n-1条边
- 任意非空树满足:
- 1、有且仅有一个根结点
- 2、当n>1时,其余结点可分为m个互不相交的有限集合,其中每一个集合本身又是一棵树,称为根节点的子树
- 树的特点
- 有且仅有一个结点没有前驱(父亲结点),该结点称为树的根。
- 除根外,其余的每一个结点都有且仅有一个前驱;
- 除根外,每个结点都通过唯一的路径连到根上(否则有环)。
- 基本术语
- 祖先结点和子孙结点
- 双亲结点和孩子结点
- 兄弟结点
- 度:树中一个结点的子结点的个数
- 树的度:树中最大度数
- 分支结点:度大于0的结点
- 叶子结点:度等于0的结点
- 结点的层次:从根结点开始
- 结点的高度:从下往上数
- 结点的深度:从上往下数
- 树的高度(深度)是树中结点的最大层数
- 有序树和无序树: 树中同层结点从左而右排列,其次序不容互换,这种树称为有序树;同层结点的次序随意,这种树称为无序树。
- 路径:树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的(自上而下)
- 路径长度:路径上所经历边的个数
- 森林:m棵互不相交的树的集合
- 树的性质
- 树中的结点等于所有结点的度数+1
- 度为m的树中第i层上至多有m^(i-1)个结点(i>=1)
- 高度为h的m叉树至多有(m^h-1)/(m-1)个结点
- 具有n个结点的m叉树的最小高度为log.m.(n(m-1)+1)(取上界)
2、树的存储结构
- 双亲表示法
采用一组连续的存储空间来存储每个结点,同时在每个结点中增加一个伪指针,指示双亲结点在数组中的位置。根结点的下标为0,其伪指针域为-1#define MAX_TREE_SIZE 100 typedef struct{//每一个结点的结构体 ElemType data; int parent;//双亲节点下标的整型变量 }PTNode; typedef struct{//这棵树的结构体 PTNode nodes[MAX_TREE_SIZE];//保存所有结点信息 int n;//该树当中结点的数量 }PTree;
- 孩子表示法
将每个结点的孩子结点都用单链表连接起来形成一个线性结构,n个结点具有n个孩子链表#define MAX_TREE_SIZE 100 typedef struct{//每一个孩子结点的结构体 int child;//孩子结点下标 struct CNode *next;//下一个孩子结点的指针 }CNode; typedef struct{//每一个结点存放的数据元素和单链表的头指针 ElemTypr data; struct CNode *child; }PNode; typedef struct{//这棵树的结构体 PNode nodes[MAX_TREE_SIZE]; int n; }CTree;
- 孩子兄弟表示法
以二叉链表作为树的存储结构,又称二叉树表示法(左孩子,右兄弟)typed struct CSNode{ ElemType data; struct CSNode *firstchild,*nextsilbing; }CSNode,CSTree;
- 优缺点
双亲表示法
孩子表示法
孩子兄弟表示法优点 缺点 寻找结点的双亲结点效率高 寻找结点的孩子结点效率低 寻找结点的孩子结点效率高 寻找结点的双亲结点效率低 寻找结点的孩子结点效率高 寻找结点的双亲结点效率低
3、树和森林的遍历
- 树的遍历:按照某种方式访问树中的每个结点,且访问一次
- 先根遍历:若树非空,则
- 1、先访问根结点,
- 2、再按从左到右的顺序遍历根结点的每棵子树。
- 树的先根遍历序列与这棵树对应的二叉树的先序遍历序列相同。
- 后根遍历:若树非空,则
- 1、先按从左到右的顺序遍历根结点的每棵子树,
- 2、再访问根结点。
- 后根遍历序列与这棵树对应的二叉树的中序遍历序列相同。
- 先根遍历:若树非空,则
- 森林的遍历
- 先序遍历:若森林非空,则
- 1、访问森林中第一棵树的根结点、
- 2、先序遍历第一棵树的子树森林、
- 3、先序遍历除去第一棵树之后剩余的树构成的子树森林
- 森林的的先序遍历序列与森林对应的二叉树的先序遍历序列相同。
- 中序遍历:若森林非空,则
- 中序遍历第一棵树的根结点的子树森林、
- 访问第一棵树的根结点、
- 中序遍历除去第一棵树之后剩余的树构成的子树森林
- 森林的中序遍历序列与森林对应二叉树的中序遍历序列相同。
- 先序遍历:若森林非空,则
4、树和森林及二叉树的转换
- 树与二叉树的转换
每个结点左指针指向它的第一个孩子结点,右指针指向它在树中相邻兄弟结点。(二叉树转换为树为逆过程)
- 森林与二叉树的转换
将每一刻棵树转换为二叉树,将每棵二叉树的根依次作为上一棵二叉树的右子数。(二叉树转换为森林为逆过程)
5、树的应用-并查集
-
并查集:一种简单的集合表示(通常用树的双亲表示法作为并查集的存储结构)
Initial(S):将集合中的每个元素都初始化为只有一个单元素的子集合
Union(S,Root1,Root2):把集合S中的子集合(互不相交)Root2并入子集合Root1中
Find(S,x):查找集合S中单元素x所在的子集合,并返回该子集合的名字#define SIZE 100 int UFSets[SIZE]; void Initial(int S[]){ for(int i=0;i<size;i++) S[i] = -1;//将每一个元素双亲结点下标初始化为-1 } int Find(int S[],int x){//查找x元素所在的子集合,即x子集合的根结点的下标 while(S[x]>=0)//如果双亲结点下标为正数,说明不是根结点,继续查找 x = S[x]; return x;//返回x子集合根结点的下标 } void Union(int S[],int Root1,int Root2){ S[Root2] = Root1;//把Root2的双亲结点下标修改为Root1 }