数据结构-树
一、前言:
线性结构:栈、队列、串、数组和广义表
非线性结构:树和图
注:线性结构为每个元素有唯一的前驱和后继元素
二、树的基本定义:
(一)树的定义:树是n个节点的有限序列。
1.空树:n=0
2.非空树:n>0
(二)树的逻辑表示:树形表示法、文氏图表示法、广义表表示法和凹入表示法。
(三)基本术语:
1.结点(node):一个数据元素及其若干指向其子树的分支。
2.结点的度(degree) :结点所拥有的子树的棵数称为结点的度。
3.树的度:树中结点度的最大值称为树的度。
4.叶子(left)结点:树中度为0的结点称为叶子结点(或终端结点)。
5.孩子结点: 一个结点的子树的根称为该结点的孩子结点(child)
6.双亲结点:一个结点的上一个结点
7.兄弟结点:与该节点同双亲结点的其他结点
8.堂兄弟结点:双亲结点在同一层上的所有结点互称为堂兄弟结点。
9.层次:规定根的层次为1,其余结点的层次等于其双亲结点的层次加1
10.结点的层次路径:从根结点开始,到达某结点p所经过的所有结点成为结点p的层次路径(有且只有一条)。
11.祖先:结点p的层次路径上的所有结点(p除外)称为p的祖先(ancester)
12.子孙结点:以某一结点为根的子树中的任意结点称为该结点的子孙结点
13.树的深度:树中结点的最大层次值,又称为树的高度
14.有序树:树中每一个结点的子树(若有)具有一定的次序
15.森林:m颗互不相交的树的集合(若将一棵树的根节点删除,其余的子树就变为了森林。
三、二叉树:
<一>普通二叉树:
(一)定义:
是树结构的一种特殊形式;
每个节点最多有两颗子树;
结点存在左右次序之分,次序不可颠倒;

(二)性质:
(1)第m(m>=1)层上至多有2m-1个结点;
(2)深度为k(k>1)的二叉树最多有2k-1个结点
(3)树种叶子结点总数为n0,度为2的节点总数n2,则:n0=n2+1
注:二叉树中结点只有一个孩子的时候无左右之分。
(三)满二叉树:
(1)定义:一颗深度为k且有2k-1个节点的二叉树
(2)性质:
1.基本特点是每一层上的结点数总是最大结点数。
2.满二叉树的所有的支结点都有左、右子树。
3.可对满二叉树的结点进行连续编号,若规定从根结点开始,按“自上而下、自左至右”的原则进行
4.知道任意序列就可确定该树
(四)完全二叉树:
(1)定义:如果深度为k,由n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应,该二叉树称为完全二叉树。
注:完全二叉树是满二叉树的一部分,而满二叉树是完全二叉树的特例
(2)特点:
若完全二叉树的深度为k ,则所有的叶子结点都出现在第k层或k-1层。
对于任一结点,如果其右子树的最大层次为l,则其左子树的最大层次为l或l+1
最多有一个度为0的结点
(3)性质:
1.若完全二叉树有n个节点,则深度为:
(该符号表示向下取整)
2.若对一棵有n个结点的完全二叉树(深度为└㏒2n┘+1)的结点按层序自左至右进行编号,则对于编号为i(1≦i≦n)的结点:
1.若i=1:则结点i是二叉树的根,无双亲结点;否则,若i>1,则其双亲结点编号是 i/2向下取整 。
2.如果2i>n:则结点i为叶子结点,无左孩子;否则,其左孩子结点编号是2i。
3.如果2i+1>n:则结点i无右孩子;否则,其右孩子结点编号是2i+1。
(五)二叉树的存储结构:
(1)顺序存储:用一组地址连续的存储单元依次“自上而下、自左至右”存储完全二叉树的数据元素(缺点:内存浪费)

(2)链式存储结构
1.结点的类型及其定义
① 二叉链表结点。有三个域:一个数据域,两个分别指向左右子结点的指针域
typedef struct BTNode { ElemType data ; struct BTNode *Lchild , *Rchild ; }BTNode ;

② 三叉链表结点。除二叉链表的三个域外,再增加一个指针域,用来指向结点的父结点。
1 typedef struct BTNode_3 2 3 { ElemType data ; 4 5 struct BTNode_3 *Lchild , *Rchild , *parent ; 6 7 }BTNode_3 ;

2.存储形式:

(3)二叉树的二叉链表创建:
1.按满二叉树方式建立:
在此补充按满二叉树的方式对结点进行编号建立链式二叉树。对每个结点,输入i、ch。(i: 结点编号,按从小到大的顺序输入;ch:结点内容,假设是字符)
2.按先序遍历方式建立:
对一棵二叉树进行“扩充”,就可以得到有该二叉树所扩充的二叉树。

1)补充方法:是char类型:扩充结点值为“?”; 是int类型:扩充结点值为0或-1;
2)算法思路:
若是扩充结点值:令根指针为NULL;
若是(正常)结点值:动态地为根指针分配一个结点,将该值赋给根结点,然后递归地创建根的左子树和右子树。
(六)二叉树遍历:
(1)遍历方式:DLR——先(根)序遍历;LDR——中(根)序遍历;LRD——后(根)序遍历。
(2)先序遍历:
1.步骤:访问根结点->先序遍历左子树->先序遍历右子树
BDGEHICFJ
2.递归实现:
1 void PreorderTraverse(BTNode *T){ 2 3 if (T!=NULL) { 4 5 visit(T->data) ; /* 访问根结点 */ 6 7 PreorderTraverse(T->Lchild) ; 8 9 PreorderTraverse(T->Rchild) ; }}
(3)中序遍历:
1.步骤:中序遍历左子树->访问根结点->中序遍历右子树
DGBHEIAFJC。
注:中序遍历二叉排序树中的结点可以得到一个递增的关键字序列
2.递归实现:
1 void InorderTraverse(BTNode *T){ 2 3 if (T!=NULL) { 4 5 InorderTraverse(T->Lchild) ; 6 7 visit(T->data) ; /* 访问根结点 */ 8 9 InorderTraverse(T->Rchild) ;}}
( 4)后序遍历:
1.步骤:后序遍历左子树->后序遍历右子树->访问根结点
GDHIEBJFCA
2.递归实现:
1 void InorderTraverse(BTNode *T){ 2 3 if (T!=NULL) { 4 5 InorderTraverse(T->Lchild) ; 6 7 InorderTraverse(T->Rchild) ; 8 9 visit(T->data) ; /* 访问根结点 */ 10 11 }}
(5)层次遍历:
1.思路:从根结点开始遍历,按层次次序“自上而下,从左至右”访问树中的各结点。
(七)二叉树的其他操作:
(1)求二叉树的叶子结点数:可以直接利用先序遍历二叉树算法求二叉树的叶子结点数。只需加入(左右指针需均为空才计数的条件即可)
(2)求二叉树深度:利用层次遍历算法可以直接求得二叉树的深度
<二>线索树:
(一)铺垫知识:
(1)设一棵二叉树有n个结点,则有n-1条边(指针连线)
(2)而n个结点共有2n个指针域(Lchild和Rchild) ,有n+1个空闲指针域未用
(二)定义:
1.线索链表:结点的指针域做如下规定:
(1)若结点有左孩子,则Lchild指向其左孩子,否则,指向其直接前驱;
(2)结点有右孩子,则Rchild指向其右孩子,否则,指向其直接后继;
为避免混淆,对结点结构加以改进,增加两个标志域
![]()

2.线索:指向前驱和后继的只占叫做线索
3.线索二叉树:按照某种次序遍历,加上线索的二叉树。
(三)线索二叉树的结构:
1 typedef struct BiThrNode{ 2 3 ElemType data; 4 5 struct BiTreeNode *Lchild , *Rchild ; 6 7 int Ltag , Rtag ; 8 9 }BiThrNode ;

注:黄色为后继、红色为前驱
存储结构:

画线索二叉树时:实现表示指向左右孩子的指针;虚线表示线索(前驱后继)
(四)线索树的遍历:
(1)先序遍历:不需要使用递归:
1 void preorder_Thread_bt(BiThrNode *T) 2 { BiThrNode *p=T ; 3 while (p!=NULL) 4 { visit(p->data) ; 5 if (p->Ltag==0) p=p->Lchild ; 6 else p=p->Rchild 7 } 8 }
(2)中序遍历:
void inorder_Thread_bt(BiThrNode *T) { BiThrNode *p ; if (T!=NULL) { p=T; while (p->Ltag==0 ) p=p->Lchild; /* 寻找最左的结点 */ while (p!=NULL) { visit(p->data) ; if (p->Rtag==1) p=p->Rchild ; /* 通过右线索找到后继 */ else /* 否则,右子树的最左结点为后继 */ { p=p->Rchild ; while (p->Ltag==0 ) p=p->Lchild; } } } }
(五)线索化二叉树:指按照某种遍历次序使二叉树成为线索二叉树的过程。在遍历过程中修改空指针使其指向直接前驱或直接后继的过程
(1)结点类型定义:
1 #define MAX_NODE 50 2 typedef enmu{Link , Thread} PointerTag ; 3 /* Link=0表示指针, Thread=1表示线索 */ 4 typedef struct BiThrNode 5 { ElemType data; 6 struct BiTreeNode *Lchild , *Rchild ; 7 PointerTag Ltag , Rtag ; 8 }BiThrNode;
(2)先序线索化二叉树:
1 void preorder_Threading(BiThrNode *T) 2 { BiThrNode *stack[MAX_NODE]; 3 BiThrNode *last=NULL, *p ; 4 int top=0 ; 5 if (T!=NULL) 6 { stack[++top]=T; 7 while (top>0) 8 { p=stack[top--] ; 9 if (p->Lchild!=NULL) p->Ltag=0 ; 10 else { p->Ltag=1 ; p->Lchild!=last ; } 11 if (last!=NULL) 12 if (last->Rchild!=NULL) last->Rtag=0 ; 13 else 14 { last->Rtag=1 ; last->Rchild!=p ; } 15 last=p ; 16 if (p->Rchild!=NULL) 17 stack[++top]=p->Rchild ; 18 if (p->Lchild!=NULL) 19 stack[++top]=p->Lchild ; 20 } 21 Last->Rtag=1; /* 最后一个结点是叶子结点 */ 22 } 23 }
(3)中序线索化二叉树:
1 void inorder_Threading(BiThrNode *T) 2 { BiThrNode *stack[MAX_NODE]; 3 BiThrNode *last=NULL, *p=T ; 4 int top=0 ; 5 while (p!=NULL||top>0) 6 if (p!=NULL) { stack[++top]=p; p=p->Lchild; } 7 else 8 { p=stack[top--] ; 9 if (p->Lchild!=NULL) p->Ltag=0 ; 10 else { p->Ltag=1 ; p->Lchild!=last ; } 11 if (last!=NULL) 12 if (last->Rchild!=NULL) last->Rtag=0 ; 13 else { last->Rtag=1 ; last->Rchild!=p ; } 14 last=p ; 15 P=p->Rchild; 16 } 17 last->Rtag=1; /* 最后一个结点是叶子结点 */ 18 } 19 }
四、树:
(一)树的存储结构:(注:树的存储结构根据应用的不同而不同)
(1)双亲表示法:用一组连续的存储空间来存储树的结点,同时在每个结点中附加一个指示器(整数域) ,用以指示双亲结点的位置(下标值) 。
1 #define MAX_SIZE 100 2 typedef struct PTNode 3 { ElemType data ; 4 int parent ; 5 }PTNode ;
1 typedef struct 2 { PTNode Nodes[MAX_SIZE] ; 3 int root; /* 根结点位置 */ 4 int num ; /* 结点数 */ 5 }Ptree ; 6

(2)孩子链表表示法:树中每个结点有多个指针域,每个指针指向其一棵子树的根结点。有两种结点结构。
1.定长结点结构:指针域的数目就是树的度:
特点:链表结构简单,但指针域的浪费明显。

2.不定长结点结构: 树中每个结点的指针域数量不同,是该结点的度
特点:没有多余的指针域,但操作不便。


(3)孩子兄弟表示法:
1.结构:
1 typedef struct CSNode /*孩子兄弟表示法的类型定义*/ 2 { 3 DataType data; 4 struct CSNode*firstchild,*nextsibling;//指向第一个孩子和下一个兄弟 5 }CSNode,*CSTree;

五、森林与二叉树的转换:
(一)基础铺垫:
1.二叉树和树都可用二叉链表作为存储结构
2.从物理结构来看,树和二叉树的二叉链表是相同的,只是对指针的逻辑解释不同而已
3.从树的二叉链表表示的定义可知,任何一棵和树对应的二叉树,其右子树一定为空

(二)数转换为二叉树:
(1)步骤:
1.加虚线。在树的每层按从“左至右”的顺序在兄弟结点之间加虚线相连。
2. 去连线。除最左的第一个子结点外,父结点与所有其它子结点的连线都去掉。
3. 旋转。将树顺时针旋转450,原有的实线左斜。
4. 整型。将旋转后树中的所有虚线改为实线,并向右斜。
(2)特点:
1.二叉树的根结点没有右子树,只有左子树;
2.左子结点仍然是原来树中相应结点的左子结点
3.所有沿右链往下的右子结点均是原来树中该结点的兄弟结点。

(三)二叉树转换成树:
⑴ 加虚线。若某结点i是其父结点的左子树的根结点,则将该结点i的右子结点以及沿右子链不断地搜索所有的右子结点,将所有这些右子结点与i结点的父结点之间加虚线相连
⑵ 去连线。去掉二叉树中所有父结点与其右子结点之间的连线
⑶ 规整化。将图中各结点按层次排列且将所有的虚线变成实线

(四)森林转换为二叉树:
(1)步骤:
1. 将F={T1, T2,⋯,Tn} 中的每棵树转换成二叉树。
2.按给出的森林中树的次序,从最后一棵二叉树开始,每棵二叉树作为前一棵二叉树的根结点的右子树,依次类推,则第一棵树的根结点就是转换后生成的二叉树的根结点,如图6-21所示。

(五)二叉树转森林:
(1)步骤:
1.去连线。将二叉树B的根结点与其右子结点以及沿右子结点链方向的所有右子结点的连线全部去掉,得到若干棵孤立的二叉树,每一棵就是原来森林F中的树依次对应的二叉树。
2.二叉树的还原。将各棵孤立的二叉树按二叉树还原为树的方法还原成一般的树

(六)树和森林的遍历:

(1)树的遍历:
1.先序遍历:先访问根结点,然后依次先序遍历完每棵子树。(上图先序遍历的次序是:ABCDEFGIJHK)
2.后序遍历:先依次后序遍历完每棵子树,然后访问根结点。(上图后序遍历的次序是:CDBFGIJHEKA)
(2)森林的遍历:
设F={T1, T2,⋯,Tn}是森林,对F的遍历有二种方法。
1.先序遍历:按先序遍历树的方式依次遍历F中的每棵树。
2.中序遍历:按后序遍历树的方式依次遍历F中的每棵树。
(七)赫夫曼(Huffman)树及其应用:(最优树)
(1)基础铺垫:
1.路径:从树中一个结点到另一个结点的之间的分支构成这两个结点之间的路径
2.路径长度(边的数目):结点路径上的分支数目称为路径长度。
3.树的路径长度:从树根到每一个结点的路径长度之和。
4.结点的带权路径长度:从结点到树的根之间的路径长度*结点的权(值)。
5.权(值):各种开销、代价、频度等的抽象称呼。
6.树的带权路径长度:树中所有叶子结点的带权路径长度之和

(2) Huffman树定义:
具有n个叶子结点(每个结点的权值为wi) 的二叉树不止一棵,但在所有的这些二叉树中,必定存在一棵WPL值最小的树,称这棵树为Huffman树(或称最优树) 。
(3)Huffman树构造:
(为了方便理解,结合例题)

1.根据n个权值{w1, w2, ⋯,wn},构造成n棵二叉树的集合F={T1, T2, ⋯,Tn},其中每棵二叉树只有一个权值为wi的根结点,没有左、右子树
![]()
2.在F中选取两棵根结点权值最小的树作为左、右子树构造一棵新的二叉树,且新的二叉树根结点权值=左结点的权值+右结点的权值。
3.在F中删除这两棵树,同时将新得到的树加入F中;
2.3步
4.重复2.3步直到F只含一颗树为止。

(4)Huffman树特点:
1.没有度数为1的节点,又称为正则二叉树
2.n 个叶子节点的哈夫曼树,度数为2的节点数为n-1个,所以哈夫曼树一共有2n-1个结点
3.哈夫曼树任意非叶结点的左右子树交换后还是哈夫曼树
4.对同一组权值{w1,w2,...,wn},是会存在不同结构的哈夫曼树
<----END---->
浙公网安备 33010602011771号