七 树
1.定义 n个结点的有限集。
2.特性 在任意一颗树中有且仅有一个特定的称为根(Root)的结点。 n>1时,其余结点可以分为m(m>0)个互不相交的有限集T1....Tm,其中每一个集合本身又是一棵树,成为子树。
3.树的常用概念
①树的结点 树的结点包含一个数据元素及若干执行其子树的分支。 相关概念 :
1 结点的度Degree:结点拥有的子树的数目。
2 叶结点Leaf:度为0的结点
3 分支结点:度不为0的结点,除根节点其他也叫内部结点
4 结点的孩子Child: 结点的子树成为该结点的孩子
5.孩子的双亲Parent: 该节点就是孩子双亲
6.结点的兄弟Sibling: 同一个双亲的孩子之间互称
7.结点的子孙: 以某结点为根的子树中任一结点都是改结点的子孙
8.结点的层次Level:从根开始定义,根为第一层,根的孩子为第二层
②树的度 树内各个结点度的最大值。
③树的深度/高度Depth:数中结点的最大层次
④有序树 如果将树中结点的各子树看成从左至右是有次序,不能互换的,则称之。
⑤无序树 ④的反面就是无序。
4.树的抽象数据结构及基本操作
InitTree(*T) 构造空树 DestroyTree(*T) 销毁树 CreateTree(*T,definition) 按照definition给出的定义构造树
ClearTree(*T) 存在则清空 TreeEmpty(T) 树是否为空 TreeDepth(T) 返回树的深度
Root(T) 返回T的根节点 Value(T,cur_e):返回结点cur_e Assign(T,cur_e,value)给树T的结点cur_e赋值为value
Parent(T,cur_e) 若cur_e是T的非根结点,则返回其双亲,否则返空
LeftChild(T,cur_e) 若cur_e是T的非叶结点,返回其最左孩子,否则返空
RightSibling(T,cur_e) 若cur_e有右兄弟,返回它的右兄弟,否则返空
InsertChild(*T,*p,i,c) p指向T的某个结点,i为所指结点p的度加1,非空树C与T不相交,操作结果为插入C为T中p指结点的第i颗子树。
DeleteChild(*T,*p,i) p指向T的某个结点,i为所指结点p的度,操作结果为删除T中p所指结点的第i颗子树。
5.树的存储结构 --->存储结构设计的合不合理取决于基于该结构的运算是否合适方便,时间复杂度好不好
①双亲表示法
1.定义 以一组连续空间存储树的结点,同时在每个结点,附设一个指示器指示其双亲结点到链表中的位置。即每个结点除了知道自己是谁,还知道它的双亲在哪。
2.原理 结构体有两个域,数据域存数据,位置域parent存该结点的双亲在数组中的位置下标。
3.结构体代码
//树的双亲表示法 typedef int TElemType; //树的结点 typedef struct PTNode{ TElemType data; int parent;//双亲所在数组中的下标值 }PTNode; //双亲表示法树 typedef struct{ PTNode nodes[MAXSIZE];//存放所有结点的数组 int r,n;//根的位置下标和总结点数 }PTree;
4.特性 根节点没有双亲,所以根节点的位置域值为-1,这样就统一为所有结点都有双亲了。
5.改进 再增加一个位置域:长子域,记录结点最左边孩子的域,如果结点没有孩子,则这个长子域设置为-1.孩子都是挨着放
再增加一个位置域:兄弟域,记录该结点右兄弟,还可以按需要再增加.....
②多重链表表示法
1定义 每个结点有多个指针域,其中每个指针指向一颗子树的根结点。
2实现方案一
1原理 每一个结点除一个数据域外,还有n个指向一颗子树的根结点的指针域,这个n的值就是树的度!(所有结点度的最大值)
2缺点 由于树的度是各结点度的最大值,所以导致很大的指针域浪费。
3实现方案二
1原理 专门开辟一个域存放当前结点的度
2缺点 每个结点度都不同,导致结点结构不一致,增加维护上的时间耗损
③孩子表示法
1原理 把每个结点的全部孩子结点串起来,以单链表作为存储结构,则n个结点有n个孩子链表,如果是叶子结点则根链表为空, n个头指针又组成一个线性表,采用顺序存储结构,放入数组。 所谓每个结点的孩子,孩子之间就是兄弟,也就是把这些兄弟串起来,而这些兄弟都是这颗子树根结点的孩子,把这个子树根结点放入一个数组,当然,顺序数组里存的就是树的全部所有元素,因为他们每一个都有可能是一个子树的根。
2结构 需要两种结点结构:链表里的孩子结点和头指针数组里的表头结构
3代码表示
//孩子表示法树 //把同根的兄弟结点都串起来成为一个链表,下面是链表的结点: typedef struct CTNode{ int child;//自己当前的位置角标 struct CTNode *next;//指向自己的下一个兄弟 }*ChildPtr; //一个结点的所有孩子都连起来了 //表头结构,包括表头中数据和它的孩子们 typedef struct{ TElemType data; ChildPtr firtchild;//自己的孩子串 int parent;//当前结点双亲的位置角标----->孩子表示法的改进:双亲孩子表示法 }CTBox; //孩子表示法树结构 typedef struct{ CTBox nodes[MAXSIZE];//数的所有元素存放的数组 int r,n; //树的根所在的角标和数的有效结点个数。 }CTree;
4缺点 无法直接找到结点的双亲,改进:双亲孩子表示法,在表头结构中加入当前结点双亲的角标位置即可
④孩子兄弟表示法----->二叉树的雏形
1定义 对于一个结点,除了必要的数据域,只要再知道它第一个孩子和它自己的右兄弟,就可以推导出整个树。
2原理 因为知道了第一个孩子,就可以推导出这个孩子所有兄弟;知道了右兄弟就可以知道自己平辈的所有兄弟,也就是自己这辈还自己的下一辈所有结点就都知道了。
3代码表示
//孩子兄弟表示法 typedef struct CSNode{ TElemType data; struct CSNode *firstchild,*rightsib; }CSNode,*CSTree;
6.二叉树
①定义 n个结点的有限集合,该集合或者为空集,或者由一个根结点和两颗互不相交的,分别成为根结点的左子树和右子树的二叉树组成。
②特点
1.每个结点最多两颗子树,所以二叉树不存在度大于2的结点,可以没有子树或一颗子树。
2.左子树和右子树是有顺序的,次序不能错。
3.即使某结点只有一颗子树也要分是左还是右。
③二叉树的五种基本形态
1.空二叉树 2.只有一个根结点的二叉树 3.根结点只有左子树 4.根结点只有右子树 5.根结点既有左子树又有右子树
④特殊二叉树
1.斜树 所有结点都只有左子树的二叉树叫左斜树。所有结点都只有右子树的叫右斜树
2.满二叉树 所有分支都有左右子树,而且所有叶子都在同一层上。
特点:①叶子只能出现在最下一层 ②非叶子结点的度一定是2. ③在同样深度的二叉树中,满二叉树结点个数最多,叶子数最多。
⑤完全二叉树 把满二叉树从上到下从左到右一层层编号后,把这些序号按小到排好,砍掉包含最后一个序号的任意个相连的序号的结点,剩下的二叉树都是完全二叉树。
也就是保证砍掉了剩下的序号还是完整的从小到大不间断。此时二叉树仍然保持连接完整和次序井然。
⑥完全二叉树特定
1.叶子结点只能出现在最下两层。
2.最下层叶子一定集中在左部连续位置。
3.倒数二层,如果有叶子,一定在右边连续位置
4.如果结点度为1,则结点只有左孩子。不存在只有右子树的情况。
5.同样结点的二叉树,完全二叉树深度最小。
⑦二叉树的数学性质
1.在二叉树的第i层至多有2^(i-1)个结点
2.深度为k的二叉树,也就是最多k层,至多有2^k -1个结点
3.任意颗二叉树,如果其叶子(终端)结点数为n0,度为2的节点数为n2, 则n0=n2+1
4.具有n个结点的完全二叉树的深度为[log2为底的n]+1
5.对有n个结点的完全二叉树的结点按层序编号,对任一结点i:
①如果i=1,则i是根,如果i>1,则其双亲是结点[i/2]
②如果2i>n,则i无左孩子,
③如果2i<=n,则i左孩子是2i
④如果2i+1>n,则结点i无右孩子
⑧二叉树的存储结构
1.二叉树的顺序存储结构 :完全二叉树由于定义严格,用顺序存储也就是把元素放入数组后,角标之间是可以判定出逻辑关系,所以完全二叉树只适合顺序存储。
2.二叉链表 指结点包含数据域和两个指针域,一个指向左孩子,一个指向右孩子(若需要,可以增加指向双亲的指针域).
//二叉树结构:二叉链表 typedef struct BiTNode{//二叉链表结点 TElemType data; struct BiTNode *lchild,*rchild;//左右孩子指针 }BiTNode,*BiTree;
3.二叉树遍历
①定义 从根节点出发,按照 “某种次序” 依次访问二叉树中所有结点,使得每个结点被访问依次而且只访问一次。
②方式
1.前序遍历 :为空则返否则,先访问(如打印)根节点,然后前序遍历左子树,再前序遍历右子树。
代码表示
//二叉树的前序遍历 void PreOrderTraverse(BiTree T){ if(T==NULL) return; printf("%c",T->data); //在这里对结点进行具体操作,显示打印数据等 PreOrderTraverse(T->lchild); //打印后再先序遍历左子树 PreOrderTraverse(T->rchild); //最后先序遍历右子树 }
2.中序遍历 :为空则返否则,从根节点开始(先不访问根),中序遍历根结点的左子树,然后访问根节点,最后中序遍历右子树。
代码表示
//二叉树的中序遍历 void InOrderTraverse(BiTree T){ if(T==NULL) return; InOrderTraverse(T->lchild); printf("%c",T->data); //在这里对结点进行具体操作,显示打印数据等 InOrderTraverse(T->rchild); }
3.后序遍历 :为空则返否则,从左到右先叶子后结点的方式遍历访问左子树然后右子树,最后访问根节点。
代码表示
//二叉树的后序遍历 void PostOrderTraverse(BiTree T){ if(T==NULL) return; PostOrderTraverse(T->lchild); PostOrderTraverse(T->rchild); printf("%c",T->data); //在这里对结点进行具体操作,显示打印数据等 }
4.层序遍历 :为空则返否则,从树的第一层,也就是根节点开始访问,从上而下逐层遍历,同一层上,从左到右逐个访问结点。
代码表示
⑨二叉树的建立
1.原理 为了确定每个结点的左右孩子情况,所以需要让每个结点都有左右孩子,本来没有的用“#”代替,如此事先写出前序遍历元素排序,按照这个顺序依次遍历录入即可。
2.代码
//二叉树的创建:前序遍历赋值 void CreateBiTree(BiTree *T){ TElemType ch;//记录用户录入的字符 scanf("%c",&ch); if(ch=='#')//如果是代表空的字符,则这个结点不赋值 *T=NULL; else{ *T=(BiTree)malloc(sizeof(BiTNode));//开辟和单个结点一样大的空间来存放输入的作为结点的字符 if(!*T)//分配失败 exit(OVERFLOW);//直接退出进程,返回错误 (*T)->data=ch;//具体的操作:赋值 CreateBiTree(&(*T)->lchild);//构造左子树 CreateBiTree(&(*T)->rchild);//构造右子树 } }
7.线索二叉树
①定义 指向前驱和后继的指针称为线索,加上线索的二叉链称为线索链表,相应的二叉树就称为线索二叉树。 Threaded Binary Tree
从二叉树结点结构体可以看到,每个结点都有指针域指向左右孩子结点,这必然会导致空间浪费,因为很多结点是没有孩子的,
因此线索二叉树让链表结点的空余指针指向其前驱和后继结点即提高空间利用率
②特点 线索二叉树等于把一颗二叉树变成了双向链表,把二叉树以某种次序遍历使其变成线索二叉树的过程称为线索化。
线索化后,整个树所有的结点的指针域都赋值了,没有空余,但是如此一来,无法知道某个结点某指针到底是指向子结点还是仅为了表明前后驱是谁
③完善 为了解决指向意义不明问题,扩充每个结点为四个指针域,左右孩子指针和左右tag指针,后两个指针只存放0或1: lchild ltag rchild rtag
ltag为0是表示lchild指向左孩子,为1表示指向链表中前一个结点:前驱。后驱指下一个
④定义代码表示
//二叉树的线索化,线索二叉树结构体表示 typedef enum{ Link, //Link=0代表当前指针是指向的孩子 Thread //Thread=1代表当前指针指向的是前后驱的线索 }PointerTag; //线索二叉树结点表示 typedef struct BiThrNode{ TElemType data; struct BiThrNode *lchild,*rchild; //左右孩子指针 PointerTag LTag; //左右指针特性标记 PointerTag RTag; }BiThrNode,*BiThrTree;
⑤线索二叉树的构造(线索化): 线索化的实质是将二叉链表中的空指针改为指向前驱或后继的线索,由于前驱和后继的信息只有在遍历该二叉树时才能得到,
所以线索化的过程就是在遍历的过程中修改空指针的过程,以对二叉树进行中序遍历线索化代码如下:
//二叉树的中序遍历线索化 BiThrTree pre;//全局变量,始终指向刚刚访问过的结点 void InThreading(BiThrTree p){ if(p){ InThreading(p->lchild);//递归左子树线索化 if(!p->lchild){//没有左孩子则为充分利用左孩子指针,让其指向当前结点的前驱 p->LTag=Thread; //更改标记,表明当前意义的变更 p->lchild=pre; //左孩子指针现在指向刚刚访问过的结点,也就是p的前驱 } /*--->p前驱结点线索化结束,对于后继结点,通过p是无法得知后继的, 所以针对p前驱的后继进行处理,因为p的前驱是已知的<----*/ if(!pre->rchild){ //前驱没有右孩子 pre->RTag=Thread;//表明当前意义的变更 pre->rchild=p;//前驱右孩子指针指向其后继,也就是p } pre=p;//当前结点成为下一个结点的前驱 InThreading(p->rchild);//递归右子树线索化 } }
⑥线索二叉树的遍历:有了线索二叉树后,对它进行遍历发现,其实就等于是操作一个双向链表结构。但相比下还少一个头结点,所以给其加上一个左指针指向根节点右指针指向中序遍历最后一个结点
的头结点,同时,让中序遍历的第一个结点的左指针指向头结点,中序遍历最后一个结点的右指针也指向头结点。
⑦线索二叉树遍历代码:---->线索二叉树的遍历本质上就是双向链表的遍历
//线索二叉树的遍历 Status InOrderTraverse_Thr(BiThrTree T){ BiThrTree P=T->lchild;//T是头指针,头指针的左子树就是原树的根结点 while(P!=T){//外循环用来遍历每一个结点,直到尾结点,也就是又回到T while(P->LTag==Link){//P!=T的外循环条件已经过滤掉P->lchild==T的可能性了 P=P->lchild; printf("%c",P->data); } while(P->RTag==Thread&&P->rchild!=T){ P=P->rchild; printf("%c",P->data); } /*至此,根结点左边全部遍历完毕,此时P又回到根结点,跳过已经遍历过的根,需要进入根的右侧树*/ P=P->rchild; //继续右侧树的遍历 } return OK; }
8.树转二叉树 步骤:
①加线,在所有兄弟结点之间加一条连线。
②去线,对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
③层次跳转,各自以所有根结点为转动轴,整个树全部顺时针移动,使树呈现层次分明
9.森林转二叉树 若干颗树组成一个森林,步骤:
①把每个树转换为二叉树
②第一颗树不动,从第二颗二叉树开始,依次把后面所有二叉树的根节点作为前一棵树根节点的右孩子,连接起来。
10.霍夫曼树--最优二叉树
①路径:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径。
②路径长度:路径上分支的数目
③树的路径长度:从树根到每一结点的路径长度之和。
④带权路径长度:树中所有叶子结点的带权路径长度之和。
⑤霍夫曼树:假设有n个权值,构造一颗有n个叶子结点的二叉树,每个叶子结点带权Wk,每个叶子的路径长度为lk,则其中带权路径长度WPL最小的二叉树成为霍夫曼树。也叫最优二叉树
⑥霍夫曼树(最优二叉树)的构造过程:
1.有n个权值{w1,w2,w3,w4...wn}构成n颗二叉树集合F={T1,T2,T3...Tn},其中每颗二叉树Ti中只有一个带权为Wi根节点,其左右子树均为空。---->带权结点
2.在F中选取两颗根结点的权值最小的树作为左右子树构造一颗新的二叉树,并把这个新的二叉树的根节点的权值定为两个字结点权值之和。
3.用这个新生产的二叉树替代生产它的两个树,加入到F中
4.重复2和3,利用贪心算法,每次都挑选生产最小权值,逐步到F中只剩一颗树为止。