6树
6.1定义
- 基本术语
- 结点:包含一个数据元素及若干指向其子树的分支
- 结点的度:结点拥有的子树数
- 叶子结点(Leaf):度为0的结点,亦称终端结点
- 分支结点:度不为0的结点,亦称非终端结点。除根结点之外,分支结点也称为内部结点。
- 树的度:树内各结点的度的最大值。
- 孩子(child):结点的子树的根
- 双亲(Parent):该结点为子树根节点的双亲。
- 兄弟(sibling):同一个双亲的孩子之间的互称。
- 祖先:是从___根到该结点所经分支上的所有结点___。
- 子孙:以某结点为根的子树中的任一结点
- 结点的层次(Level):包含一个数据元素及若干指向其子树的分支根开始定义起,根为第一层,根的孩子为第二层。若某结点在第i层.则其子树的根就在第i+1 层。其双亲在同一层的结点互为堂兄弟。
- 树的深度(Depth):树中结点的最大层次,亦称高度
- 有序树:将树中结点的各子树看成从左至右是有次序的(即不能互换),否则称为无序树。
- 森林(Forest):是m(m>0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以森林和树相互递归的定义来描述树。
- 基本操作 (不做叙述,自己看书)
6.2二叉树
-
定义:二叉树是n(n>=0)个结点的有限集,它或为空树(n=0),或由一个根结点和两棵分别称为左子树和右子树的互不相交的二叉树构成
-
特点:
- 每个结点至多有二棵子树(即不存在度大于2的结点)
- 二叉树的子树有左、右之分,且其次序不能任意颠倒
-
性质
- 在二叉树的第i层上至多有 2 i − 1 2^{i-1} 2i−1个结点(i ≥1)
- 深度为k的二叉树至多有 2 k − 1 2^k-1 2k−1个结点(k>=1)
- 对任何一棵二叉树T,如果其终端结点数为n_0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
- 证明: n 1 n_1 n1是二叉树中度为1的节点数,由于,二叉树所有结点的度均小于等于2,故, n = n 0 + n 1 + n 2 n = n_0+n_1+n_2 n=n0+n1+n2,且二叉树中除根节点外,其余节点都有一个入度,则 n − 1 = n 1 + 2 ∗ n 2 n-1 = n_1 + 2 * n_2 n−1=n1+2∗n2也即是 n 0 = n 2 + 1 n_0 = n_2 +1 n0=n2+1
-
特殊的二叉树
- 满二叉树
- 定义:深度为k且有 2 k − 1 2^k-1 2k−1个结点的二叉树
- 特点:每一层上的结点数都是最大结点数
- 实例:
- 完全二叉树
- 定义:深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称完全二叉树
- 特点:
- 叶子结点只可能在层次最大的两层上出现
- 对任一结点,若其右分支下子孙的最大层次为 l l l,则其左分支下子孙的最大层次必为 l l l 或 l + 1 l+1 l+1
- 性质:
- 具有n个结点的完全二叉树的深度为 ⌊ log 2 n ⌋ + 1 \lfloor \log_2{n}\rfloor+1 ⌊log2n⌋+1
- 如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点
i
(
i
≤
1
≤
n
)
i(i\leq1\leq n)
i(i≤1≤n),有:
(1)如果 i = 1 i=1 i=1,则结点 i i i是二叉树的根,无双亲;如果 i > 1 i>1 i>1,则其双亲是 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋
(2) 如果 2 i > n 2i>n 2i>n,则结点 i i i无左孩子;如果 2 i ≤ n 2i\leq n 2i≤n,则其左孩子是 2 i 2i 2i
(3) 如果 2 i + 1 > n 2i+1>n 2i+1>n,则结点 i i i无右孩子;如果 2 i + 1 ≤ n 2i+1\leq n 2i+1≤n,则其右孩子是 2 i + 1 2i+1 2i+1
- 实例
- 满二叉树
-
二叉树储存结构
- 顺序储存结构
/* 宏定义 */
#define MAX_TREE_SIZE 1024 // 二叉树的最大结点数
/* 二叉树元素类型定义,这里假设其元素类型为char */
typedef char TElemType;
/*
* 二叉树类型定义,0号单元存储根结点。
* 在二叉树的顺序结构中,其元素是按照完全顺序二叉树的层序序列排列的。
*/
typedef TElemType SqBiTree[MAX_TREE_SIZE];
-
二叉链表
空指针域 = 2n - 所有点的入度(n-1) -
三叉链表
-
遍历二叉树
-
深度优先算法
-
先序遍历 :先访问根结点,然后分别先序遍历左子树、右子树
-
中序遍历 :先中序遍历左子树,然后访问根结点,最后中序遍历右子树
-
后序遍历 :先后序遍历左、右子树,然后访问根结点
-
-
广度优先算法(层次)
- 从上到下、从左到右访问各结点
实例:
先序遍历:- + a * b - c d / e f **
中序遍历:a + b * c- d - e / f **
后序遍历:a b c d - * + e f /-
广度遍历:- + / a * e f b - c d -
代码:
//先序遍历 void preorder(BiTree *bt) { if(bt!=NULL) { printf("%d\t",bt->data); preorder(bt->lchild); preorder(bt->rchild); } } //中序遍历 void postorder(TreeNode *bt) { if(bt!=NULL) { postorder(bt->lchild); postorder(bt->rchild); printf("%d\t",bt->data); } } //后序遍历 void inorder(TreeNode *bt) { if(bt!=NULL) { inorder(bt->lchild); printf("%d\t",bt->data); inorder(bt->rchild); } }
-
中序的非递归:(栈):
此问题非常重要,作者会用专门的一个文章对此问题进行阐述,更新后会放在文章末尾。
-
-
线索二叉树
-
定义:
-
前驱与后继:在二叉树的先序、中序或后序遍历序列中两个相邻的结点互称为~
-
线索:指向前驱或后继结点的指针称为~
-
线索二叉树:加上线索的二叉链表表示的二叉树叫~
-
-
实现:
-
在有n个结点的二叉链表中必定有n+1个空链域
-
在线索二叉树的结点中增加两个标志域
-
l t : 若 l t = 0 , l c 域 指 向 左 孩 子 ; 若 l t = 1 , l c 域 指 向 其 前 驱 l_t :若 l_t =0, l_c 域指向左孩子;若 l_t=1, l_c域指向其前驱 lt:若lt=0,lc域指向左孩子;若lt=1,lc域指向其前驱
-
r t : 若 r t = 0 , r c 域 指 向 右 孩 子 ; 若 r t = 1 , r c 域 指 向 其 后 继 r_t :若 r_t =0, r_c 域指向右孩子;若 r_t=1, r_c域指向其后继 rt:若rt=0,rc域指向右孩子;若rt=1,rc域指向其后继
-
-
-
Typedef enum PointerTag { Link, Thread};
typedef struct BiThrNode {
TElemType data;
PointerTag LTag, RTag;//标志域
struct BiThrNode *lchild, *rchild;
} BiThrNode, *BiThrTree;
-
遍历线索二叉树(中序为例)
-
在中序线索二叉树中找结点后继的方法:
(1)若rt=1, 则rc域直接指向其后继
(2)若rt=0, 则结点的后继应是其右子树的左链尾(lt=1)的结点
在中序线索二叉树中找结点前驱的方法:
(1)若lt=1, 则lc域直接指向其前驱
(2)若lt=0, 则结点的前驱应是其左子树的右链尾(rt=1)的结点
-
6.3 树的储存结构
-
双亲表示法
/* 树的最大结点数 */ #define MAX_TREE_SIZE 1024 /* (双亲)树的结点定义 */ typedef struct PTNode { TElemType data; int parent; // 双亲位置域 } PTNode; typedef struct { PTNode nodes[MAX_TREE_SIZE]; // 存储树中结点 int r; // 树根位置(索引) int n; // 树的结点数 } PTree;
-
孩子表示法
typedef struct CTNode { //孩子节点 int child; struct CTNode *next; } *ChildPtr; typedef struct { TElemType data; ChildPtr firstchild; //孩子链表头指针 } CTBox; typedef struct { CTBox nodes[MAX_TREE_SIZE]; int n;// 树的结点数 int r;// 树根位置(索引) } CTree;
-
带双亲的孩子表示法
-
父子表示法
-
孩子兄弟表示法
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}
- 树与二叉树的转换
原理:储存结构类似,此问题也是考试重点,作者会用专门的文档进行笔记。- 树转换为二叉树
- 二叉树化为树
- 二叉树变为森林
- 森林变为二叉树
6.4 树与二叉树的遍历
- 树的遍历:按一定规律走遍树的各个顶点,且使每一顶点仅被访问一次,即找一个完整而有规律的走法,以得到树中所有结点的一个线性排列
- 方法:
- 先根(序)遍历:先访问树的根结点,然后依次先根遍历根的每棵子树
- 后根(序)遍历:先依次后根遍历每棵子树,然后访问根结点
- 按层次遍历:先访问第一层上的结点,然后依次遍历第二层,……第n层的结点
6.5 应用:
6.5.1哈夫曼树:带权路径长度最短的树
-
基本概念:
-
路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的~
-
路径长度:路径上的分支数
-
树的路径长度:从树根到每一个结点的路径长度之和
-
树的带权路径长度:树中所有带权结点的路径长度之和
-
Huffman树——设有n个权值{w1,w2,……wn},构造一棵有n个叶子结点的二叉树,每个叶子的权值为wi,则wpl最小的二叉树叫~
-
-
构造的步骤
- 根据给定的n个权值 w 1 , w 2 , … … w n {w_1,w_2,……w_n} w1,w2,……wn,构造n棵只有根结点的二叉树,令其权值为 w j w_j wj
- 在森林中选取两棵根结点权值最小的树作左右子树,构造一棵新的二叉树,置新二叉树根结点权值为其左右子树根结点权值之和
- 在森林中删除这两棵树,同时将新得到的二叉树加入森林中
- 重复上述两步,直到只含一棵树为止,这棵树即哈夫曼树
-
算法实现
- 一棵有n个叶子结点的Huffman树有 2 n − 1 2n-1 2n−1个结点
/* 赫夫曼树结点定义,是一种双亲存储结构 */
typedef struct {
unsigned int weight; // 权值
unsigned int parent; // 双亲位置
unsigned int lchild; // 左孩子位置
unsigned int rchild; // 右孩子位置
} HTNode;
/*
* 赫夫曼树类型定义
* 0号单元的weight域指示赫夫曼树的结点数量
* 存储空间动态分配
*/
typedef HTNode* HuffmanTree;
-
Huffman编码与解码
- 编码:将树中结点引向其左孩子的分支标“0”,引向其右孩子的分支标“1”
- 解码:若编码是“0”,则向左走;若编码是“1”,则向右走,一旦到达叶子结点,则译出一个字符;
6.5.2二叉排序树
-
定义:二叉排序树或是一棵空树,或是具有下列性质
-
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
-
若它的右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值
-
它的左、右子树也分别为二叉排序树
-
-
插入:
-
若二叉排序树为空,则插入结点应为新的根结点
-
否则,继续在其左、右子树上查找,直至某个结点的左子树或右子树为空为止,则插入结点应为该结点的左孩子或右孩子
-
-
删除:
- p为叶子结点:修改p双亲f的指针
$ f->lchild=NULL ; f->rchild=NULL$
-
¨p只有左子树或右子树
- p只有左子树,用p的左孩子代替p
- p只有右子树,用p的右孩子代替p
-
¨p左、右子树均非空
- 沿p的左子树的根C的右子树分支找到S,S的右子树为空,将S的左子树成为S的双亲Q的右子树,用S取代p
- 若C无右子树,用C取代
本文为华中科技大学(WuHan China)人工智能与自动化学院 2020级《数据结构与算法分析》课堂所做的个人笔记,结课之余慢慢更新,未来将继续更新《数据结构习题集(C语言版)》解析以及所用代码等。
当然,如果你会使用git工具,也可以直接在作者的Github个人主页直接下载学习
本文相关链接:作者的Github相关主页
由于作者才疏学浅,文章有错误之处请私信或发邮件(szh-hust@outlook.com)指正