《数据结构》 第五章 树与二叉树之 笔记
来源:互联网 发布:数据割接流程 编辑:程序博客网 时间:2024/06/10 00:07
第五章 树和二叉树
【学习重点】1 数的遍历;
2 二叉树的性质;
3 二叉树和数的存储表示;
4 二叉树的遍历及算法实现;
5 树与二叉树之间的转换;
6 哈夫曼树及应用。
【学习难点】1 二叉树遍历算法的非递归实现;
2 基于二叉树的遍历实现二叉树的其他操作;
3 线索二叉树;
4 有关树的算法。
5.1 数的逻辑结构
5.1.1 树的定义和基本术语
1. 数的定义
在树中常常将数据元素称为结点。
树是n(n>=0)个结点的有限集合。当n=0时,称为空树;任意一颗非空树满足以下条件:
(1) 有且只有一个特定的称为根的结点;
(2) 当n>1时,除根结点
2.树的基本术语
结点的度、树的度
结点的度:某节点所拥有的结点的个数。
树的度:树中各结点度的最大值。
叶子结点、分支结点
叶子结点(终端结点):度为0的结点。
分支结点(非终端结点):度不为0的结点。
孩子结点、双亲结点、兄弟结点
某节点的子树的根结点称为该结点的孩子结点;反之,该结点称为其孩子结点的双亲结点;具有同一个双亲的孩子结点互称为兄弟结点。
路径、路径长度
如果树的结点序列n1,n2,n3,…nk满足如下关系:结点ni是结点ni+1的双亲(1<=i<k)则把n1,n2…nk称为一条由n1至nk的路径。
路径长度:路径上经过的边数。 在树中路径是唯一的。
祖先、子孙
如果从结点x到结点y有一条路径,那么x就称为y的祖先,y称为x的子孙。以某结点为根的子树中的任意结点都是该结点的子孙。
结点的层数、树的深度(高度)
规定根结点的层数为1,对其余任何节点,若某结点在第k层,则其孩子结点在第k+1层。
树的深度(树的高度):树中所有结点的最大层数。
层序编号
将树中结点按照从上层到下层、同层从左到右的次序依次给他们编以从1开始的连续自然数,树的这种编号方式称为层序编号。显然,通过层序编号可以将一棵树变成线性序列。
有序树、无序树
如果一棵树中结点的各子树从左到右是有次序的,即若交换了结点各子树的相对位置,则构成不同的树,称这棵树为有序树;反之,称为无序树。
森林
m(m>=0)棵互不相交的树的集合构成森林。任何一棵树,删去根结点就变成森林。
树结构与线性结构的比较
线性结构 树结构
第一个数据元素 根结点(只有一个)
无前驱 无双亲
最后一个数据元素 叶子结点(可以有多个)
无后继 无孩子
其他数据元素 其他结点
一个前驱,一个后继 一个双亲,多个孩子
一对一 一对多
5.1.2 树的抽象数据类型定义
树的应用很广泛,在不同的实际应用中,树的基本操作不尽相同。下面给出一个树的抽象数据类型定义的例子,简单起见,基本操作只包含树的遍历,针对具体应用,需要重新定义其基本操作
ADTTree
Data
树是由一个根结点和若干棵子树构成, 树中结点具有相同数据类型及层次关系
Operation
InitTree
前置条件:树不存在
输入:无
功能:初始化一棵树
输出:无
后置条件:构造一个空树
DestroyTree
前置条件:树已存在
输入:无
功能:销毁一棵树
输出:无
后置条件:释放该树占用的存储空间
PreOrder
前置条件:树已存在
输入:无
功能:前序遍历树
输出:树的前序遍历序列
后置条件:树保持不变
PostOrder
前置条件:树已存在
输入:无
功能:后序遍历树
输出:树的后序遍历序列
后置条件:树保持不变
endADT
5.1.3 树的遍历操作
树中最基本的操作是遍历。
树的遍历:从根结点出发,按照某种次序访问树中所有结点,使得使得每一个结点被访问一次且仅访问一次。
遍历方式:前序(根)遍历、后序(根)遍历、层序(次)遍历。
1. 前序遍历:
若树为空,则空操作返回;否则
(1)访问根结点;
(2)按照从左到右的顺序前序遍历根结点的每一棵子;树。
2. 后序遍历:
若树为空,则空操作返回;否则
(1) 按照从左到右的顺序后序遍历根结点的每一棵子树;
(2) 访问根结点。
3. 层序遍历(广度遍历):
从树的第一层及根结点开始,自上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐
5.2 树的存储结构
5.2.1 双亲表示法
基本思想:用一维数组来存储树的各个结点(一般按层序存储),数组中的一个元素对应树中的一个结点,包括结点的数据信息以及该结点的双亲在数组中的下标。
data
parent
data:存储树中结点的数据信息
parent:存储该结点的双亲在数组中的下标
template <class DataType>
struct PNode
{
DataType data; //数据域
intparent; //指针域,双亲在数组中的下标
} ;
树的双亲表示法实质上是一个静态链表。
5.2.2孩子表示法
1. 多重链表表示法
2. 孩子链表表示法
链表中的每个结点包括一个数据域和多个指针域,每个指针域指向该结点的一个孩子结点。
方案一:指针域的个数等于树的度
方案二: 指针域的个数等于该结点的度
5.2.3 双亲孩子表示法
双亲孩子表示法:将双亲表示法和孩子链表表示法相结合的存储方法。
孩子链表的基本思想:把每个结点的孩子排列起来,看成是一个线性表,且以单链表存储,则n个结点共有n 个孩子链表。这 n 个单链表共有 n 个头指针,这 n 个头指针又组成了一个线性表,为了便于进行查找采用顺序存储。最后,将存放n 个头指针的数组和存放n个结点的数组结合起来,构成孩子链表的表头数组。
5.2.4 孩子兄弟表示法(二叉链表表示法)
方法是:链表中的每个结点除数据域外,还设置了两个指针分别指向该结点的第一个孩子和有兄弟。
5.3 二叉树的逻辑结构
5.3.1 二叉树的定义
二叉树:是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
特点:⑴ 每个结点最多有两棵子树;
⑵ 二叉树是有序的,其次序不能任意颠倒。
二叉树的基本形态(5种):空二叉树、只有一个根结点、根节点只有左子树、根节点只有右子树、根节点既有左子树又有右子树。
特殊的二叉树:
1.斜树
左斜树:所有结点都只有左子树的二叉树。
右斜树:所有结点都只有右子树的二叉树。
特点:每一层只有一个结点,所以,斜树的结点个数与其深度相同。
2. 满二叉树
满二叉树:在一棵二叉树中,所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上的二叉树。
特点:
(1)叶子只能出现在最下一层;
(2)只有度为0和度为2的结点。
满二叉树在同样深度的二叉树中结点个数最多
满二叉树在同样深度的二叉树中叶子结点个数最多
3. 完全二叉树
完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同。
特点:
(1). 叶子结点只能出现在最下两层且最下层的叶子结点都集中在二叉树的左面;
(2). 完全二叉树中如果有度为1的结点,只可能有一个,且该结点只有左孩子。
(3). 深度为k的完全二叉树在k-1层上一定是满二叉树。
(4). 在同样结点个数的二叉树中,完全二叉树的深度最小。
5.3.2 二叉树的基本性质
性质5-1 二叉树的第i层上最多有2i-1个结点(i≥1)。
性质5-2 一棵深度为k的二叉树中,最多有2k-1个结点,最少有k个结点。
注意:深度为k且具有2k-1个结点的二叉树一定是满二叉树,
深度为k且具有k个结点的二叉树不一定是斜树。
性质5-3 在一棵二叉树中,如果叶子结点数为n0,度为2的结点数为n2,则有:n0=n2+1。
性质5-4 具有n个结点的完全二叉树的深度为 log2n +1。
性质5-5 对一棵具有n个结点的完全二叉树中从1开始按层序编号,则对于任意的序号为i(1≤i≤n)的结点(简称为结点i),有:
(1)如果i>1,则结点i的双亲结点的序号为 i/2;如果i=1,则结点i是根结点,无双亲结点。
(2)如果2i≤n,则结点i的左孩子的序号为2i;
如果2i>n,则结点i无左孩子。
(3)如果2i+1≤n,则结点i的右孩子的序号为2i+1;如果2i+1>n,则结点i无右孩子。
5.3.3 二叉树的抽象数据类型定义
ADT BiTree
Data
由一个根结点和两棵互不相交的左右子树构成,
结点具有相同数据类型及层次关系
Operation
InitBiTree
前置条件:无
输入:无
功能:初始化一棵二叉树
输出:无
后置条件:构造一个空的二叉树
DestroyBiTree
前置条件:二叉树已存在
输入:无
功能:销毁一棵二叉树
输出:无
后置条件:释放二叉树占用的存储空间
PreOrder
前置条件:二叉树已存在
输入:无
功能:前序遍历二叉树
输出:二叉树中结点的一个线性排列
后置条件:二叉树不变
InOrder
前置条件:二叉树已存在
输入:无
功能:中序遍历二叉树
输出:二叉树中结点的一个线性排列
后置条件:二叉树不变
PostOrder
前置条件:二叉树已存在
输入:无
功能:后序遍历二叉树
输出:二叉树中结点的一个线性排列
后置条件:二叉树不变
LeverOrder
前置条件:二叉树已存在
输入:无
功能:层序遍历二叉树
输出:二叉树中结点的一个线性排列
后置条件:二叉树不变
endADT
5.3.4 二叉树的遍历操作
前序遍历:
若二叉树为空,则空操作返回;否则
(1)访问根结点;
(2)前序遍历根结点的左子树;
(3)前序遍历根结点的右子树。
中序遍历:
若二叉树为空,则空操作返回;否则
(1) 中序遍历根结点的左子树;
(2) 访问根结点;
(3) 中序遍历根结点的右子树。
后序遍历:
若二叉树为空,则空操作返回;否则
(1) 后序遍历根结点的左子树;
(2) 后序遍历根结点的右子树;
(3) 访问根结点。
层序遍历
从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问。
典例:已知一棵二叉树的前序序列和中序序列,构造该二叉树的过程如下:
1. 根据前序序列的第一个元素建立根结点;
2. 在中序序列中找到该元素,确定根结点的左右子树的中序序列;
3. 在前序序列中确定左右子树的前序序列;
4. 由左子树的前序序列和中序序列建立左子树;
5. 由右子树的前序序列和中序序列建立右子树。
5.4 二叉树的存储结构及实现
5.4.1 顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置(下标)应能体现结点之间的逻辑关系——父子关系。
二叉树的顺序存储结构一般仅存储完全二叉树
5.4.2 二叉链表
基本思想:令二叉树的每个结点对应一个链表结点,链表结点除了存放与二叉树结点有关的数据信息外,还要设置指示左右孩子的指针。
template <class DataType>
class BiTree
{
public:
BiTree(){root = Creat(root);} //构造函数,建立一棵二叉树
~BiTree(){Release(root);} //析构函数
voidPreOrder( ){PreOrder(root);} //前序遍历二叉树
voidInOrder( ){InOrder(root);} //中序遍历二叉树
voidPostOrder( ){PostOrder(root);} //后序遍历二叉树
voidLeverOrder( ); //层序遍历二叉树
private:
BiNode<DataType> *root; //指向根结点的头指针
BiNode<DataType> *Creat(BiNode<DataType> *bt); //构造函数调用
voidRelease(BiNode<DataType> *bt); //析构函数调用
voidPreOrder(BiNode<DataType> *bt) //前序遍历函数调用
voidInOrder(BiNode<DataType> *bt); //中序遍历函数调用
voidPostOrder(BiNode<DataType> *bt); //后序遍历函数调用
};
前序遍历——递归算法
template <class DataType>
void BiTree<DataType> ::PreOrder(BiNode<DataType> *bt)
{
if (bt== NULL) return; //递归调用的结束条件
else {
cout << bt->data; //访问根结点bt的数据域
PreOrder(bt->lchild); //前序递归遍历bt的左子树
PreOrder(bt->rchild); //前序递归遍历bt的右子树
}
}
中序遍历——递归算法
template <class DataType>
void BiTree<DataType> :: InOrder(BiNode<DataType> *bt)
{
if(bt == NULL) return; //递归调用的结束条件
else{
InOrder(bt->lchild); //中序递归遍历bt的左子树
cout << bt->data; //访问根结点bt的数据域
InOrder(bt->rchild); //中序递归遍历bt的右子树
}
}
后序遍历——递归算法
template <class DataType>
void BiTree<DataType> ::PostOrder(BiNode<DataType> *bt)
{
if (bt== NULL) return; //递归调用的结束条件
else{
PostOrder(bt->lchild); //后序递归遍历bt的左子树
PostOrder(bt->rchild); //后序递归遍历bt的右子树
cout << bt->data; //访问根结点bt的数据域
}
}
层序遍历
template <class DataType>
void BiTree<DataType> :: LeverOrder( )
{
front= rear = -1; //采用顺序队列,并假定不会发生上溢
if(root == NULL) return; //二叉树为空,算法结束
Q[++rear] = root; //根指针入队
while(front != rear) //当队列非空时
{
q = Q[++front]; //出队
cout << q->data;
if (q->lchild != NULL) Q[++rear] = q->lchild;
if (q->rchild != NULL) Q[++rear] = q->rchild;
}
}
构造函数——建立二叉树
template <class DataType>
BiTree :: BiTree( )
{
root= Creat(root);
}
template <class DataType>
BiNode<DataType>*BiTree<DataType>::Creat(BiNode<DataType> *bt)
{
cin>> ch; //输入结点的数据信息,假设为字符
if(ch == '# ') bt = NULL; //建立一棵空树
else{
bt = new BiNode; bt->data = ch; //生成一个结点,数据域为ch
bt->lchild = Creat(bt->lchild); //递归建立左子树
bt->rchild = Creat(bt->rchild); //递归建立右子树
}
returnbt;
}
析构函数——释放二叉链表
template <class DataType>
void BiTree<DataType>::Release(BiNode<DataType>*bt)
{
If(bt!=NULL){
Release(bt->lchild);
Release(bt->rchild);
Delete bt;
}
}
5.3.4 三叉链表
在二叉链表存储方式下,从某结点出发可以直接访问到它的孩子结点,但要找到它的双亲结点,则需要从根结点开始搜索,最坏情况下,需要遍历整个二叉链表。此时,应该采用三叉链表存储二叉树。在三叉链表中,每个结点由4个域组成。
优点:便于查找孩子结点和双亲结点。
缺点:相对二叉链表而言,增加了空间开销。
5.4.4线索链表
线索:将二叉链表中的空指针域指向前驱结点和后继结点的指针被称为线索。
线索化:使二叉链表中结点的空链域存放其前驱或后继信息的过程称为线索化。
线索链表:加上线索的二叉链表称为线索链表。
线索二叉树:加上线索的二叉树称为线索二叉树。
4种线索链表:前序线索链表、中序线索链表、后序线索链表、层序线索链表
将二叉树的抽象数据类型定义在中序线索链表上用C++中的类实现。
template <class DataType>
class InThrBiTree
{
public:
InThrBiTree( ); //构造函数,建立中序线索链表
~InThrBiTree( ); //析构函数,释放各结点的存储空间
ThrNode *Next(ThrNode<DataType> *p); //查找p的后继
voidInOrder( ); //中序遍历线索链表
private:
ThrNode *root; //指向线索链表的头指针
ThrNode<DataType> *Creat(ThrNode<DataType> *bt);
voidThrBiTree(ThrNode<DataType> *bt,
ThrNode<DataType> *pre); //构造函数调用
};
1.构造函数
建立二叉链表(带线索标志)Creat
ThrNode <DataType>*InThrBiTree<DataType>::Creat(ThrNode<DataType> *bt)
{
Cin>>ch;
If(ch==’#’) bt=NULL;
else{
bt=newThrNode;bt->data=ch;
bt->ltag=0;bt->rtag=0;
bt->lchild=Creat(bt->lchild);
bt->rchild=Creat(bt->rchild);
}
return bt;
}
中序线索化链表——ThrBiTree
template <class DataType>
void InThrBiTree<DataType>::ThrBiTree(ThrNode<DataType> *bt,
ThrNode<DataType> *pre)
{
if (bt == NULL) return;
ThrBiTree(bt->lchild, pre);
if (bt->lchild == NULL) { //对bt的左指针进行处理
bt->ltag = 1;
bt->lchild = pre; //设置pre的前驱线索
}
if (bt->rchild == NULL) bt->rtag = 1; //对bt的右指针进行处理
if (pre->rtag == 1) pre->rchild = bt; //设置pre的后继线索
pre = bt;
ThrBiTree(bt->rchild, pre);
}
伪代码:
1. 建立二叉链表,将每个结点的左右标志置为0;
2. 遍历二叉链表,建立线索;
2.1 如果二叉链表root为空,则空操作返回;
2.2 对root的左子树建立线索;
2.3 对根结点root建立线索;
2.3.1 若root没有左孩子,则为root加上前驱线索;
2.3.2 若root没有右孩子,则将root右标志置为1;
2.3.3 若结点pre右标志为1,则为pre加上后继线索;
2.3.4 令pre指向刚刚访问的结点root;
2.4 对root的右子树建立线索。
2.查找后继结点
中序线索链表查找后继——Next
template <class DataType>
ThrNode<DataType>*InThrBiTree<DataType> :: Next(
ThrNode<DataType> *p)
{
if (p->rtag == 1)
q = p->rchild; //右标志为1,可直接得到后继结点
else {
q = p->rchild; //工作指针q指向结点p的右孩子
while (q->ltag == 0) //查找最左下结点
q =q->lchild;
}
return q;
}
5.5 二叉树遍历的非递归算法
5.5.1 前序遍历非递归算法
伪代码:
1.栈s初始化;
2.循环直到root(p)为空且栈s为空
2.1 当root(p)不空时循环
2.1.1 输出root(p)->data;
2.1.2 将指针root(p)的值保存到栈中;
2.1.3 继续遍历root(p)的左子树
2.2 如果栈s不空,则
2.2.1 将栈顶元素弹出至root(p);
2.2.2 准备遍历root(p)的右子树;
二叉树前序遍历非递归——PreOrder
template <class DataType>
voidBiTree::PreOrder(BiNode<DataType> *root)
{
top = -1; //采用顺序栈,并假定不会发生上溢
while (root != NULL || top != -1)
{
while (root != NULL)
{
cout<<root->data;
s[++top] = root;
root = root->lchild;
}
if (top != -1) {
root = s[top--];
root = root->rchild;
}
}
}
5.5.2 中序遍历非递归算法
中序遍历:
template <class DataType>
voidBiTree::PreOrder(BiNode<DataType> *root)
{
top = -1; //采用顺序栈,并假定不会发生上溢
while (root != NULL || top != -1)
{
while (root != NULL)
{
s[++top] = root;
root = root->lchild;
}
if (top != -1) {
root = s[top--];
cout<<root->data;
root = root->rchild;
}
}
}
5.5.3 后序遍历非递归算法
后序遍历:
template <class DataType>
void BiTree<DataType> ::PostOrder(BiNode<DataType> *bt)
{
top = -1; //采用顺序栈,并假定栈不会发生上溢
while (bt != NULL || top != -1) //两个条件都不成立才退出循环
{
while (bt != NULL)
{
top++; s[top].ptr = bt;s[top].flag = 1; //root连同标志flag入栈
bt = bt->lchild;
}
while (top != -1 && s[top].flag == 2)
{
bt = s[top--].ptr; cout <<bt->data;
}
if (top != -1) {
s[top].flag = 2; bt =s[top].ptr->rchild;
}
}
}
5.6 树、森林与二叉树的转换
1.树转换为二叉树
方法:
(1) 加线——树中所有相邻兄弟结点之间加一条连线;
(2) 去线——对树中每个结点,只保留它与第一个孩子结点之间的连线,删去它与其他孩子结点之间的连线;
(3) 层次调整——以根节点为轴心,将树顺时针转动一定的角度,使之层次分明。
注意:树的前序遍历——二叉树的前序遍历
树的后序遍历——二叉树的中序遍历
2. 森林转换为二叉树
方法;
(1) 将森林中的每棵树转换成二叉树;
(2) 从第二棵二叉树开始,依次把后一棵二叉树的根节点作为前一棵二叉树根结点的右孩子,当所有二叉树连起来后,此时得到的二叉树就是由森林转换得到的二叉树。
3. 二叉树转换为树或森林
方法:
(1) 加线——若某结点x是其双亲y的左孩子,则把结点x的右孩子、右孩子的右孩子、……,都与结点y用线连起来;
(2) 去线——删去所有二叉树中所有双亲结点与右孩子结点的连线;
(3) 层次调整——整理由(1)(2)两步所得到的树或森林,使之层次分明。
4. 森林的遍历
森林有两种遍历方法:前序(根)遍历、后序(根)遍历。
5.7 应用举例
5.7.1二叉树的应用举例——哈夫曼树及哈夫曼编码
1. 哈夫曼树 也称最优二叉树
叶子结点的权值:对叶子结点赋予的一个有意义的数值量。
二叉树的带权路径长度:设二叉树具有n个带权值的叶子结点,从根节点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和。
哈夫曼树:给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树。
哈夫曼树特点:
(1) 权值越大的叶子结点越靠近根结点,权值越小的叶子结点越远离根结点;
(2) 只有度为0或度为2的结点,不存在度为1的结点。
哈夫曼算法 基本思想:
⑴ 初始化:由给定的n个权值{w1,w2,…,wn}构造n棵只有一个根结点的二叉树,从而得到一个二叉树集合F={T1,T2,…,Tn};
⑵ 选取与合并:在F中选取根结点的权值最小的两棵二叉树分别作为左、右子树构造一棵新的二叉树,这棵新二叉树的根结点的权值为其左、右子树根结点的权值之和;
⑶ 删除与加入:在F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到F中;
⑷ 重复⑵、⑶两步,当集合F中只剩下一棵二叉树时,这棵二叉树便是哈夫曼树。
性质:具有n个叶子结点的哈夫曼树共有2n-1个结点,其中有n-1个非叶子结点,他们是在n-1次的合并过程中生成的。
2. 哈夫曼编码
前缀编码:如果一组编码中任一编码都不是其他任何一个编码的前缀,我们称这组编码为前缀编码。
哈夫曼编码:构造一棵哈夫曼树,规定哈夫曼编码树的左分支代表0,右分支代表1,则从根结点到每个叶子结点所经过的路径组成的0和1的序列便为该叶子结点对应字符的编码,称为哈夫曼树。
5.7.2 树的应用举例——八枚硬币问题
- 《数据结构》 第五章 树与二叉树之 笔记
- 数据结构笔记之树与二叉树
- 数据结构笔记——第五章 树和二叉树
- 第五章 二叉树 练习题(数据结构与算法MOOC)
- 数据结构 第五章 树和二叉树
- 数据结构 第五章 树和二叉树
- 数据结构 第五章 树和二叉树
- 数据结构 第五章 树和二叉树
- 数据结构 第五章 树和二叉树
- 数据结构 第五章 树和二叉树
- 数据结构 第五章 树和二叉树
- 数据结构 第五章 树和二叉树
- 数据结构第五章树和二叉树
- 数据结构概述 第五章 二叉树
- 数据结构 第五章 数和二叉树
- 【数据结构笔记】三、树与二叉树
- 数据结构学习笔记之二叉树
- 数据结构学习笔记之二叉树
- NJUPT ACM 合法日期
- CodeForces 148D Bag of mice
- 之六:虚存管理中的抽象
- Go by Example: Hello World
- 栅格重采样
- 《数据结构》 第五章 树与二叉树之 笔记
- 网络编程①---基本概念
- aeiou(pp6.9)
- 数据结构 第五章 树和二叉树
- mysql字段长度(sql语句)
- websocket前台js代码重构
- 前言 Java Web开发
- Jar文件的数字签名
- Windows获取系统语言