ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、视频、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 树的概念 树(Tree):树是n(n>=0)个结点的有限集T,T为空时称为**空表**,否则它满足一下两个条件: + 有且仅有一个特定的称为**根(root)**的结点 + 其余的结点可分为m(m>=0)个互不相干的子集T1、T2、T3...Tm,其中每个子集本身又是一棵树,称为**子树**。 **树的图形表示** 结点:通常使用**圆圈**表示 结点名字:一般写在圆圈旁边(有时写在圆圈内),如图: ![](https://box.kancloud.cn/100f3de05774c86e4f903d6fab30b794_972x504.png) 通常有四种表示法:树形表示法、嵌套集合表示法、凹入表表示法、广义表表示法。 **树结构常用的基本术语** + 度(Degree):一个树拥有的子树数。 + 叶子(Leaf)或终端结点:度为零的结点。 + 分支结点或非终端结点:度不为零的结点 + 内部结点:除根结点之外的分支结点 + 开始结点:根节点又称为开始结点。 > 一棵树的度指的是该树结点的最大的度数。 > 如上图中的A、B、E的度分别为3、2、0。 > 树的度为 3 。 > C、E、G、H、I、J均为叶子。 + 孩子(Child)或儿子:树中某个结点的子树之根,该结点称为**双亲**或父亲。 + 兄弟:同一个双亲的孩子 > 例子: > 上图中,B是A的子树T1的根,B是A的孩子,A是B的双亲。 > B、C、D互为兄弟。 + 路径(Path)或道路:若树中存在一个结点序列k<sub>1</sub>,k<sub>2</sub>,k<sub>3</sub>....k<sub>j</sub>,使得k<sub>1</sub>是k<sub>i+1</sub>的双亲,则称该结点是从 k1 到 kj的一条路径。路径的长度为:j-1。 > 例子: > 如上图: > 结点 A 到 I有一条路径: `ABFI`,它的长度为 3。 + 祖先(Ancestor):树中结点 k 到 k<sub>s</sub>存在一条路径,那么k是k<sub>s</sub>的祖先,k<sub>s</sub>是k的子孙(Descendant) 约定:结点 k 的祖先和子孙不包含结点 k本身。 > 例子: > `F` 的祖先:A、B; > `F` 的子孙:I、J。 + 结点的层数是从 根 算起的,设层数为 1。 + 堂兄弟:双亲在同一层的结点互为**堂兄弟**。 + 树的高度(Height)或深度(Depth):树种结点的最大层数。 > 例子: > A的层数:1,I、J的层数:4; > E、F和G、H互为堂兄弟,该树的高度:4 > Note:很多书上树根定义的层数为 0. + 有序树(Ordered Tree):树中的每个结点的各子树看成从左往右有次序的,则该树称为**有序树**,否则称为**无序树**。 > 例子: > 下图中的两棵树,如果作为有序树则它们是不同的,因为A结点的两个孩子的左右次序不一样。作为无序树,它们是相同的。 ![](https://box.kancloud.cn/8989b30c206e9680533fb61af29aae7f_350x248.png) + 森林:m(m>=0)棵互不相交的树的集合。 > 删去一棵树的根,就得到了一个森林。 > 加上结点作为树根,森林变成了一棵树。 # 二叉树 二叉树(Binary Tree):n (n>=0)个结点的有限集,它或者是空集(n=0),或是由一个根节点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。 二叉树的五种基本形态: + 空二叉树 + 仅有一个根节点的二叉树 + 右子树为**空**的二叉树 + 左子树为**空**的二叉树 + 左右子树均为*非空***的二叉树 > 二叉树中每个结点最多只能由两颗子树,并有左右之分。(只有一颗子树也有左右之分) ![](https://box.kancloud.cn/b3085886ef58e3c1567c37e2018e9f67_654x356.png) ## 二叉树的性质 + **性质 1** 二叉树第 i 层的结点数目最多为`2<sup>i-1</sup>(i>=1)`。 + **性质 2** 深度为 k 的二叉树至多为`2<sup>k</sup>-1(k>=1)`个结点。 + **性质 3** 在任意一颗二叉树中,若终端结点的个数为 n<sub>0<sub>,度为 2 的结点数为n<sub>2<sub>,则 `n<sub>0<sub> = n<sub>2<sub> + 1` **满二叉树** 一棵深度为 k 且有 2<sup>k-1</sup> 个结点的二叉树。 特点:每一层上的结点数达到最大值;满二叉树不存在度数为 1 的结点,每个分支结点均有两棵高度相等的子树。 **完全二叉树** 若一颗二叉树至多只有最下面的两层上的结点的度数小于2,并且最下层上的结点都集中在最左边的若干位置。 > 满二叉树是完全二叉树,反之则不成立。 在满二叉树的最下一层,从最右边开始**连续删去**若干结点后得到二叉树仍是一棵完全二叉树。 ![](https://box.kancloud.cn/0b0ced01169a3f559c6ecd9aa296f71c_1028x330.png) + **性质 4** 具有 n 个结点的完全二叉树的深度为 └lgn┘ + 1或者┌lg(n+1)┐。 ## 二叉树的存储结构 **1. 顺序存储结构** 完全二叉树使用顺序结构存储既简单又节省空间。一般二叉树使用顺序结构存储也会按照完全二叉树的存储结点,会造成空间的浪费。 ![](https://box.kancloud.cn/c27c214e2ef747ffbf266a5e8ad06a71_1031x233.png) **2. 链式存储结构** |lchild|data|rchild| |-----|----|----| 数据结构的说明为: ```c typedef char DataType; typedef struct node{ DataType data; struct node *lchild, *rchild; // 左右孩子指针 }BinType; // 结点类型 typedef BinType * BinTree; ``` ![](https://box.kancloud.cn/625dc6d60745b868a2a036facf42c556_1016x418.png) 二叉链表:二叉树的链式存储结构。 > 一个二叉链表由根指针 root 唯一确定。 > 若二叉树为空,则 root = NULL > 若结点的某个孩子不存在,则相应的指针为 空。 具有 n 个结点的二叉树的二叉链表表示中,一种有 `2n` 个指针域,其中只有`n-1`个用来表示结点的左右孩子,其余`n+1`个指针域为空。如图: ## 二叉树的遍历 给定任意结点,可以按照某种次序执行三个操作: + 访问结点本身(N) + 遍历该节点的左子树(L) + 遍历该结点的右子树(R) 以上这三种操作还有六种执行顺序: + NLR + LNR + LRN + NRL + RNL + RLN 前三种次序是左子树总是先于右子树被遍历。 我们将这三种遍历分别称为: + **前序遍历(Preorder Traversal)**或**先序遍历**: NLR(先根遍历) + 中序遍历(Inorder Traversal):LNR(中根遍历) + 后续遍历(Postorder Traversal):LRN(后根遍历) N(Node)、L(Left subtree)、R(Right subtree)可理解为根、根的左子树、根的右子树 ### 中序遍历 递归算法定义为: + 遍历左子树 + 访问根节点 + 遍历右子树 递归的终止条件为:二叉树为空 使用二叉链表作为存储结构,算法如下: ```c void Inorder(BinTree T) { if (T) { Inorder(T->lchild); printf("%c", T->data); // 访问结点 Inorder(T->rchild); } } ``` **构造二叉链表** ```c void CreateBinTree(BinTree *T) { char ch; if ((ch = getchar()) == ' ') { *T = NULL; // 写入空格,将相应指针置空 } else { *T = (BinTree *)malloc(sizeof(BinTree)); // 生成结点 (*T)->data = ch; CreateBinTree(&(*T)->lchild); //构造左子树 CreateBinTree(&(*T)->rchild); //构造右子树 } } ``` 时间复杂度为: O(n), n表示树的结点数 ## 线索二叉树 用二叉链表作为存储结构时,它只有左右孩子的指针域,没办法存储结点的前趋和后继结点的指针,这种附加的指针称为**线索**。加上线索的二叉链表为**线索链表**,相应的二叉树为**线索二叉树(Threaded Binary Tree)**。 其结构如下: | lchild | ltag | data | rtag | rchild | | --- | --- | --- | --- | --- | 左标志:ltag + 0: lchild是指向结点的左孩子的指针 + 1:lchild是指向结点的前趋的左线索 右标志:rtag + 0: rchild是指向结点的右孩子的指针 + 1:rchild是指向结点的前趋的右线索 一个结点是叶结点的充要条件:它的左、右标志均为1 线索化:二叉树变成二叉线索树的过程。 > 按照某种次序将二叉树线索化,只要按该次序遍历二叉树,在遍历过程中用线索代替空指针。 二叉树线索化的算法: ```c typedef enum {Link, Thread} PointerTag; // 枚举值Link、Thread分别为0,1 typedef struct node { DataType data; PointerTag ltag, rtag; //左右标志 struct node *lchild, *rchild; } BinThrNode; typedef BinThrNode *BinThrTree; BinThrNode *pre = NULL; //全局量,始终指向刚刚访问过的结点 // 将二叉树 p 线索化 void InorderThreading(BinThrTree p) { if (p) { InorderThreading(p->child); // 左子树线索化 // 以下相当于遍历算法中访问结点操作 // 左指针非空左标志为Link(即0),否则为Thread(即为1) p->ltag = (p->lchild) ? Link : Thread; p->ltag = (p->lchild) ? Link : Thread; p->rtag = (p->rchild) ? Link : Thread; if (pre) { // 若*p的前趋*pre存在 // *p的前趋右标志为线索 if (pre->rtag == Thread) { pre->rchild = p; // 令*pre的右线索指向中序后继 } // *p的左标志为线索 if (p->ltag == Thread) { p->lchild = pre;// 令*pre的左线索指向中序前趋 } } pre = p; // 令pre的下一访问结点的中序前趋 InorderThreading(p->rchild); //右子树线索化 } } ``` **二叉树上常见的运算** 1. 查找某结点 *p 在指定次序下的前趋和后继结点 中序线索二叉树求**中序后继结点**的算法: ① 若 *p 的右子树空(即p->rtag == Thread),则p->rchild为右线索。直接指向 *p 的中序后继。 ![](https://box.kancloud.cn/7ffa00aa1ba5f3a97af1be1530eede85_856x761.png) ② 若 *p 的右子树非空(即p->rtag == Link),则*p的中序后继必是其右子树中第一个中序遍历结点(从*p的右孩子开始,沿着孩子的左链往下找,直到找到一个没有左孩子的结点为止)。 ![](https://box.kancloud.cn/8f0339cd99ca28a9fb748440e875ea46_617x422.png) ```c // 在中序线索树中找结点 *p 的中序后继。设p非空 BinThrNode * InorderSuccessor(BinThrNode *p){ BinThrNode *q; // *p的右子树为空 if (p->rtag == Thread ) { return p->rchild; // 返回右线索指的中序后继 } else { q = p->rchild;// 从*p的右孩子开始查找 while(q->ltag == Link) { q = q->lchild; // 左子树非空时,沿左链往下查找 } return q; // 当 q 的左子树为空时,他就是最左下结点 } } ``` 2. 遍历线索二叉树 中序遍历算法: ```c void TraverseInorderThrTree(BinThrTree p) { if (p) { while (p->ltag == Link) { p = p->lchild; } do { printf("%c", p->data); // 访问结点 p = InorderSuccessor(p); // 找*p的中序后继 }while( p ); } } ``` 解释:中序序列的终端结点的右线索为空,所以do语句的终止条件是 `P == NULL`。 ## 树和森林 ### 树、森林到二叉树的转换 **1. 树、森林到二叉树的转换** **树转换为二叉树** 树中的每个结点可能有多个孩子,但二叉树种每个结点最多有两个孩子。 树中每个结点最多只有一个最左的孩子和一个右邻的兄弟。 所以将**将树转换成相应的二叉树为**: + 在所有的兄弟结点之间加一些线; + 对每个结点,除了保留其长子的连线外,去掉该结点与其他孩子的连线。 ![](https://box.kancloud.cn/00593af486b8c37b04e831d85d4016ca_998x351.png) **森林转换为二叉树** + 先将森林中的每棵树变为二叉树; + 将各二叉树的根连接视为兄弟结点从左往右连在一起。 ![](https://box.kancloud.cn/c5cbb046845c6e980f84209da053a0ec_927x304.png) **2. 二叉树到树、森林的转换** + 若结点 x 是其亲 y 的左孩子,则把x的有孩子,右孩子的右孩子,... 都与y用线连接起来 + 最后去掉所有双亲到右孩子的连线。 ![](https://box.kancloud.cn/410644dd462935e1ac44b9a7a65afa9d_675x386.png) ### 树的存储结构 + 双亲链表表示法 + 孩子链表表示法 + 孩子兄弟链表表示法 **1. 双亲链表表示法** 在树中,每个结点的双亲是唯一的。 形式说明如下: ```c #define MaxTreeSize 100 typedef char DataType; typedef struct { DataType data; int parent; // 双亲指针,指示结点的双亲在向量中的位置 }PTreeNode; typedef struct { PTreeNode nodes[MaxTreeSize]; int n; //结点总数 }PTree; PTree T; // T是双亲链表 ``` ![](https://box.kancloud.cn/490f428f8f7b38fe07d7d2056ddc00ca_603x162.png) 若 `T.nodes[i].parent == j`,则`T.nodes[i] == T.nodes[j]`。如E、F所在结点的双亲域是1,表示它们的双亲结点在向量中的位置是 1,即B是它们的双亲。 > 根无双亲,其`parent`为-1 **2. 孩子链表表示法** ```c // 孩子链表结点 typdedef struct cnode{ int child; // 孩子结点在向量中对应的序号 struct cnode *next; }CNode; typedef struct { DataType data; // 存放树中的结点数据 CNode * firstchild; // 孩子链表的头指针 }PTNode; typedef struct { PTNode nodes[MaxTreeSize]; int n, root; // n为结点总数,root指出根在向量中的位置 }CTree; CTree T; // T为孩子链表表示 ``` ![](https://box.kancloud.cn/073234d4e5b6c2c69624ecfbed309fa3_340x292.png) 上图使用孩子链表表示如下: ![](https://box.kancloud.cn/b024abb508153de6ac44c3daa2090742_699x1103.png) 说明: 当结点 **3. 孩子兄弟链表表示法**