自己在看《大话数据结构》这本书,顺便需要练习使用博客,就顺便把自己的读书笔记发上来啦。
树的定义
一对多的数据结构
树(Tree)是 n (n>=0)个结点的有限集。n = 0时称为空树。在任意一颗非空树中:
(1) 有且仅有一个特定的称为根(Root)的结点;
(2) 当 n >1时,其余结点可分为 m(m > 0)个互不相交的有限集 T1、T2、…、Tm,其中每个集合本身又是一棵树,并且称为根的子树(SubTree)。
树的定义是用到了递归的方法,也就是树的定义之中还用到了树的概念。
注意:
- n > 0 时,根结点是唯一的。
- m > 0 时,子树的个数没有限制,但它们一定是互不相交的。
结点分类
树的结点包含一个数据元素及若干指向其他子树的分支。
结点拥有的子树数称为结点的度(Degree)。
度为0的结点称为叶结点(Leaf)或终端结点;
度不为0的结点称为非终端结点或分支结点。
除根结点之外,分支结点也称为內部结点。
树的度是树内各结点的度的最大值。
结点间关系
结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。(结点其父母同体)
同一个双亲的孩子之间互称兄弟(Sibling)。
结点的祖先是从根到该结点所经分支上的所有结点。
以某结点为根的子树中的任一结点都称为该结点的子孙。
树的其他相关概念
结点的层次(Level)从根开始定义,根为第一层,根的孩子为第二层。
若某结点在第 l 层,则其子树的根就在第 l + 1 层。
其双亲在同一层的结点互为堂兄弟。
树中结点的最大层次称为树的深度(Depth)或高度。
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
**森林(Forest)是 m (m≥0)棵互不相交的树的集合。**对于树中每个结点而已,其子树的集合即为森林。
(树是由一个根结点和若干棵子树构成。树中结点具有相同数据类型及层次关系。)
树的存储结构
双亲表示法
树,除了根结点之外,其余每个结点,它不一定有孩子,但是一定有且仅有一个双亲。
双亲域、长子域、右兄弟域
存储结构的设计是一个非常灵活的过程。一个存储结构设计得是否合理,取决于基于该存储结构的运算是否合适、是否方便,时间复杂度好不好等。
孩子表示法
把每个结点的孩子结点排列起来,以单链表作存储结构,则n 个结点有 n 个孩子链表,如果是叶子结点则此单链表为空。然后 n 个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中
双亲孩子表示法
孩子兄弟表示法
把一棵复杂的树变成了二叉树
二叉树
Binary Tree
特殊的二叉树
- 斜树
所有结点都只有左子树的二叉树叫左斜树。
所有结点都是只有右子树的二叉树叫右斜树。
斜树的特点:每一层只有一个结点,结点的个数与二叉树的深度相同。 - 满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层,这样的二叉树称为满二叉树。 - 完全二叉树
对一棵具有 n 个结点的二叉树按层序编号,如果编号为 i (1 ≤ i ≤ n)的结点与同样深度的满二叉树中编号为 i 的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
满二叉树一定是完全二叉树,而完全二叉树不一定是满二叉树。
完全二叉树的特点:
- 叶子结点只能出现在最下两层。
- 最下层的叶子一定集中在左部连续位置。
- 倒数二层,若有叶子结点,一定都在右部连续位置。
- 如果结点度为 1 ,则该结点只有左孩子,即不存在只有右子树的情况。
- 同样结点数的二叉树,完全二叉树的深度最小。
二叉树的性质
二叉树性质1
在二叉树的第 i 层上至多有 2^(i - 1) 个结点。( i ≥ 1)
二叉树性质2
深度为 k 的(k 层)二叉树至多有 2^k - 1 个结点。( i ≥ 1)
二叉树性质3
对于任意一棵二叉树 T,如果其终端结点数为 n0,度为 2 的结点数为 n2,则 n0 = n2 + 1。
结点总数 n = n0 + n1 + n2
分支线总数 = n - 1 = n1 + 2 * n2
二叉树性质4
二叉树性质5
二叉树存储结构
二叉树顺序存储结构
顺序存储结构一般只用于完全二叉树。
二叉链表
一个数据域,两个指针域。
增加一个指向双亲的指针域,三叉链表。
遍历二叉树
二叉树遍历原理
(traversing binary tree) 从根节点出发,按某种次序访问所有结点,使得每个结点被访问一次且仅被访问一次。
访问和次序。
二叉树遍历方法
若树为空,则空操作返回,否则:
-
前序遍历
(1) 访问根结点
(2) 前序遍历左子树
(3) 前序遍历右子树
- 中序遍历
(1)中序遍历左子树
(2)访问根节点
(3)中序遍历右子树
- 中序遍历
-
后序遍历
从左到右 先叶子 后结点的方式遍历访问左右子树,然后访问根结点。
-
层序遍历
从 根结点开始,从上而下逐层遍历,同一层,从左往右逐个访问。
计算机只会处理线性序列,进行循环或判断。四种遍历方式把树中的结点变成某种意义上的线性序列。
前序遍历算法
递归。
//二叉树的前序遍历递归算法
void PreOrderTraverse(BiTree T)
{
if (T == null)
return;
printf("%c", T->data); //显示结点数据,可以更改为对结点其他操作
PreOrderTraverse(T->lchild);//再先序遍历左子树
PreOrderTraverse(T->rchild); //最后先序遍历右子树
}
中序遍历算法
// 二叉树的中序遍历递归算法
void InOrderTraverse(BiTree T)
{
if (T == null)
return;
InOrderTraverse(T->lchild); //中序遍历左子树
printf("%c", T->data);
InOrderTraverse(T->rchild); //最后中序遍历右子树
}
后序遍历算法
// 二叉树的后序遍历递归算法
void PostOrderTraverse(BiTree T)
{
if (T == null)
return;
PostOrderTraverse(T->lchild); //先后序遍历左子树
PostOrderTraverse(T->rchild); //再后序遍历右子树
printf("%d", T->data); //显示结点数据,可以更改为其他对结点操作
}
推导遍历结果
二叉树遍历的性质:
- 已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树;
- 已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
- 已知前序和后序遍历,不能确定一棵二叉树。
二叉树的建立
原二叉树的扩展二叉树
扩展二叉树可以做到一个遍历序列确定一棵二叉树了。
前序遍历
按前序输入二叉树中结点的值(一个字符)
# 表示空树,构造二叉链表表示二叉树 T
递归
线索二叉树
线索二叉树原理
n 个结点的二叉链表,有 2n 个指针域
n 个结点的二叉树有 n - 1 条分支线数,所以会存在 2n - (n - 1) = n + 1 个空指针域。
二叉树遍历后的字符序列,可以很清楚地知道某个结点的前驱与后继,但在二叉链表上,我们只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱与后继,要想知道,就必须遍历一次。
利用空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。
指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。
将一棵二叉树进行中序遍历后,将所有空指针域中的rchild,改为指向当前结点的后继,lchild改为指向它的前驱。
线索二叉树等于是把一棵二叉树转变成了一个双向链表。
对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。
区分前驱与左孩子,后继与右孩子,每个结点增设两个标志域 ltag 与 rtag。
其中:
- ltag为 0 时指向该结点的左孩子,为 1 时指向该结点的前驱。
- rtag为 0 时指向该结点的右孩子,为 1 时指向该结点的后继。
线索二叉树结构实现
线索化的过程就是在遍历的过程中修改空指针的过程。
中序线索化部分代码:
if (p)
{
InTreading (p -> lchild); //递归左子树线索化
if (!p -> lchild) //没有左孩子
{
p -> LTag = Thread; //前驱线索 Thread == 1
p -> lchild = pre; //左孩子指针指向前驱
}
if (!pre -> rchild) //前驱没有右孩子
{
pre -> RTag = Thread; //后继线索
pre -> rchild = p; //前驱右孩子指针指向后继(当前结点 p)
}
pre = p; //保持 pre 指向 p 的前驱
InThreading (p -> rchild); //递归右子树线索化
}
if (!p -> lchild) 表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,赋值给了 pre,所以可以将 pre 赋值给 p->lchild,并修改p -> LTag = Thread(也就是定义为 1)以完成前驱结点的线索化。
后继稍微有点麻烦,因为此时 p 结点的后继还没有访问到,因此只能对它的前驱结点 pre 的右指针 rchild 做判断,if (!pre -> rchild) 表示如果为空,则 p 就是 pre 的后继,于是 pre->rchild = p,并且设置pre -> RTag = Thread,完成后继结点的线索化。
完成了前驱和后继的判断后,别忘记将当前结点 p 赋值给 pre,以便于下一次使用。
对线索二叉树遍历时发现,其实相当于遍历一个双向链表。
在二叉树线索链表上添加一个头结点,并令其 lchild 域的指针指向二叉树的根节点,其 rchild 域的指针指向中序遍历时访问的最后一个结点。反之,令二叉树的中序序列中的第一个结点中,lchild 域和最后一个结点的 rchild 域指针均指向头结点。这样的定义的好处就是我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
线索二叉树给二叉树的结点查找和遍历带来了高效率。
时间复杂度:O(n)
树、森林于二叉树的转换
树的存储结构中,树的孩子兄弟法可以将一棵树用二叉链表进行存储,所以借助二叉链表,树和二叉树可以相互进行转换。从物理结构来看,它们的二叉链表也是相同的,只是解释不太一样而已。因此,只要我们设定一定的规则,用二叉树来表示树,甚至表示森林都是可以的,森林与二叉树也可以相互转换。
树转换为二叉树
步骤:
- 加线。在所有兄弟结点之间加一条连线。
- 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点的连线。
- 层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
森林转换为二叉树
森林中的每一棵树都是兄弟。
步骤:
- 把每棵树转换为二叉树。
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树。
二叉树转换为树
步骤:
- 加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点…,即就是左孩子的 n 个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
- 去线。删除原二叉树中所有结点与其右孩子结点的连线。
- 层次调整。使之结构分明。
二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,看这棵二叉树有没有右孩子。有就是森林,没有就是树。
步骤:
- 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除… 直到所有右孩子连线都删除为止,得到分离的二叉树。
- 再将每棵分离的二叉树转换为树即可。
森林与树的遍历
森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。
当以二叉链表作树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。
赫夫曼树及其应用
压缩编码方法——赫夫曼编码
赫夫曼树定义与原理
叶子结点带权的二叉树
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称作路径长度。
树的路径长度就是从树根到每一结点的路径长度之和。
考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。
树的带权路径长度为树中所有叶子结点的带权路径长度之和。
带权路径长度WPL最小的二叉树称做赫夫曼树。
构造赫夫曼树的赫夫曼算法描述:
- 根据给定的 n 个权值 { w1,w2,…,wn}构成 n 棵二叉树的集合 F={ T1,T2,…,Tn},其中每棵二叉树 Ti中只有一个带权为 wi根结点,其左右子树均为空。
- 在 F 中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
- 在 F 中删除这两棵树,同时将新得到的二叉树加入 F 中。
- 重复 2 和 3 步骤,直到 F 只含一棵树为止。这棵树便是赫夫曼树。
赫夫曼编码
权值左分支改为 0 ,右分支改为 1 的赫夫曼树。
用从树根到叶子所经过路径的 0 和 1 来编码。
如何解码?
若要设计长短不等的编码,则必须是任一字符的编码都不是另一字符的编码的前缀,这种编码称做前缀编码。
然后解码时,还是要用到赫夫曼树,即发送方和接收方必须要约定好同样的赫夫曼编码规则。
一般地,设需要编码的字符集为{ d1,d2,…,dn },各个字符在电文中出现的次数或频率集合为 { w1,w2,…,wn},以d1,d2,…,dn作为叶子结点,以w1,w2,…,wn作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表 0,右分支代表 1,则从根结点到叶子结点所经过的路径分支组成的 0 和 1 的序列便为该结点对应字符的编码,这就是赫夫曼编码。