文章目录
树的类型定义
查找类(引用型操作):
- 求根节点
- 求当前结点元素
- 求双亲
- 求最左孩子
- 求右兄弟
- 判空
- 求深度
- 遍历
插入类:
- 初始化空树
- 按定义构造树
- 给当前结点赋值
- 将以e为根的树插入结点为p的第i棵子树
删除类:
- 清空树,结构还在
- 销毁树的结构
- 删除结点p的第i棵子树
有向树:
- 有确定的根
- 树根和子树根之间为有向关系
- 有序树:子树之间存在确定的次序关系。
- 无序树:子树之间不存在确定的次序关系。
二叉树
二叉树的定义
概念:二叉树或为空树;或是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成
这里约定根结点所在层数为第一层(有的书规定为第0层)
树的深度:树的最大层数。
树的高度:树的最大层数。
(注:其实深度和高度的定义并不统一,详细的看这里 )
度:拥有的孩子数。
双亲结点:就是父节点(如果有,就一个,不要被字面意思迷惑)。
二叉树:度不大于2的有序树。
二叉树的五种基本形态:
- 空树
- 只含根节点
- 右子树为空树
- 左子树为空
- 左右子树都不为空
二叉树的重要特性
两类特殊的二叉树
满二叉树:深度为k 且含有 2^k-1个结点的二叉树。
完全二叉树:
树中所含的n 个结点和满二叉树中编号为1 至n 的结点一一对应。
完全二叉树(高度为k )的性质:
- 完全二叉树的前k−1 层为满二叉树;
- 完全二叉树的叶子结点可能出现在k−1 层和k 层;
二叉树的遍历
问题的提出
顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次。
二叉树是非线性存储结构,每个结点有两个后继,则存在如何遍历,即按什么样的搜索路径进行遍历的问题。
对于二叉树而言,可以有三种搜索路径:
- 先上后下的按层次遍历;
- 先左(子树)后右(子树)的遍历;
- 先右(子树)后左(子树)的遍历。
先左后右的遍历算法
- 先序遍历
- 中序遍历
- 后序遍历
其中,先、中、后指的是根的访问顺序
先(根)序的遍历算法:遇到根就访问,然后进入左子树
中(根)序的遍历算法:遇到根先不访问,先访问左子树,等到左子树访问回来,再访问根
后(根)序的遍历算法:遇到根先不访问,先访问左子树,等到左子树访问回来,再访问右子树,最后再访问根
无论是先序、中序、后序,路径都是一样的。都是从双亲下来去根,从根去左,再回来,从根去右,再回来。即下图黄色线,整个搜索路径会有三次经过根:
先序遍历算法:
若二叉树为空树,则空操作;否则:
- 访问根结点
- 先序遍历左子树
- 先序遍历右子树
对于上图,无论是先序、中序、后序,路径都是一样的。整个搜索路径会有三次经过根:第一次是红色线,第二次是绿色线,第三次是深蓝色线。
不同的是先序遍历在第一次经过根的时候就访问了(红色线时访问,相对应的,结点用红色表示)。
访问顺序:A B C D E F G H K
中序遍历算法:
若二叉树为空树,则空操作;否则:
- 中序遍历左子树
- 访问根结点
- 中序遍历右子树
中序遍历在第二次经过根(从左子树回来)的时候访问(绿色线时访问,相对应的,结点用绿色表示)。
访问顺序:B D C A E H G K F
后序遍历算法:
若二叉树为空树,则空操作;否则:
4. 后序遍历左子树
5. 后序遍历右子树
6. 访问根结点
后序遍历在第三次经过根(从左子树回来)的时候访问(深蓝色线时访问,相对应的,结点用深蓝色表示)。
访问顺序:D C B H K G F E A
算法的递归和非递归版本
先序遍历
1.递归版
#前序遍历
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root == None:
return []
res = []
res.append(root.val)
res += self.preorderTraversal(root.left)
res += self.preorderTraversal(root.right)
return res
非递归
先序遍历非递归算法:
- 由先序遍历过程可知,先访问根结点,再访问左子树,最后访问右子树。
- 因此,先将根结点进栈,在栈不为空时循环;出栈p,访问*p结点,将其右孩子结点进栈,再将左孩子结点进栈。
#非递归实现前序遍历
def preOrderNonRec(self,root):
if root == None:
return
#用数组当栈
stack = []
node=root
#res=[]
while node or stack:
while node:
# 从根节点开始,一直找它的左子树
print(node.data)
#将右子树压入栈中
#res.append(node.data)
stack.append(node)
#一直往下找左子树
node = node.left
# while结束表示当前节点Node为空,即前一个节点没有左子树了
# 栈中开始弹出上右子树,再重复前面步骤
node = stack.pop()
node=node.right
#return res
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None:
return []
res = [] # LeetCode的要求非打印,而是返回一list,因而又准备了一个空list:res
stack = [] # 思路中提及的“辅助栈”
stack.append(root)
while stack:
cur = stack.pop()
res.append(cur.val)
if cur.right:
stack.append(cur.right)
if cur.left:
stack.append(cur.left)
return res
中序遍历
递归版
#中序遍历
def inorderTraversal(self, root):
if root ==None:
return[]
res=[]
res+=self.inorderTraversal(root.left)
res.append(root.val)
res+=self.inorderTraversal(root.right)
return res
非递归
考虑中序遍历:第一次经过根没有访问,对其左子树访问完,第二次经过根时,才访问根,需要一个栈的结构来保存结点的地址。凡是递归定义的数据结构,想用非递归的方法实现,需要用户自己定义栈!
中序遍历非递归算法:
- 由中序遍历过程可知,采用一共栈保存需要返回的结点指针,先扫描(并非访问)根结点的所有左结点并将它们一一进栈。
- 然后出栈一个结点,显然该结点没有左孩子结点或者左孩子结点已访问过,则访问它。
- 然后扫描该机欸但的右孩子结点,将其进栈,再扫描该右孩子结点的所有左结点并一一进栈,如此这样,直到栈空为止。
def preOrderNonRec(self,root):
if root == None:
return
#用数组当栈
stack = []
node=root
# res=[]
while node or stack:
while node:
# 从根节点开始,一直找它的左子树
#将父节点压入栈中
stack.append(node)
#一直往下找左子树
node = node.left
# while结束表示当前节点Node为空,即前一个节点没有左子树了
# 栈中开始弹出上右子树,再重复前面步骤
node = stack.pop()
#res.append(node.data)
print(node.data)
node=node.right
#return res
def preorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None:
return []
res = [] # LeetCode的要求非打印,而是返回一list,因而又准备了一个空list:res
stack = [] # 思路中提及的“辅助栈”
stack.append(root)
while stack:
cur = stack.pop()
res.append(cur.val)
if cur.right:
stack.append(cur.right)
if cur.left:
stack.append(cur.left)
return res
后续遍历
递归版
def postorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root == None:
return []
res=[]
res+=self.postorderTraversal(root.left)
res+=self.postorderTraversal(root.right)
res.append(root.val)
return res
非递归
def post_order_stack(self, root): # 堆栈实现后序遍历(非递归)
# 先遍历根节点,再遍历右子树,最后是左子树,
#这样就可以转化为和先序遍历一个类型了,最后只把遍历结果逆序输出就OK了。
if not root:
return
myStack1 = []
myStack2 = []
node = root
while myStack1 or node:
while node:
myStack2.append(node)
myStack1.append(node)
node = node.right
node = myStack1.pop()
node = node.left
while myStack2:
print (myStack2.pop().data)
def postorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if root is None:
return []
res = [] # 栈2
stack = [] # 栈1
stack.append(root) # 当前节点“中”最先压栈2
while stack:
cur = stack.pop()
res.append(cur)
# 当前节点的孩子节点按“左右”次序压栈
#1。最终按“先右后左”顺序弹栈并压进栈
#2,致使栈2从上到下的顺序为“左、右、中”,这也正是后序遍历打印顺序
if cur.left:
stack.append(cur.left)
if cur.right:
stack.append(cur.right)
final = [] # 返回答案用的空list
while res:
final.append(res.pop().val)
return final
构造二叉树
二叉树的先序和中序序列建树
给先序和后序不能建树,为什么?先序是根左右,中序是左根右。所以先序是得到根,然后代入中序,得到左右划分,进而确定左右子树的根。
跟后序和中序来建树的道理是一样的。
因此,给先序和后序只能确定根在哪,无法确定左和右的划分,不能唯一的确定二叉树。
例子:仅知道二叉树的先序序列"abcdefg"不能唯一确定一棵二叉树,如果同时已知二叉树的中序序列"cbdaegf",则会如何?
通过二叉树的先序序列确定根是谁,把根代入中序序列得到左右划分,然后回到先序序列的左子树(右子树)确定谁是根,再将根代入中序序列得到左子树(右子树)的左右划分。
如上图,在先序序列先确定根结点a,然后代入中序序列得到左右划分cbd和egf
继续划分,找到根b,然后根c,c左子树为空,然后右子树为空。返回上一层b,找右子树d,左子树为空,然后右子树为空,再放回上一层a,a的右子树为空,再返回,到达根a。
def tree_by_pre_mid(self, pre, mid):
if len(pre) != len(mid) or len(pre) == 0 or len(mid) == 0:
return
T = NodeTree(pre[0])
index = mid.index(pre[0])
T.lchild = self.tree_by_pre_mid(pre[1:index + 1], mid[:index])
T.rchild = self.tree_by_pre_mid(pre[index + 1:], mid[index + 1:])
return T
中序和后序
def tree_by_post_mid(self, post, mid):
if len(post) != len(mid) or len(post) == 0 or len(mid) == 0:
return
T = NodeTree(post[-1])
index = mid.index(post[-1])
T.lchild = self.tree_by_post_mid(post[:index], mid[:index])
T.rchild = self.tree_by_post_mid(post[index:-1], mid[index + 1:])
return T
算法的应用
树的声明
class Node:
def __init__(self,data,left=None,right=None):
self.data=data
self.left=left
self.right=right
统计树的节点
def countNode(self,t):
if t is None:
return 0
else:
return 1+self.countNode(t.left)+self.countNode(t.right)
树的深度
def TreeDepth(self, pRoot):
# write code here
if pRoot == None:
return 0
lDepth = self.TreeDepth(pRoot.left)
rDepth = self.TreeDepth(pRoot.right)
return max(lDepth,rDepth)+1
复制二叉树(后序遍历)
采用后序遍历:先复制左子树,后复制右子树,最后确定根。
生成一个二叉树的结点(其数据域为item,左指针域为lptr,右指针域为rptr)
# 对二叉树进行拷贝
# 先对分别对左右子树进行拷贝
# 再把其作为新的树的左右子树
class BiTNode:
def __init__(self):
self.data = None
self.lchild = None
self.rchild = None
def createDupTree(root):
if root == None:
return None
# 二叉树根结点
dupTree = BiTNode()
dupTree.data = root.data
# 复制左子树
dupTree.lchild = createDupTree(root.lchild)
# 复制右子树
dupTree.rchild = createDupTree(root.rchild)
按层次遍历二叉树
-
由层次遍历的定义可知,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。
-
因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树根结点开始,先将根结点指针入队列,然后从队头取出一个元素,每取一个元素,执行下面两个操作
-
访问该元素所指结点
-
若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子指针和右孩子指针顺序入队。
-
此过程不断进行,当队列为空时,二叉树的层次遍历结束。
class Solution:
# 返回从上到下每个节点值列表,例:[1,2,3]
def PrintFromTopToBottom(self, root):
# write code here
res=[]
queue=[root]
while queue and root:
node=queue.pop(0)
res.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return res
小结:
- 若先序序列与后序序列相同,则或为空树,或为只有根结点的二叉树。
- 若中序序列与后序序列相同,则或为空树,或为任一结点至多只有左子树的二叉树。
- 若先序序列和中序序列相同,则或为空树,或为任一结点至多只有右子树的二叉树。
- 若中序序列与层次序列相同,则或为空树,或为任一结点至多只有右子树的二叉树