1
数据结构——树
树的基本概念
由N个节点(N>=0)构成的集合
有一个特殊的节点,称为根节点,根节点没有前驱节点
除过根节点外,其余节点别分成M个(M>0)个互不相交的集合T1、T2...Tn,其中每一个集合又是一棵与树结构类似的子树
树是递归定义的
名词解释
结点
结点包括一个数据元素及若干指向其他子树的分支(指针(索引))
结点的度
结点所拥有的的子树的个数
度为0的结点
又称终端节点
分支结点
非终端结点
度不为零的结点
祖先结点
从根结点到该结点所经分支上的所有结点
子孙结点
以某结点为根结点的子树中的所有结点
双亲结点
树中某结点有孩子节点,该结点称为它孩子节点的双亲结点
孩子结点
树中一个结点的子树的根结点称为该结点的孩子结点
兄弟结点
具有相同双亲结点的结点
树的度
树中所有结点的度的最大值
结点的层次
从根结点到树中某结点所经路径上的分支数
树的深度
树中所有结点的层次的最大值
有序树
树中结点的各棵子树T0,T1...是有序的
无序树
树中结点的各棵子树之间的次序不重要,可以相互交换位置
森林
树m棵树的集合
树的表示方法
直观目录结构
集合文氏图
{ a, {b,{e},{f}}, {c}, {d,{g}} }
树的存储结构
双亲表示法
优点
缺点
寻找一个结点的双亲结点操作实现很方便
寻找一个结点的孩子结点很不方便
用指针表示出每个结点的双亲结点
孩子表示法
优点
缺点
寻找一个结点的孩子结点比较方便
寻找一个结点的双亲结点很不方便
用指针指出每个结点的孩子结点
双亲孩子表示法
用指针既表示出每个结点的双亲结点,也表示出每个结点的孩子结点
孩子兄弟表示法
表示出第一个结点的第一个孩子结点,也表示出每个结点的下一个兄弟结点
2
树之二叉树
概念
一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵分别称为左子树和右子树的二叉树组成
特点
每个结点最多有两棵子树
二叉树的子树有左右之分,其次序不能颠倒
满二叉树
所有分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上
完全二叉树
如果一棵具有N个结点的二叉树的结构与满二叉树的前N个节点 的结构相同,称为完全二叉树
二叉树的性质
若规定根节点的层次为1,则一棵非空二叉树第i层上最多有2^(i-1), i>=1个结点
若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数是2^k-1(k>=0)
对任何一棵二叉树,如果其叶结点个数为n0,度为2的非叶结点个数为n2,则有n0 = n2+1
具有n个结点的完全二叉树,如果按照从上至下从左到右的顺序对所有结点从0开始编号,则对于序号为i的结点有:
如果i>0,则序号为i的结点的双亲结点的序号为(i-1)/2,如果i==0,则序号为i的结点为根节点,无双亲结点
如果2i+1=n,则序号为i结点无右孩子结点
如果2i+2=n,则序号为i结点无右孩子结点
二叉树的存储结构
顺序存储
对一般二叉树尤其单支树,存储空间利用不理想
存储完全二叉树,简单省空间
链式存储
仿真指针(静态链表)
二叉树的基本操作
二叉树的创建
二叉树的遍历
前序
中序
后序
层序
3
二叉树的遍历
递归方法
preorder
inorder
postorder
res = list()def travel(cur_node){ if cur_node is None: return #res.append(cur_node.val) ---->前序遍历 travel(cur_node.left) #res.append(cur_node.val) ---->中序遍历 travel(cur_node.right) #res.append(cur_node.val) ---->后序遍历}
迭代方法
迭代方法只不过是递归形式的转化,本质就是将递归隐式的出入栈过程显式的表现出来。其思想就是:如果当前访问的节点(看成树或者子树的根节点),不应该在当前输出,那么就是将当前节点或者它的左孩子或右孩子入栈保存。
preorder
栈S; cur = root; while(cur || S不空){ 访问cur节点; if cur的右子树 not none: cur的右子树入栈S; if cur的左子树 not none: cur的左子树入栈S; cur = S栈顶弹出 #左孩子入栈之后会立马访问,因此可以不入栈直接进入下一个循环。 #于是可以改成下面的形式 } ################################################# 栈S; cur= root; while(cur || S不空){ while(cur){ 访问cur节点; cur的右子树入S; #右孩子为空也要直接入栈,否者访问最后一个节点之后,第二十行会对空栈进行pop操作 cur = cur的左子树 #省去了左孩子进栈出栈的冗余步骤 } cur = S栈顶弹出; }
inorder
栈S; cur= root; while(cur || S不空){ while(cur){ cur入栈S;#cur作为根节点入栈 cur = cur的左子树; } #直至到达左子树最左边的节点 cur = S栈顶弹出; 访问cur节点; cur = cur的右子树; }
postorder
栈S; cur = root; prev = None; #prev可以表征当前节点的前驱while(cur || S不空){ while(cur){ cur入栈S; cur = cur的左子树; } cur = S栈顶弹出; if cur的右孩子为空 or cur的右孩子等于prev:{ #第二个条件成立则表示右子树访问完成,cur节点是第二次出栈 访问cur节点; prev = root; root = None; #指示下一次循环的操作 }else{ cur入栈S; #树或子树的根节点第二次入栈 cur = cur.right; }}
还可以给每个节点进行标记,实现迭代的方法。参考leetcode(空间和时间都基本和递归没什么区别)
Morris方法
preorder
思路:(假设当前遍历的节点为cur)
如果cur无左孩子,则将__cur.val添加到输出数组__;再访问cur的右孩子节点,即cur=cur.right。
如果cur有左孩子,则令临时变量前驱节点predecessor=cur.left;然后一直更新predecessor为predecessor的右孩子,**最终predecessor为cur的左子树中序遍历的最后一个节点(即在中序遍历中,cur的前驱节点)。**然后根据predecessor的右孩子是否为空进行相应操作:
-
如果predecessor.right为空,**表示cur节点为第一次访问**。
-
-
将predecessor的右孩子指向cur;
并且将__cur.val添加到输出数组__。
访问cur的左子树(cur=cur.left)
-
-
如果predecessor.right非空,那么它一定是指向cur(表示当前cur已经是第二次访问了,且在第一次访问时将它指向cur。)
-
-
于是将predecessor.rigth重置为空。(还原原来的二叉树)
访问cur的右子树(cur=cur.right)
-
重复上述操作,直至当前访问节点cur为空。(表明所有节点访问完毕
class Solution: def preorderTraversal(self, root: TreeNode) -> List[int]: res = list() cur = root while cur: if cur.left is None: res.append(cur.val) cur = cur.right else: #找到cur的中序遍历的前驱节点 predessor = cur.left while predessor.right and predessor.right != cur : predessor = predessor.right #根据predessor.right的情况判定cur是第几次访问 if predessor.right is None: res.append(cur.val) predessor.right = cur cur = cur.left if predessor.right == cur: predessor.right = None cur = cur.right return res
(已经被垃圾的编辑排版给逼疯了。。。。后面就随它吧,还是博客方便)
inorder
思路:(假设当前遍历的节点为cur)
1. 如果cur无左孩子,则__将cur.val添加到输出数组__;再访问cur的右孩子节点,即cur=cur.right。1. 如果cur有左孩子,则令临时变量前驱节点predecessor=cur.left;然后一直更新predecessor为predecessor的右孩子,**最终predecessor为cur的左子树中序遍历的最后一个节点(即在中序遍历中,cur的前驱节点)。**然后根据predecessor的右孩子是否为空进行相应操作: 1. 如果predecessor.right为空,表示cur节点为第一次访问。 1. 将predecessor的右孩子指向cur; 1. 访问cur的左子树(cur=cur.left) 1. 如果predecessor.right非空,那么它一定是指向cur(表示当前cur已经是第二次访问了,且在第一次访问时将它指向cur。)**说明左子树已经遍历完了**。 1. 于是将predecessor.rigth重置为空。(还原原来的二叉树) 1. 并且__将cur.val添加到输出数组__。 1. 访问cur的右子树(cur=cur.right)1. 重复上述操作,直至当前访问节点cur为空。(表明所有节点访问完毕)
# Morrisclass Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: res = list() cur = root while cur: if cur.left is None: res.append(cur.val) cur = cur.right else: predecessor = cur.left while predecessor.right and predecessor.right != cur: predecessor = predecessor.right if predecessor.right is None: #cur为第一次访问 predecessor.right = cur cur = cur.left if predecessor.right == cur: predecessor.right = None res.append(cur.val) cur = cur.right return res
postorder
思路:(假设当前遍历的节点为cur)
1. 如果cur无左孩子,(**无结果输出**)访问cur的右孩子节点,即cur=cur.right。1. 如果cur有左孩子,则令临时变量前驱节点predecessor=cur.left;然后一直更新predecessor为predecessor的右孩子,**最终predecessor为cur的左子树中序遍历的最后一个节点(即在中序遍历中,cur的前驱节点)。**然后根据predecessor的右孩子是否为空进行相应操作: 1. 如果predecessor.right为空,表示cur节点为第一次访问。 1. 将predecessor的右孩子指向cur; 1. 访问cur的左子树(cur=cur.left) 1. 如果predecessor.right非空,那么它一定是指向cur(表示**当前cur已经是第二次访问了**,且在第一次访问时将它指向cur。) 1. 于是将predecessor.rigth重置为空。(还原原来的二叉树) 1. 倒序输出从cur节点的左子节点到该前驱节点predecessor这条路径上的所有节点。 1. 访问cur的右子树(cur=cur.right)1. 重复上述操作,直至当前访问节点cur为空。1. 倒序输出root到最右节点的路径
class Solution: def postorderTraversal(self, root:TreeNode)->List[int]: def addpath(node:TreeNode)->List[int]: path = [] while node: path.append(node.val) node = node.right return path[::-1] res, cur = [], root while cur: if cur.left is None: cur = cur.right else: predcessor = cur.left while predcessor.right and predcessor.right != cur: predcessor = predcessor.right if predcessor.right is None: predcessor.right = cur cur = cur.left if predcessor.right == cur: predcessor.right = None res.extend(addpath(cur.left)) cur = cur.right res.extend(addpath(root)) return res
![17a28cad7e4aeece19e4b335ecc43362.png](https://i-blog.csdnimg.cn/blog_migrate/fea4a0779d4fabd2240a303d62494556.png)
End