一、树
1.1 数的逻辑结构
1.1.1 树的一些基本术语
1、树中常常将数据元素称为结点
2、树是n个结点的有限集合。当 n = 0时,称为空树
3、任意一颗非空树满足一下条件:
- 1)有且仅有一个特定的称为根的结点
- 2)n>1时,除根节点外的其余结点被分为互不相交的有限集合,其中每一个集合又是一棵树,称为子树
4、某结点所拥有的子树的个数称为该节点的度;树中各结点度的最大值称为该树的度
5、度为0的结点称为叶子结点;度不为0的结点称为分支节点
6、某节点的子树的根节点称为称为该节点的孩子结点;反之,该节点称为其孩子节点的双亲结点;具有同一个双亲的孩子结点护称为兄弟结点
7、两个结点之间的路程称为路径;经过的结点个数称为路径长度
8、从结点x到结点y有一条路径,称x为y的祖先,y为x的子孙
9、树中所有节点的最大层数称为树的深度;
10、层序编号
11、有序树、无序树
12、m棵互不相交的树的集合构成森林
1.1.2 数的遍历操作
(一)前序遍历
①访问根节点 ; ②按照从左到右的顺序前序遍历根节点的每一棵子树
(二)后序遍历
①按照从左到右的顺序后序遍历根节点的每一棵子树 ; ②访问根节点
(三)层序遍历
每层从左至右依次遍历
1.2 树的存储结构
1.2.1 双亲表示法
思路:用一个结构体数组存储,每一个结构体数组元素既包含树中结点的数据,同时包含该节点的双亲在数组中的下标
struct PNode{
int data; //树中节点的数据信息
int parent; //该节点的双亲载数组中的下标
};
1.2.2 孩子表示法
1、多重链表表示法
思路:树中的每个结点包括一个数据域和多个指针域,每个指针域指向该节点的一个孩子节点
2、孩子链表表示法
思路:一种用多个单链表表示树的方法,即把每个节点的孩子排列起来,看成是一个线性表,且以单链表存储,称为该节点的孩子链表
struct CTNode{
//孩子结点
int child;
CTNode* next;
};
struct CBBode{
//表头结点
int data;
CTNode* firstchild; //指向孩子的头指针
};
1.2.3 双亲孩子表示法
思路:该方法结合了双亲表示法和孩子链表表示法,即表头结点中,再开辟一块存储空间用来存储该节点的双亲结点的下标
1.2.4 孩子兄弟表示法
该方法又称为二叉链表表示法,其方法是链表中的每个结点除数据域外,设置了两个指针分别指向该节点的第一个孩子和右兄弟
struct TNode{
int data;
TNode* firstchild;
Tnode* rightsib;
};
二、二叉树
2.1 二叉树的逻辑结构
2.1.1 二叉树的定义和基本术语
1、二叉树是n个结点的有限集合,该集合或者为空集(空二叉树),或由一个根节点和两颗互不相交的、分别称为根节点的左子树和右子树构成
2、二叉树的特点:
①每个结点最多有两棵子树,因此二叉树中不存在度大于2的结点
②二叉树是有序的,其次序不能任意颠倒,即使树中的某个节点只有一颗子树,也要区分是左子树还是右子树
3、所有结点都是只有左子树的二叉树称为左斜树;所有结点都是只有右子树的二叉树称为右斜树;在斜树中,每一层只有一个结点,因此斜树中的结点数 = 树的深度
4、一棵二叉树中,所有分支结点都存在左子树和右子树,且所有叶子结点都在同一层上,这样的树称为满二叉树
满二叉树的特点:①所有的叶子结点只能出现在最下一层;②只有度为0的结点和度为2的结点
5、完全二叉树,一句话概括:完全二叉树不是满二叉树就是在实现满二叉树的路上
特点:①叶子节点只能出现在最下两层,且最下层的叶子节点都集中在二叉树左侧连续的位置;②如果有度为1的结点,只可能有一个这样的结点,且该节点只能有左孩子
2.1.2 二叉树的基本性质
性质一:二叉树的第i层上最多有2的(i-1)次方个结点
性质二:一颗深度为k的二叉树中,最多有(2的k次方)-1 个结点,最少有k个结点
性质三:在一颗二叉树中。如果叶子结点的个数为n1,度为2的节点个数为n2,则n1 = n2 +1;
性质四:具有n个节点的完全二叉树的深度为[log2的n次方]+1;其中[ ]里代表不大于这个数字的最大整数
性质五:对一颗具有n个结点的完全二叉树中的结点从1开始按照层序编号,则对于任意的编号为i的结点,有:
- (1)如果i>1,则结点i的双亲编号为[ i/2 ],否则结点i是根节点,无双亲
- (2)如果2i<=n,则结点i的左孩子编号为2i ,否则结点i无左孩子
- (3)如果2i+1<=n,则结点i的右孩子编号为 2i+1,否则结点i无右孩子
2.1.3 二叉树的遍历操作
二叉树的遍历操作分为四类:前序、中序、后序、层序遍历
- (1)前序遍历
1、访问根节点 2、前序遍历跟根节点的左子树 3、前序遍历根节点的右子树 - (2)中序遍历
1、中序遍历跟根节点的左子树 2、访问根节点 3、中序遍历根节点的右子树 - (3)后序遍历
1、后序遍历跟根节点的左子树 2、后序遍历根节点的右子树 3、访问根节点 - (4)层序遍历
2.1.4 若已知一颗二叉树的某一遍历序列,如何确定一颗二叉树?
能够判断的前提条件:已知前序和中序遍历;或已知后序和中序遍历
以已知前序和中序遍历为例:假设一颗二叉树的前序遍历为:A B C D E F G H ;中序遍历为:C D B A F E H G
首先,由前序遍历可得,结点A为根结点
然后,根据根结点A和中序遍历结果可知,结点C D B为根结点的左子树,结点F E H G为根结点的右子树
其次,因为结点A的左子树的前序序列为B C D,所以结点B是左子树的根结点,又有中序遍历可知 C D B,在B之前的结点C D是B结点左子树的结点,而B的右子树为空,
最后,依次分解,得到二叉树
2.2 二叉树的存储结构
2.2.1 二叉树的顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树的结点,要把一颗二叉树改造成完全二叉树,空节点的位置也要存储到其中
2.2.2 二叉树的连接存储结构
2.2.2.1 二叉链表
基本思想:令二叉树的每个结点对应一个链表结点,该节点既存放了数据信息,还设置了指向左右孩子的指针
struct BiNode{
char data;
struct BiNode * lchild,* rchild;
};
若没有左(右)孩子,设为空指针。注意:具有n个节点的二叉链表中,有n+1个空指针。
主类中参数的设定:(将公共方法全部封装,均调用相对应的私有的方法)
class BiTree{
private:
BiNode* root; //指向根节点的头指针,即创建一个空二叉树
BiNode* Q[MAX_SIZE];
private:
BiNode* Create(BiNode *bt); //构造函数的调用
void Release(BiNode* bt); //析构函数的调用
void PreOrder(BiNode* bt); //前序遍历函数的调用
void InOrder(BiNode* bt); //中序遍历函数的调用
void PostOrder(BiNode* bt); //后序遍历函数的调用
public:
BiTree(){
root = Create(root);} //构造函数,创建一棵二叉树 //Create(root);
~BiTree(){
Release(root);} //析构函数,释放各节点的存储空间
void PreOrder(){
PreOrder(root);} //前序遍历二叉树
void InOrder(){
InOrder(root);} //中序遍历二叉树
void PostOrder(){
PostOrder(root);} //后序遍历二叉树
void LeverOrder(); //层序遍历二叉树
};
遍历操作:(用递归实现)
(1)前序遍历:
void BiTree::PreOrder(BiNode* bt){
//前序遍历函数的调用
if(bt == NULL){
return ;
}else{
cout<<bt->data;
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
}
(2)中序遍历:
void BiTree::InOrder(BiNode* bt){
//中序遍历函数的调用
if(bt == NULL){
return ;
}else{
InOrder(bt->lchild);
cout<<bt->data;
InOrder(bt->rchild);
}
}
(3)后序遍历:
void BiTree::PostOrder(BiNode* bt){
//后序遍历函数的调用
if(bt == NULL){
return ;
}else{
PostOrder(bt->lchild);
PostOrder(bt->rchild);
cout<<bt->data;
}
}
(4)层序遍历:
伪代码:
①初始化队列Q
②如果队列不为空,根指针入队
③循环,循环条件:队列不为空
- 队头指针出队,输出队头指针的数值
- 判断左子树是否为空,不为空入队
- 判断右子树是否为空,不为空入队
void BiTree::LeverOrder(){
//层序遍历二叉树
int front ,rear;
front = rear = -1; //队列Q初始化
BiNode* q = new BiNode;
if(root == NULL){
return;
}else{
Q[++rear] = root; //如果二叉树为非空,将根指针入队
}
while(front != rear){
//循环直至队列Q为空
q = Q[++front]; //队列Q的队头元素出队
cout<<q->data; //访问
if(q->lchild != NULL){
Q[++rear] = q->lchild; //如果结点q的左孩子不为空,入队
}
if(q->rchild != NULL){
Q[++rear] = q->rchild; //如果结点q的右孩子不为空,入队
}
}
}
构造函数:
构造函数的功能是创建一颗二叉树。
扩展二叉树:将二叉树中每个节点的空指针引出一个虚结点,其值为一个特定值,如“#”,以标识其为空,把这样处理后的二叉树称为原二叉树的扩展二叉树
如何创建一颗二叉树:
BiNode* BiTree::Create(BiNode* bt){
char ch;
cin>>ch; //输入节点的数据信息,假设为字符
if(ch == '#'){
//建立一棵空树
bt = NULL;
}else{
bt = new BiNode; //通过前序遍历创建一颗二叉树
bt->data = ch;
bt->lchild = Create(bt->lchild); //递归建立左子树
bt->rchild = Create(bt->rchild); //递归建立右子树
}
return bt;
}
析构函数:
void BiTree::Release(BiNode* bt){
//析构函数的调用
if(bt!=NULL){
Release(bt->lchild); //释放左子树
Release(bt->rchild); //释放右子树
delete bt; //释放根节点
}
}
2.2.2.2 三叉链表
三叉链表是在二叉链表的基础上再设置一个parent指针指向双亲结点
2.2.2.3 线索链表
如果按照某种遍历次序对二叉树进行遍历,可以把二叉树中所有的结点排成一个线性序列。在具体应用中,有时需要访问二叉树结点在某种遍历序列中的前驱和后继,可以利用扩展二叉树得到的空指针来指向每个节点的前驱或后继,这些指向前驱和后继结点的指针称为线索,含有线索的二叉树称为线索二叉树,还有线索的二叉链表称为线索链表
我们在结点中,设置ltag和rtag变量,用于区分某个节点的指针域存放的是指向孩子的指针还是指向前驱或后继的线索,令:
Itag/rtag = 0时,lchild/rchild指向该节点的左/右孩子;Itag/rtag = 1时,lchild/rchild指向该节点的前驱/后继;
struct ThrNode{
char data;
struct ThrNode* lchild,* rchild;
int ltag,rtag;
};
下面我们以中序线索化二叉链表为例:
class InThrBiTree{
private:
ThrNode* root;
private:
ThrNode* Create(ThrNode* bt); //创建一个二叉链表
void ThrBiTree(ThrNode* bt); //中序线索化链表
void Release(ThrNode* bt); //析构函数的调用
public:
InThrBiTree(){
//构造函数,建立一个二叉树并且线索化该二叉树
root = Create(root);
ThrBiTree(root);
}
~InThrBiTree(){
Release(root);
}
ThrNode* Next(ThrNode* bt); //查找结点p的后继
void InOrder(); //中序遍历线索链表
};
建立二叉链表算法
ThrNode* InThrBiTree::Create(ThrNode* bt){
//创建一个二叉链表
char ch;
cin>>ch;
if(ch == '#'){
bt = NULL;
}else{
bt = new ThrNode;
bt->data = ch;
bt->ltag = 0; //先将左右指针初始化
bt->rtag = 0;
bt->lchild = Create(bt->lchild);
bt->rchild = Create(bt->rchild);
}
return bt;
}
中序线索化二叉链表算法
伪代码:
①设置一个静态的指针
②判断结点是否为空,若为空退出
③递归遍历左子树
④判断结点的左子树是否为空,若为空,保存其前驱的值
⑤判断前驱结点,若前驱结点不为空且右孩子为空,则前驱结点的右孩子指向当前被访问的结点
⑥前驱结点指向当前被访问的结点
⑦递归遍历右子树
void InThrBiTree::ThrBiTree(ThrNode* bt){
//中序线索化链表
static ThrNode* pre = NULL; //设置一个静态的指针pre,这样递归之后pre指针就保留的上一个指针的地址值
if(bt == NULL){
//如果结点为空,返回
return;
}
ThrBiTree(bt->lchild); //对bt的左子树进行线索化处理
//建立bt的前驱线索
if(bt->lchild == NULL){
//判断:如果左子树为空,即该节点没有左孩子
bt->ltag = 1; //将ltag改为1
bt->lchild = pre; //保存其前驱的值
}
//建立pre的后继线索
if(pre!=NULL && pre->rchild == NULL){
//判断:如果pre不为空,且不存在右子树
pre->rtag = 1;
pre->rchild = bt; //pre指针的右子树就指向当前访问的结点
}
pre = bt; //令pre指针指向刚刚访问过的结点bt
ThrBiTree(bt->rchild); //对bt的右子树进行线索化处理
}
中序线索链表查找后继的算法
伪代码:
①判断当前被访问的结点是否存在后继,若存在则返回
②若不存在,循环,循环条件结点存在左孩子,直至找到最左下角的结点
ThrNode* InThrBiTree::Next(ThrNode* bt){
//查找结点p的后继
ThrNode* q ;
if(bt->rtag == 1){
//判断:如果该结点的右指针为1,表明该节点的右指针是线索,那么右指针所指向的
q = bt->rchild; //结点便是他的后继节点
}else{
q = bt->rchild; //否则,工作指针q指向结点p的右孩子
while(q->ltag == 0){
//循环,查找最左下的结点,直至结点的ltag == 1时,这个结点就是后继
q = q->lchild;
}
}
return q;
}
中序线索链表的遍历算法
伪代码:
①如果结点为空,返回
②若不为空,循环找到第一个结点,循环条件,存在左孩子
③输出第一个结点的值
④循环,循环条件,结点存在右孩子,调用查找后继结点的函数,输出
void InThrBiTree::InOrder(){
if(root == NULL){
return ;
}
ThrNode* p = root;
while(p->ltag == 0){
//循环,查找中序遍历序列的第一个结点p
p = p->lchild;
}
cout<<p->data; //输出第一个结点的值
while(p->rchild != NULL){
//循环:如果结点p存在后继,就依次访问其后继节点
p = Next(p);
cout<<p->data;
}
}
2.2.3 二叉树遍历的非递归算法
前序遍历:前序遍历的关键:在前序遍历过某个结点的整个左子树后,如何找到该结点的右子树的根指针
伪代码:
①初始化栈
②循环,循环条件:结点不为空或栈不为空
- 循环,循环条件:结点不为空
输出值,将入栈,找左子树 - 判断,判断条件,栈不为空
出栈,找右子树
void BiTree::PreOrder(BiNode* bt){
int top = -1;
BiNode* data[MAX_SIZE]; //栈的初始化
while(bt!=NULL || top!=-1){
//循环直到bt为空或栈为空
while(bt!=NULL){
//若bt不为空时循环
cout<<bt->data; //输出
data[++top] = bt; //将指针bt指向的结点入栈
bt = bt->lchild; //读取指针bt的左子树
}
if(top != -1){
//栈不为空时循环
bt = data[top--]; //将栈顶元素弹出
bt = bt->rchild; //准备遍历右子树
}
}
}
中序遍历:中序遍历跟前序遍历相似,只是输出的操作要在准备遍历右子树之前输出
void BiTree::InOrder(BiNode* bt){
int top = -1;
BiNode* data[MAX_SIZE];
while(bt!=NULL || top!=-1){
while(bt!=NULL){
data[++top] = bt;
bt = bt->lchild;
}
if(top!=-1){
bt = data[top--];
cout<<bt->data; //准备遍历右子树之前输出
bt = bt->rchild;
}
}
}
后序遍历:后序遍历不同于前序和中序,在后序遍历中,结点要两次入栈,两次出栈,其中:
第一次入栈,只遍历完左子树,右子树尚未遍历,则该节点不出栈,利用栈顶结点找到它的右子树,准备遍历他的右子树
第二次入栈,遍历完右子树,将该结点出栈,并访问它
那么,如何区别同一结点的两次入栈?
为了区别同一个结点的两次入栈,试着标志flag,令
flag = 1时,第一次出栈,只遍历完左子树,该结点不能访问
flag = 2时,第二次出栈,只遍历完右子树,该结点可以访问
struct element{
BiNode* ptr;
int flag;
};
后序遍历:
伪代码:
①初始化栈
②循环,循环条件:结点不为空或栈不为空
- 判断,如果栈不为空,结点封装好入栈,查找左子树
- 如果栈为空,出栈,判断是否是第一次出栈,如果是的,则修改记号,继续入栈,结点指向右子树
- 若不是,则输出,结点清除
判断
void BiTree::PostOrder(BiNode* bt){
int top = -1;
element data[MAX_SIZE];
element elem; //初始化栈
while(bt!=NULL || top!=-1){
//循环直到bt为空且栈s为空
if(bt!=NULL){
//当bt不为空时
elem.ptr = bt;
elem.flag = 1; //第一次入栈前赋值为1
data[++top] = elem; //入栈
bt = bt->lchild; //遍历其左子树
}else{
//如果bt为空时
elem = data[top--]; //bt的双亲结点出栈
bt = elem.ptr;
if(elem.flag == 1){
//判断如果flag == 1,该节点不能访问
elem.flag = 2; //变成2
data[++top] = elem; //再入栈
bt = bt->rchild; //准备访问右子树
}else{
cout<<bt->data; //否则输出
bt = NULL; //bt置为空
}
}
}
}
2.3 树、森林与二叉树之间的转换
树的孩子兄弟存储法同二叉树的二叉链表存储方式是一样的
2.3.1 树、森林转换成二叉树
1、树转换成二叉树
(1)加线:树中所有同双亲的相邻兄弟结点之间加一条连线
(2)去线:对树的每个节点,只保留他与第一个孩子结点之间的连线,删去他与其他孩子节点之间的连线
树的前序遍历 = 二叉树的前序遍历
树的后序遍历 = 二叉树的中序遍历
2、森林转换成二叉树
(1)将森林中的每棵树转换成二叉树
(2)从第二颗二叉树开始,依次把后一棵二叉树的根节点作为前一棵二叉树根节点的右孩子
2.3.2 二叉树转换为森林或树
(1)加线:若某结点x是其双亲y的左孩子,则把结点x的右孩子、右孩子的右孩子…都与结点y用线连接起来
(2)去线:删去原二叉树中所有双亲结点与右孩子结点的连线
2.3.3 森林的遍历
森林的遍历就是依次遍历每一颗树
2.4 举例应用
2.4.1 哈夫曼树
2.4.1.1 几个术语
1、叶子结点的权值:是对叶子节点赋予一个有意义的数值量
2、二叉树的的带权路径长度:设二叉树具有n个带权值的叶子节点,从根节点到各个叶子结点的路径长度与相应叶子节点权值的乘积之和叫做二叉树的带权路径长度
3、哈夫曼树:带权路径长度最小的二叉树
2.4.1.2 如何构造哈夫曼树
1、初始化:由给定的n个权值{w1,w2,…wn}构造出n棵只有一个根节点的二叉树,从而得到一个二叉树集合F
2、选取与合并:在二叉树集合F中选取根节点的权值最小的两棵二叉树分别作为左右子树,构造出一颗新的二叉树,该二叉树的权值即为其左右子树的权值之和
3、删除与加入:在F中删除作为左右子树的两棵二叉树,并将新建立的二叉树加入其中
4、重复操作,直到F集合中只剩下一棵二叉树
5、注意点:
(1)具有n个叶子结点的哈夫曼树共有2n-1个结点,其中有n-1个非叶子节点,n个叶子节点
2.4.1.3 哈夫曼树的存储结构
1、设置一个数组Huffman[2n-1]
2、数组元素包含①权值域weight;②指针域lchild:保存该结点的左孩子结点在数组中的下标;③指针域rchild:保存该结点的右孩子结点在数组中的下标;④指针域parent:保存该结点的双亲结点在数组中的下标
struct Element{
int weight;
int lchild;
int rchild;
int parent;
}
3、
构建哈夫曼树的伪代码:
(1)数组Huffman初始化,所有数组元素的双亲,左右孩子都赋值为-1;
(2)数组Huffman的前n个元素的权值赋值给定权值
(3)进行n-1次合并
- 在二叉树集合中选取两个权值最小的根节点,其下标分别为i1,i2;
- 将二叉树i1,i2合并为一棵新的二叉树k
typedef struct element{
int weight; //权值域,保存该节点的权值
int lchild; //指针域,保存该节点的左孩子在数组中的下标
int rchild; //指针域,保存该节点的左孩子在数组中的下标
int parent; //指针域,保存该节点的双亲节点在数组中的下标
}Element;
void HuffmanTree(Element huffTree[],int w[],int n){
int i1 = 0;
int i2 = 0;
for(int i=0;i<2*n-1;i++){
huffTree[i].parent = -1;
huffTree[i].lchild = -1;
huffTree[i].rchild = -1;
} //初始化二叉树
for(int i=0;i<n;i++){
huffTree[i].weight = w[i];
} //前n项附上权值
for(int k=n;k<2*n-1;k++){
//进行 n-1 次合并
int b1 = 100;
int b2 = 100;
for(int i=0;i<k;i++){
//查找权值最小的两个根节点,下标为 i1,i2
if(b1 > huffTree[i].weight){
if(huffTree[i].parent == -1){
//待定的结点不能有双亲
b1 = huffTree[i].weight;
i1 = i;
}
}
}
for(int i=0;i<k;i++){
if(b2 > huffTree[i].weight){
if(i == i1) //和 i1区分开来
continue;
else{
if(huffTree[i].parent == -1){
b2 = huffTree[i].weight;
i2 = i;
}
}
}
}
cout<<i1<<" : "<<i2<<endl;
huffTree[i1].parent = k;
huffTree[i2].parent = k; //对之前的两个结点附上双亲
huffTree[k].lchild = i1;
huffTree[k].rchild = i2;
huffTree[k].weight = huffTree[i1].weight + huffTree[i2].weight; //新的根节点赋值
}
}