目录
一、树的思维导图
二、树的基本概念
1、树的定义:树(Tree)是一种非线性结构,它是由 n(n≥0)个结点的有限集。
若n=0,称为空树;若n>0,则它满足如下两个条件:
①有且仅有一个特定的称为根(Root)的结点;
②其余结点可分为m(m≥0)个互不相交的有限集T1,T2,…Tm,其中每一个集合本身又是一棵树,并称为根的子树。
由此可知树的定义是递归的,即在树的定义中又用到了树的定义。
2、树的基本术语:
结点:结点不仅包含数据无素,而且包含指向子树的分支。例如,A结点不仅包含数据元素A,而且包含3个指向子树的指针。
节点的度:结点拥有的子树个数或者分支的个数。例如,A结点有3棵子树,所以A结点的度为3。
树的度:树中各结点度的最大值。图中结点度最大为3(A、D结点),最小为0(F、G、I、J、x、L、M结点,所以树的度为3。
叶子结点:又叫作终端结点,指度为0的结点,如F、G、l、J、K、L.M结点都是叶子结点。非终端结点:又叫作分支结点,指度不为0的结点,如A、B、C、D、E、H结点都是非终端结点。除了根结点之外的非终端结点,也叫作内部结点,如B、C、D、E、日结点都是内部结点。
孩子:站点的子树的根,如A结点的孩子为B、C、D。
双亲:与孩子的定义对应,如B、C、D结点的双亲都是A。
兄弟:同一个双亲的孩子之间互为兄弟。如B、C、D互为兄弟,因为它们都是A结点的孩子。
祖先:从根到某结点的路径上的所有结点,都是这个结点的相先。如K的祖先是A、B、E,因为从A到K的路径为A-B-E-K。
子孙:以某结点为根的子树中的所有结点,都是该结点的子孙。如D的子孙为H、I、J、M。
层次:从根开始,根为第一层,根的孩子为第二层,根的孩子的孩子为第三层,以此类推。
树的高度(或者深度):树中结点的最大层次。图中的树共有4层,所以高度为4。
结点的深度和高度:
1)结点的深度是从根结到该结点路径上的结点个数。
2)从某结点往下走可能到达多个叶子结点,对应了多条通往这些叶子结点的路径,其中最长的那条路径的长度即为该结点在树中的高度,如结点D的高度为3,就是从D到M的路径长度。
3)根结点的高度为树的高度,如结点A,其高度为4,是从A到K(L、M)这条路径的长度,也是整棵树的高度。
堂兄弟:双亲在同一层的结点互为堂兄弟。如G和H互为堂兄弟,因为G的双亲是C。H的双亲是D,C和D在同一层上。
有序树:树中结点的子树从左到右是有次序的,不能交换,这样的树叫作有序树。
无序树:树中结点的子树没有顺序。可以任意交换,这样的树叫作无序树。
丰满树:丰满树即理想平衡树,要求除最底层外,其他是都是满的。
森林:若干棵互不相交的树的集合。图中如果把根A去掉,剩下的3棵子树互不相交,它们组成一个森林。
3、树结构和线性结构的比较
(1)线性结构:第一个元素无前驱,最后一个元素无后继,其他数据元素一个前驱一个后继(一对一)
(2)树结构:根节点无双亲,叶子节点无后继,其他中间节点一个双亲多个孩子(一对多)
4、树的存储结构
(1)顺序存储结构:即双亲存储结构,用一维数组来实现。最简单的定义方法为:int tree[maxSize]; ,即用一个整形数组就可以存储一棵树的信息。用数组下标表示树中的节点,数组元素的内容表示该节点的双亲节点。
(2)链式存储结构:
① 孩子存储结构:图的邻接表存储结构。
② 孩子兄弟存储结构:与树和森林与二叉树的相互转换关系密切。
三、二叉树
1、二叉树的定义:二叉树是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。二叉树的特点如下:
① 每个节点最多只有两棵子树,即二叉树中节点的都只能为0、1、2。
② 子树有左右顺序之分,不能颠倒。
③ 二叉树可以是空集合,根可以有空的左子树或空的右子树。
通过二叉树的定义,可知二叉树共有5种基本形态,(1)空二叉树(2)只有根节点(3)只有左子树,右子树为空(4)只有右子树,左子树为空(5)既有左子树,又有右子树。
在一棵二叉树中,如果所有的分支节点都有左孩子和右孩子结点,并且叶子节点都集中在二叉树的最下一层,则这样的二区树称为满二叉树。如果对满二叉树进行编号,约定编号从1开始,从上到下,自左至右进行。如果对一棵深度为k、有 n 个结点的二叉树进行编号后,各节点的编号与深度为k的满二叉树中相回位置上的结点的编号均相同,那么这棵二叉树就是一棵完全二叉树。
通俗地说,一棵完全二叉树一定是由一棵满二叉树从右至左从下至上,挨个删除结点所得到的,如果跳着删除,则得到的不是完全。
2、二叉树的主要性质:
(1)非空二叉树上叶子节点数等于双分支节点数加 1。
(2)二叉树的第 i 层上最多有 2^(i-1)(i>=1)个节点。
(3)高度(或深度)为 k 的二叉树最多有 2^k - 1(k>=1) 个节点。
换句话说:满二叉树中前 k 层的节点个数为 2^k - 1。
(4)有 n 个节点的完全二叉树,对各节点从上到下、从左到右依次编号(1~n),则有以下情况:
若 i 为某节点 a 的编号则:
如果 i ≠ 1,则 a 的双亲节点的编号为【i/2】(向下取整)。
如果 2i ≤ n,则 a 的左孩子的编号为2i,如果 2i ≥ n,则 a 无左孩子。
如果 2i +1 ≤ n,则 a 的右孩子的编号为 2i +1 ,如果 2i +1 > n ,则 a 无右孩子。
(5)给定 n 个节点,能构成 h(n) 种不同的二叉树,h(n) = Cn 2n /(n+1)。
(6)具有 n 个节点的完全二叉树的高度(或深度)为 【log2n】(向下取整) +1。
3、二叉树的存储结构
(1)顺序存储结构:顺序存储结构即用一个数组来存储一棵二叉树,这种存储方式最适合于完全二叉树,用于存储一般二叉树会浪费大量的存储空间。将完全二叉树中的节点值按编号依次存入一个一维数组中,即完成了一棵二叉树的顺序存储。
例如,知道了顶点A的下标为1,要得到A左孩子节点只需要访问BTree[1*2]即可。类似地,如果知道一个节点 i ,如果 2i 不大于 n ,则 i 的左孩子就存在于BTree[2*i]内。
(2)链式存储结构:观察二叉树的形态可以发现,一个根节点对应两棵树的关系,因此设计含有一个数据域和两个指针域的链式节点结构,具体如下:
对应的节点类型的结构体定义如下:
typedef struct BTNode{
char data;
struct BTNode *lchild;
struct BTNode *rchild;
}BTNode;
其中,data 表示数据域,用于存储对应的数据元素;lchild 和 rchild 分别表示左指针域和右指针域,分别用于存储左孩子结点和右孩子结点的位置:这种存储结构又称为二叉链表存储结构,如下图所示。
(3)二叉树的遍历算法
① 先序遍历:访问根节点,线序遍历左子树,先序遍历右子树。(根-左-右)
void preorder(BTNode *p){
if(p! = NULL){
Visit(p); //先访问根节点
preorder(p->lchild); //先序遍历左子树
preorder(p->rchild); //先序遍历右子树
}
}
② 中序遍历:中序遍历左子树,访问根节点,中序遍历右子树。(左-根-右)
void inorder(BTNode *p){
if(p! = NULL){
inorder(p->lchild);
Visit(p);
inorder(p->rchild);
}
}
③ 后序遍历:后序遍历左子树,后序遍历右子树,访问根节点。(左-右-根)
void posorder(BTNode *p){
if(p! = NULL){
posorder(p->lchild);
posorder(p->rchild);
Visit(p);
}
}
④ 层次遍历:进行层次遍历,要建立一个循环队列。先将二叉树的头节点入队,然后出队,访问该节点,如果它有左子树,则将左子树的根节点入队,如果它有右子树,则将右子树的根节点入队。然后出队,对出队节点访问,如此反复,直到队空位置。
void level(BTNode *p){
int front,rear;
BTNode *que[maxSize]; //定义一个循环队列,用来记录将要访问的层次上的节点
front = rear = 0;
BTNode *q;
if(p!=NULL){
rear = (rear+1)%maxSize;
que[rear] = p; //根节点入队
while(front!=rear){ //当队列不为空的时候进行循环
front = (front+1)%maxSize;
q = que[front]; //队头节点出队
Visit(q); //访问队头节点
if(q->lchild!=NULL){ //如果左子树不为空,则左子树的根节点入队
rear = (rear+1)%maxSize;
que[rear] = q->lchild;
}
if(q->rchild!=NULL){ //如果右子树不为空,则右子树的根节点入队
rear = (rear+1)%maxSize;
que[rear] = q->rchild;
}
}
}
}
上面介绍的二叉树深度优先遍历算法都是用递归实现的,这是很低效的,原因在于你调用了一个栈,并做了诸如保护现场和恢复现场等复杂操作,才使得遍历可以用非常简洁的代码实现。除了递归方法外还有两种方法:二叉树深度优先遍历算法的非递归实现和线索二叉树。第一种算法用用户定义的栈来代替系统栈,也就是用非递归的方式实现遍历算法,可以得到不小的效率提升;第二种算法将二叉树线索化,不需要栈来辅助完成遍历操作,跟进一步提高了效率。
(4)二叉树的遍历算法的改进 - 二叉树深度优先遍历算法的非递归实现
① 先序遍历非递归算法:
void preorderNorecursion(BTNode *bt){
if(bt != NULL){
BTNode *Stack[maxSize]; //定义一个栈
int top = -1; //初始化栈
BTNode *P;
Stack[++top] = bt; //根节点入栈
while(top != -1){ //栈空退出循环,遍历结束
p = Stack[top--]; //出栈并输出栈顶节点
Visit(p); //访问出栈的节点
if(p->rchild != NULL); //栈顶节点的右孩子存在,则右孩子入栈
Stack[++top] = p->rchild;
if(p->lchild != NULL) //栈顶节点的左孩子存在,则左孩子入栈
Stack[++top] = p->lchild;
}
}
}
② 中序遍历非递归算法:
void inorderNorecursion(BTNode *bt){
if(bt != NULL){
BTNode *Stack[maxSize]; //定义一个栈
int top = -1; //初始化栈
BTNode *P;
p = bt;
while(top!=-1 || p!=NULL){
while(p!=NULL){ //左孩子存在,则左孩子入栈
Stack[++top] = p;
p = p->lchild;
}
if(top!=-1){ //在栈不空的 情况下出栈并输出出栈节点
p = Stack[top--];
Visit(p); //访问p节点
p = p->rchild;
}
}
}
}
③ 后序遍历非递归算法:
void posorderNorecursion(BTNode *bt){
if(bt != NULL){
BTNode *Stack1[maxSize]; //定义一个栈Stack1
int top1 = -1; //初始化栈
BTNode *Stack2[maxSize]; //定义一个栈Stack2
int top2 = -1; //初始化栈
BTNode *P = NULL;
Stack1[++top1] = bt;
while(top!=-1){
p = Stack1[top--];
Stack2[++top2] = p;
if(p->lchild != NULL);
Stack1[++top1] = p->lchild; //左孩子先入栈
if(p->rchild != NULL)
Stack1[++top1] = p->rchild; //右孩子再入栈
}
while(top2!=-1){
p = Stack2[top--]; //出栈序列即为后序遍历序列
Visit(p);
}
}
}
二叉树非递归遍历算法避免了系统栈,提高了一定的执行效率。下面的线索二叉树连用户栈也省掉了,把二叉树的遍历过程线程化,进一步提高了二叉树的遍历效率。
(5)二叉树的遍历算法的改进 - 线索二叉树
① 中序线索二叉树的构造:线索二叉树的节点结构如下:
在二叉树线索化的过程中会把树中的空指针利用起来作为寻找当前结点前驱或后继的线索,这样就出现了一个问题,即线索和树中原有指向孩子结点的指针无法区分。上边的结点设计就是为了区分这两类指针,其中,ltag 和 rtag 为标识域:它们的具体意义如下:
1)如果 ltag=0,则表示 lchild为指针,指向结点的左孩子:如果 Itag=1,则表示lchild为线索,指向结点的直接前驱。
2)如果 rtag=0,则表示 rchild为指针,指向结点的右孩子;如果 rtag-1,则表示rchild为线索,指向结点的直接后继。
对应的线索二叉树的结点定义如下:
typedef struct TBTNode{
char data;
int ltag,rtag;
struct TBTNode *lchild;
struct TBTNode *rchild;
}TBTNode;
线索二叉树可以分为前序线索二叉树、中序线索二叉树和后序线索二叉树。对一棵二叉树中所有结点的空指针域按照某种遍历方式加线索的过程叫作线索化,被线索化了的二又树称为线索二叉树。下图为中序线索二叉树及其二叉链表表示。
② 二叉树终须线索化分析:
1)对二叉树进行中序线索化,首先要有个中序遍历的框架,采用二叉树中序递归遍历算法,在遍历的过程中连接上合适的线索
2)线索化的规则是,左线索指针指向当前节点在中序遍历序列中的前驱节点,右线索指针指向当前节点在中序遍历序列的后继节点。因此需要一个指针 p 指向当前正在访问的节点,pre 指向 p 的前驱节点,p 的左线索如果存在,则让其指向 pre ,pre 的右线索如果存在则让其指向 p ,应为 p 是 pre 的后继节点,这样就完成了一对线索的链接。
3)上一步中保持 pre 始终指向 p 前驱的具体过程是,当 p 将要离开一个访问过的节点时,pre 指向 p;当 p 来到一个新节点时,pre 显然指向的是此时 p 所指节点的前驱节点。
③ 通过中序遍历对二叉树线索化的递归算法如下:
void InThread(TBTNode *p,TBTNode *&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->rtag = 1;
}
pre = p;
InThread(pre->rchild,pre); //递归,右子树线索化
}
}
④ 通过中序遍历建立中序线索二叉树的主程序如下:
void createInThread(TBTNode *root){
TBTNode *pre = NULL;
if(root != NULL){
InThread(root,pre);
pre->rchild = NULL; //非空二叉树,线索化
pre->rtag = 1; //后处理中序最后一个节点
}
}
(6)遍历中序线索二叉树
① 以 p 为根的中序线索二叉树中,求其中序序列下的第一个节点的算法如下:
TBTNode *First(TNTNode *p){
while(p->ltag == 0){ //若 p 有左孩子,则执行该循环
p = p->lchild; //最左下节点(不一定是叶子节点)
}
return p;
}
② 求在中序线索二叉树中,节点 p 在中序下的后继节点的算法如下:
TBTNode *Next(TNTNode *p){
if(p->rtag == 0) //如果p有右孩子,则返回右子树的中序第一个节点
return First(p-rchild);
else
return p->rchild; //rtag == 1,直接返回后继线索
}
③ 中序线索二叉树上执行中序遍历算法:
void Inorder(TBTNode *root){
for(TBTNode *p=First(root);p!=null;p=Next(p))
Visit(p);
}
四、树和森林与二叉树的相互转化
1、树转化为二叉树:
① 将同一节点的各孩子节点用线串起来。
② 将每个节点的分支从左到右除了第一个以外其余的都剪掉。
③ 调整节点使之符合二叉树的层次结构。
2、二叉树转换为树:
① 先将一棵树从左上到右下分为若干层。
② 找到每一层在其上一层的父节点。
③ 将每一层的节点和其父节点相连,然后删除每一层节点之间的连接。
3、森林转化为二叉树:
① 先将森林分别转化为 二叉树。
② 将第二棵二叉树作为第一颗二叉树的右子树,依次拼接。
4、二叉树转化为森林:
① 不停的将根节点有右孩子的二叉树的右孩子链断开,知道不存在根基点有右孩子的二叉树位置。
② 将得到的多棵二叉树按照二叉树转化为树的规则依次转化即可。
5、树和森林的遍历:
(1)树的遍历:有两种方式:先序遍历和后序遍历。
先序遍历是先访问根结点,再依次访问根结点的每棵子树,访问子树时仍然遵循先根再子树的规则。
后序遍历是先依次访问根结点的每棵子树,再访问根结点,访问子树时仍然遵循先子树再根的规则。
树转换为二叉树后,树的先序遍历对应二叉树的先序遍历。树的后序遍历对应二叉树的中序遍历。
(2)森林的遍历:有两种方式:先序遍历和后序遍历。
先序遍历:先访问森林中第一棵树的根节点,然后先序遍历第一棵树中根节点的子树,最后先序遍历森林中其他树。
后序遍历:后序遍历第一颗树的根节点的子树,然后访问第一棵树的根节点,最后后序遍历森林中的其他树。
五、哈夫曼树和哈夫曼编码
1、赫夫曼树:又叫作最优二叉树,它的特点是带权路径最短,首先需要说明几个关于路径的概念:
(1)路径:路径是指从树中一个结点到另一个结点的分支所构成的路线。
(2)路径长度:路径长度是指路径上的分支数目。
(3)树的路径长度:树的路径长度是指从根到每个结点的路径长度之和。
(4)带权路径长度:结点具有权值:从该结点到根之间的路径长度乘以结点的权值,就是该结点的带权路径长度。
(5)树的带权路径长度(WPL):树的带权路径长度是指树中所有叶子结点的带权路径长度之和。
2、赫夫曼树的构造方法:给定n个权值,用这n个权值来构造赫夫曼树的算法描述如下:
(1)将这n个权值分别看作只有根结点的n棵二叉树,这些二叉树构成的集合记为F。
(2)从F中选出两棵根结点的权值最小的树(假设为a、b)作为左、右子树,构造一棵新的二叉树(假设为c),新的二叉树的根结点的权值为左、右子树根结点权值之和。
(3)从F中删除a、b,加入新构造的树c。
(4)重复进行(2)、(3)两步,直到F中只剩下一棵树为止,这棵树就是赫夫曼树。
3、赫夫曼树的特点:
(1)权值越大的节点,距离根节点越近。
(2)树中没有度为1的节点。这类树又叫做正则(严格)二叉树。
(3)树的带权路径长度最短。
4、赫夫曼编码:赫夫曼编码可以实现文件的压缩存储。根据赫夫曼树分配编码串的前缀,这样是不会出现重复的。
5、赫夫曼n叉树:构造方式和赫夫曼二叉树一样,如果无法构造的时候可以通过增加权值为0的节点来实现赫夫曼n叉树的构造。