![776e15056f29f43ca7df1a6fcc295bfd.png](https://i-blog.csdnimg.cn/blog_migrate/e8e620c1f13e2c129e924267f4e05400.jpeg)
第六章 树
6.2 树的定义
树是n个结点的有限集。n = 0 时称为空树,在任意一棵非空树中:(1)有且只有一个特定的称之为根的结点;(2)当 n > 1 时,其余结点可分为m(m > 0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称之为根的子树
![892c266ffa80c7c4a88af6ec9e4a90b8.png](https://i-blog.csdnimg.cn/blog_migrate/a8d0c1353375aa082d6c15b1450863dd.png)
强调两点:
(1)n > 0 时,根结点是唯一的
(2)m > 0 时,子树的个数不受限制,但是子树之间是不相交的
下面两个就不能称之为树,因为子树之间相交了
![197932a2b06ee69da4f91abf65c4bf2b.png](https://i-blog.csdnimg.cn/blog_migrate/1ac3ee8e54671d52f541b539ce53d427.png)
6.2.1 结点分类
树的结点包含一个数据元素和多个指向其子树的分支。结点拥有的分支数称之为结点的度,度为0的结点是叶子节点,度不为0的结点是分支结点或非终端结点。分支节点中,除了根结点,分支结点也被称之为内部结点。树的度是树中各结点的度的最大值
![92627f2d75bd3c95a4e70543efe8bb44.png](https://i-blog.csdnimg.cn/blog_migrate/aa08f7a9c46392c20e925846067e4587.png)
6.2.2 结点间关系
结点的子树的根称之为该节点的孩子(Child),相应的,该结点称之为孩子的双亲(Parent),同一双亲的孩子之间互称兄弟(Sibling)。
该结点的祖先是从根到该结点所经分支上的所有结点。以某结点为根的子树中的任一结点都称之为该结点的子孙
![f178a0472a923f9ba4db2be3380164a7.png](https://i-blog.csdnimg.cn/blog_migrate/6f7d2e5f315db7dd91b7bc4f049b470f.png)
6.3.2 树的其他相关概念
节点的层次(Level)从根开始定义,根为第一层,根的孩子为第二层。以此类推,其中树的深度或高度为树中结点的最大层次
![fb3fc3531682aba7126adaa395db980a.png](https://i-blog.csdnimg.cn/blog_migrate/456207ec0a03be2ef120632b6161a749.png)
森林(Forest)是m(m >= 0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
![d259b42465b99b92fb7ad64241ef4345.png](https://i-blog.csdnimg.cn/blog_migrate/cf9aa8d1d62c87216a4666ff45c2991f.png)
6.3 树的抽象数据类型
![01f2f622f28d1e3d59cc5ac81c7978fc.png](https://i-blog.csdnimg.cn/blog_migrate/7c71da6d16abd6dc766d2d236199c34e.jpeg)
6.4 树的存储结构
6.4.1 双亲表示法
我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置
![17841fc0d4161e7b2d030ed218d11733.png](https://i-blog.csdnimg.cn/blog_migrate/fe87a971834ac7118dc3bd1510fd6367.png)
data为数据域,存储结点的数据信息,parent为指针域,存储该结点的双亲在数组中的下标
![c4a0260fab4b4842d42d471549ae402b.png](https://i-blog.csdnimg.cn/blog_migrate/9cef0b84bee46947e346ed4390d0ca67.png)
![aea88369221dd71e46b955c1393d1d64.png](https://i-blog.csdnimg.cn/blog_migrate/a33bf7ee490367d423345bc60b41c005.jpeg)
我们约定根结点的位置域为-1
![51ec648f95ecb7f41c000769ea8d3b2b.png](https://i-blog.csdnimg.cn/blog_migrate/b6800c827a7843aede83c1366f8d2931.png)
我们根据结点的parent指针很容易找到双亲结点,所用的时间复杂度为O(1),但是如果想要找到结点的孩子,就需要遍历整个树
所以我们增加了一个结点的左孩子域,即长子域,而对于没有长子的结点,我们设置其长子域为-1
![6b0724e8ea06a77e543cbe16e4c03641.png](https://i-blog.csdnimg.cn/blog_migrate/92e64ff1c28d791fb6c88a1cef734a25.png)
这样表示后,我们发现,这种表示方法不能表示兄弟之间的关系,于是,我们有设置了右兄弟域,如果右兄弟不存在,用-1表示
![432588c61fc00e117d93a72cb3f3e570.png](https://i-blog.csdnimg.cn/blog_migrate/135eaf30e4bc7b7e275e097b7f1d51ae.png)
6.4.2 孩子表示法
每个结点有多个指针域,其中每个指针指向子树的根结点,我们把这种方法称之为多重链表表示法
方案一
该方案中,指针域的个数就是树的度数
![e1503e879b33747c257eff5837d88a01.png](https://i-blog.csdnimg.cn/blog_migrate/911e84ea841739a6d3df1d0808df6611.png)
其中data是数据域。child1到child2是指针域,用来指向该结点的孩子结点
![884f3a420abf3234add09e705ee618bb.png](https://i-blog.csdnimg.cn/blog_migrate/c63deaecfbbd2b45b940ce6d48eabb4e.png)
这种方案空指针太多,并没有有效利用空间
方案二
该方案每一个结点的指针域的个数为该结点的度,然后专门取了一个位置存放该结点的度
![709363b4c3ec6e7ae637588bcfe1d661.png](https://i-blog.csdnimg.cn/blog_migrate/0fd10a38c4396864ea626b3c9a7fec84.png)
degree为度域,存放指针域的个数
![db0e6e14239e4abcbeed063e862e7d20.png](https://i-blog.csdnimg.cn/blog_migrate/2522d61f4dd3e16bd4b46de0e180a6d5.png)
这种方案虽然有效地利用了空间,但是由于各个节点的链表是不同的结构,维护起来会很麻烦
6.4.3 孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此我们设计两个指针,分别指向该结点的第一个孩子和右兄弟
![650b680da5a23bdc6d11ef2ba1d0eaa4.png](https://i-blog.csdnimg.cn/blog_migrate/92caf0a00aaf34c1a44d3786a82201e2.png)
结构代码如下:
![1161173ec0195124dfee4098930b336e.png](https://i-blog.csdnimg.cn/blog_migrate/d79554f9687861655dcb5f085357d44a.png)
示意图如下
![9bef3316cd51a6fef6b85cf80a669407.png](https://i-blog.csdnimg.cn/blog_migrate/436e9b373d83a3fa0d74f012abfa6490.png)
这种方法的好处就是将树转化成了二叉树
6.5 二叉树的定义
二叉树是由n个结点组成的有限集合,该集合或者称之为空集(称为空二叉树),或者由一个根结点和两个互不交叉的,分别为根结点的左子树和右子树的二叉树组成。
![dceae823a842e3acb0af75a884c5de17.png](https://i-blog.csdnimg.cn/blog_migrate/961ab8a4d99fcbd832bd8ffadf8a9d6e.png)
6.5.1 二叉树的特点
1、每个结点,最多有两个分支
2、左子树和右子树是有顺序的,不能颠倒
3、即使结点只有一个子树,也要分清是左子树还是右子树
![2b3e508eecd98ee39ef65d0c4ca19169.png](https://i-blog.csdnimg.cn/blog_migrate/a740ed012b9bc1dc0c1ba1df401ee4fd.png)
6.5.2 特殊的二叉树
1、斜树
所有的结点都只有左子树称为左斜树。所有结点都只有右子树称为右斜树。两者通称为斜树。斜树每一层都只有一个结点,结点的个数与二叉树相同
2、满二叉树
在一棵二叉树中,所有的结点都有左子树和右子树,且所有的叶子结点都在同一层,这样的二叉树称之为满二叉树
满二叉树有以下的特点:
1、叶子结点都在最后一层
2、除了叶子结点的所有结点的度都是2
3、在同样深度的二叉树中,满二叉树的结点数和叶子结点数是最多的
![6d9eb22cfccdb7e074fc9125ce6763c9.png](https://i-blog.csdnimg.cn/blog_migrate/6dc87d5608e7e8eb20f2123746f7468d.png)
3、完全二叉树
对一棵具有n个结点的二叉中的结点进行编号,如果该二叉树的结点编号和与之相对应的满二叉树的结点编号相对应的话,则称这个二叉树为完全二叉树
![bf4da254d7875e2e5a3f0dd1cf19cf9c.png](https://i-blog.csdnimg.cn/blog_migrate/ddc0f746ad28ac660959b15c81dd9ce6.png)
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
完全二叉树的特点:
1、叶子结点只存在最后两层
2、最后一层的叶子结点统一出现在左侧
3、倒数第二层的叶子结点统一出现在右侧
4、如果一个结点的度为1,那么该结点的只有左孩子
5、同样结点数的二叉树,完全二叉树的深度最小
6.6 二叉树的性质
6.6.1 二叉树的性质1
在二叉树的第i层上至多有
6.6.2 二叉树的性质2
深度为k的二叉树至多有
6.6.3 二叉树的性质3
对于任何一个二叉树T,假设度为2的结点个数是
证明:
对于树T,总的分支数是总结点数减去1,即总分支数为:
6.6.4 二叉树的性质4
具有n个结点的 完全二叉树 的深度为
![31c3b6ad12e0b42d9c411d20ab1f8d9f.png](https://i-blog.csdnimg.cn/blog_migrate/2e1915573f94ea317c4ec424bfbb9618.png)
其中[x]表示不大于x的最大整数
6.6.5 二叉树的性质5
如果有一棵有n个结点的完全二叉树,对任一结点i,有:
1、如果i = 1,则结点i是二叉树的根,无双亲,如果 i > 1,则其双亲是结点[i / 2]2、如果2i > n, 则结点无左孩子(结点i为叶子结点)3、如果2i + 1 > n,则结点无右孩子;否则其右孩子是结点 2i + 1
6.7 二叉树的存储结构
6.7.1 二叉树的顺序存储结构
二叉树的顺序存储结构就是用一维数组存放二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲和孩子的关系,左右兄弟关系
![d01af95b65da04bfd6f95ddde6c7f1e0.png](https://i-blog.csdnimg.cn/blog_migrate/0d581af803938b7fb5272bda12dc6116.png)
将此完全二叉树的结点放入数组中有
![cf50da569abcdf19e11bcd545af25652.png](https://i-blog.csdnimg.cn/blog_migrate/c3fb1aad34f18967c19a81c7358716d9.png)
完全二叉树,由于其严格定义,所以用顺序结构也能表现出二叉树的结构。普通的二叉树,尽管层序不能反应逻辑结构,不过我们可以将其按照完全二叉树进行编号,不存在的结点我们使用“
![138cda11ecd136cfa55e9368d285cd6e.png](https://i-blog.csdnimg.cn/blog_migrate/566e6b906e337c0bc5ce2de1eca10a16.png)
考虑一种极端的情况,一棵深度为k的右斜树,虽然只有k个结点,却要分配
![5b5db460361fe4703a74956c3946d907.png](https://i-blog.csdnimg.cn/blog_migrate/78af63198643e7749565e91d4febf835.png)
所以顺序存储方式一般适用于完全二叉树
6.7.2 二叉链表
由于二叉树的一个结点最多有两个分支,所以我们为一个结点分配三个存储空间,一个数据域和两个指针域
![d6ab2ff223230996bf7601c17965bd0f.png](https://i-blog.csdnimg.cn/blog_migrate/344b2c40247e290cd5a0d253310cdd80.png)
这种链表我们称二叉链表
以下是结构代码和示意图
![22ee8e31cf46e2dba8a9c1c5dd2219dc.png](https://i-blog.csdnimg.cn/blog_migrate/5baa238f4f2c2ac5044b38a4206ad146.png)
![c45812ef5a6ab91baf6557196fcc2c4e.png](https://i-blog.csdnimg.cn/blog_migrate/c500ffa5007366e30ba6da70c833c3bd.png)
6.8 遍历二叉树
二叉树的遍历,是从根节点出发(是出发,不是必须从根结点进行访问),按照某种次序依次访问二叉树的所有结点,使每个结点仅且只能被访问一次
6.8.2 二叉树的遍历方法
1、前序遍历
二叉树若为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树
![f6016c4664871c871b31cdfdac6b2d35.png](https://i-blog.csdnimg.cn/blog_migrate/4a1739d4c4b67936c8c94553a48c9272.png)
2、中序遍历
若树为空,则返回空操作,否则从根结点开始(注意不是从根结点进行访问),先中序遍历左子树,再访问根结点,再中序遍历右子树
![f39518a126fe69cf01ecd8e32488de90.png](https://i-blog.csdnimg.cn/blog_migrate/1e6d4b75b16bce2e544ff5b9716b6c2b.png)
3、后序遍历
若树为空,则返回空操作,否则先从左到右先叶子后结点的方式遍历访问左右子树,最后再访问根结点
![12656b1103a6dd80e54f74a2f29391d6.png](https://i-blog.csdnimg.cn/blog_migrate/9e9973e7bca33eac952257058dcdd674.png)
4、层序遍历
若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按照左到右的顺序对结点进行访问
![26d478f492bbe88f9351eb662551382c.png](https://i-blog.csdnimg.cn/blog_migrate/8bea3ff8e39fa8794b138957ae08155a.png)
6.8.3 前序遍历算法
![deb856d8e0e269e37bad5391afaaed33.png](https://i-blog.csdnimg.cn/blog_migrate/fe7fa478478782659683d578a5ef4fda.jpeg)
6.8.4 中序遍历算法
![d39384eaafe6024942277762f2a3ef12.png](https://i-blog.csdnimg.cn/blog_migrate/a2cad3881f6b90051c77c173aba5edff.jpeg)
6.8.5 后序遍历算法
![eed9926368fc3c413448f6f4e12f323d.png](https://i-blog.csdnimg.cn/blog_migrate/72544f0469558d94a08751b4a9bf7c54.jpeg)
三种遍历算法都用到了递归,而且三种算法由于打印语句的位置不同,所以才会造成访问顺序的不同。
我们如果得知前序遍历顺序和中序遍历顺序,可以得到树的真实顺序
我们如果得知后序遍历顺序和中序遍历顺序,可以得到树的真实顺序
我们如果得知前序遍历顺序和后序遍历顺序,不可以得到树的真实顺序
6.9 二叉树的建立
这里我们使用python代码实现二叉树的类:
class BinaryTree(object):
def __init__(self, item):
self.key = item
self.leftChild = None #左孩子指针
self.rightChild = None #右孩子指针
def insertLeft(self, item):
node = BinaryTree(item)
if self.leftChild == None: #左结点不存在,直接插入
self.leftChild = node
else: #左结点存在,将左结点变成要插入结点的左结点,再将要插入的结点放在原先左结点的位置
node.leftChild = self.leftChild
self.leftChild = node
def insertRight(self, item):
node = BinaryTree(item)
if self.RightChild == None: #右结点不存在,直接插入
self.RightChild = node
else: #右结点存在,将右结点变成要插入结点的右结点,再将要插入的结点放在原先右结点的位置
node.RightChild = self.RightChild
self.RightChild = node
6.10 线索二叉树
6.10.1 线索二叉树的原理
对于二叉链表来说,存在很多空指针域,如果一个树有n个结点,则就会有2n个指针域,而n个结点有n-1个指针域,所以就会有2n - (n - 1) = n + 1个空指针域
![3bf4f272ee25ae43213202bda5e3551e.png](https://i-blog.csdnimg.cn/blog_migrate/87c683d7ed267095486ca74b1fdf382a.png)
线索二叉链表:我们把指向前驱和后继的指针称之为线索,加上线索的二叉链表称之为线索链表,相应的二叉树就是线索二叉树
我们将二叉树进行中序遍历之后,把rchild空指针改为指向该结点的后继结点
![cc4eb16aed2429633a898971c69dec55.png](https://i-blog.csdnimg.cn/blog_migrate/7944906d242dd317a878be361f76ac22.png)
我们将空的lchild指针改为指向前驱结点
![14cce5787dacc7e1ce87c144e9647f10.png](https://i-blog.csdnimg.cn/blog_migrate/1ca7afac6217d56c32434177fc044918.png)
其实线索二叉树等于是把一棵二叉树改为一个双向链表,对二叉树以某种次序遍历使其成为线索二叉树的过程称为线索化
![e084ebdfc1cf7d600ee69256003030c9.png](https://i-blog.csdnimg.cn/blog_migrate/20e55b02b9e8e206c92c162b1fb8f8cc.png)
但是这样很容易就会混淆指针指向的是孩子还是前驱/后继,所以我们为每个结点增加了两个区域ltag和rtag,这两个区域只能存放0和1,其占用的内存空间要小于指针变量
![51df5d31d5affc867e395ae661b6609a.png](https://i-blog.csdnimg.cn/blog_migrate/eab2b2c18698518d7b873b8217e4f0a1.png)
其中tag为0时,指向的时孩子,为1时,指向的时前驱/后继
![5ab0ee548642947adaef19d37c9231e8.png](https://i-blog.csdnimg.cn/blog_migrate/617a8852e69cf74db114d7e5d72c3cad.png)
6.10.2 线索二叉树的结构实现
class TreeNode(object):
def __init__(self, val = -1):
self.val = val #设置根结点初始值
self.left = None
self.right = None
#新增类型指针
#如果left_type == 0,表示指向左子树,如果是1,表示指向前驱节点
#如果right_type == 0,表示指向右子树,如果是1,表示指向后继节点
self.left_type = 0
self.right_type = 0
class ThreadBinary(object):
def __init__(self):
self.root = None
# 在递归进行线索化,总是保留前一个结点
self.pre = None # 为实现线索化,需要创建指向当前结点的前驱结点指针
#添加结点
def add(self, val):
node = TreeNode(val)
if self.root == None: #没有根结点的话,直接将要添加的结点当成根结点
self.root = node
return
queue = [self.node] #创建列表
while queue:
temp_node = queue.pop(0) #将列表中的第一个元素从列表中删除并取出,并将其设为临时结点
if temp_node.left == None: #如果临时结点没有左孩子,则将要插入的结点当成左孩子插入
temp_node.left = node
return
else: #否则将临时结点的左孩子加入到列表中,此时该孩子变成列表中的第一位元素,以便下一次循环
queue.append(temp_node.left)
if temp_node.right == None: #右孩子同样操作
temp_node.right = node
return
else: #此时该结点在列表中不是第一个
queue.append(temp_node.right)
#中序遍历
def in_order(self, node):
if node = None:
return
self.in_order(node.left)
print(node.val, '')
self.in_order(node.right)
#中序遍历线索化二叉树
def threaded_in_order(self, node):
if node == None: #结点为空,直接返回空操作
return
temp_node = node #将(根)结点设为临时结点,从(根)结点开始
while temp_node:
while temp_node.left_type == 0: #当该结点的左类型是0,即一直有左孩子时
temp_node = temp_node.left #将该结点的左孩子设为临时结点,直到最后结点没有左孩子,退出循环
print(temp_node.val, '') #打印没有左孩子的这个结点
while temp_node.right_type == 1: #再看该结点是否有右孩子
temp_node = temp_node.right #如果没有右孩子,则右指针指向的是后继结点,并将后继节点设为临时结点
print(temp_node.val, '') #打印后继结点
temp_node = temp_node.right #如果不是后继节点,则将该结点的右孩子设为临时变量
#直到左右孩子遍历完毕,前驱后继结点遍历完毕,临时结点为空,遍历结束
#二叉树进行中序线索化的方法
def threaded_node(self, node): #node是当前需要线索化的结点
if node = None:
return
self.threaded_node(node.left) #先线索化左子树
if node.left == None: #处理当前结点的前驱结点
node.left = self.pre #如果当前结点没有左孩子,则让其左指针指向前驱结点,并设置left_type= 1
node.left_type = 1
if self.pre and self.pre.right is None: #处理当前结点前驱结点的后继结点
self.pre.right = node #如果当前结点有前驱结点且前驱结点没有右孩子
self.pre.right_type = 1 #则让前驱结点的右指针指向当前结点,修改right_type = 1
self.pre = node #把当前结点设为下一个结点的前驱
self.threaded_node(node.right) #线索化右子树
以上代码转自 Python实现线索化二叉树
6.11 树、森林与二叉树的转换
6.11.1 树转换为二叉树
1、加线:在所有兄弟结点之间加一条连线
2、去线:对树中的每一个节点,只保留他与第一个孩子的连线
3:以根结点为中心,调整二叉树,保留的孩子结点为左孩子结点,兄弟结点右孩子节点
![5543cbdd65241d9a8e08c7af7232d5d5.png](https://i-blog.csdnimg.cn/blog_migrate/669666b3bb000e5ef2e364ed459c6ab1.png)
6.11.2 森林转换为二叉树
1、先将森林里的每一棵树转换为二叉树
2、第一棵二叉树不动,从第二棵二叉树开始,每一个二叉树的根结点作为前一棵二叉树根结点的右孩子,用线连接,得到二叉树
![cde561fc2e24f1c84e1af16ae5c5d327.png](https://i-blog.csdnimg.cn/blog_migrate/1bb7a4a4eb296a8390d2cb4029f3ed9a.jpeg)
6.11.3 二叉树转换为树
1、将左孩子的n个右孩子结点都作为此节点的孩子,将该结点与这些孩子进行连线
2、删除原二叉树中所有结点与其右孩子结点的连线
3、调整层次
![25bf94260b63842fcd761c30c0be204e.png](https://i-blog.csdnimg.cn/blog_migrate/ff90bed8ce6edec2767c315fcb87839e.png)
6.11.4 二叉树转换为森林
判断一棵二叉树可不可以转换为森林,就是看该二叉树的根结点有没有右孩子,有就是森林
1、从根结点开始,若有右孩子,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子在,则连线删除,直到所有右孩子连线都删除为止,得到分离二叉树
2、再将每棵分离的二叉树转换为树
![8b18ca9d6345ed516e56d0f77da4d57c.png](https://i-blog.csdnimg.cn/blog_migrate/ab4d2ce3cd9178000c412c1f4528402c.jpeg)
6.11.5 树与森林的遍历
树的遍历有两种方式
1、先根遍历,即先访问树的根结点,然后先根遍历每棵子树
2、后根遍历,依次先后根遍历每棵子树,然后再访问根节点
森林遍历也有两种方式:
1、前序遍历:即先访问第一棵树的根结点,然后先根遍历每棵子树,再依次同样的方法遍历剩余的树
2、后序遍历:即先访问第一棵树,先后根遍历每棵子树,然后再访问根节点,再依次同样的方法遍历剩余的树
6.12 赫夫曼树及其应用
6.12.2 赫夫曼树定义及其原理
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数就是路径长度。
树的路径长度就是树的根结点到每一个结点的路径长度之和
如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与节点上权的乘积,树的带权路径长度就是树中所有叶子结点的带权路径长度之和,我们把带权路径长度最小的二叉树称作赫夫曼树
如何构造赫夫曼树
![45c45a56853738e52799133b1915ce71.png](https://i-blog.csdnimg.cn/blog_migrate/ec8032de2251da7680eb6cc7b58e7a9e.jpeg)
![b8ed410b219d429a9493ded4e3da6036.png](https://i-blog.csdnimg.cn/blog_migrate/6445786d9597c7890f244e326bd9cb45.jpeg)
赫夫曼算法描述:
![453ee206bc7cb991d0a6cfd8d7bdacf3.png](https://i-blog.csdnimg.cn/blog_migrate/418f1c616c77e134996e0f7df1e516f7.jpeg)