这几节内容,概念少,全靠代码撑住场面。多看代码,才能学习到二叉树的基本知识和应用。
二叉树的基本运算
所谓的基本运算主要包括:括号表示法的树与二叉链存储结构的树互相转换,树节点查找,求树高度。
1. 树节点查找:
最基本的操作了:
class TreeNode: # 树节点的定义
def __init__(self, val):
self.data = val
self.left = None
self.right = None
def findNode(node, val):
if not node:
return None
if node.data == val:
return node
if node.left:
res = findNode(node.left, val)
if res:
return res
if node.right:
res = findNode(node.right, val)
if res:
return res
return None
2. 求树的高度
def getHeight(treeHead):
if not treeHead:
return 0
if not treeHead.left and not treeHead.right:
return 1
height, h = 0, 0
if treeHead.left:
h = getHeight(treeHead.left)
height = max(h, height)
if treeHead.right:
h = getHeight(treeHead.right)
height = max(h, height)
return height + 1
3. 与括号表示法的互相转换
在教材中称为树的创建及输出,其实就是两种表示方法转换,二叉链结构的树是标准的存储结构,至于括号表示法,主要是由于方便输出(是一个字符串)。从这一基本操作可以领会二叉树各种运算的基本特点,难度还是不小的。
代码如下,二叉树的创建,应用了栈,不熟悉的回前面再看一下;二叉树的输出,应用递归方法。栈、队列、递归,是二叉树中运算的法宝。
tree_str = "A(B(E,T(W,C)),D(,F(P(S(I),V))))" # 我们定义了一颗蛮复杂的树
print(tree_str)
def createTree(tree_str):
if not tree_str:
return None
p = 0 # 当前节点
t = 0 # 标记符,表示后面的数值是左子树还是右子树
stack = []
head = None
for char in tree_str: # 字符串中一共四类符号,分别进行处理
# print(char)
if char == "(": # 遇左括号,当前节点入栈
stack.append(p)
t = 1
elif char == ")": # 遇右括号,当前节点出栈
stack.pop(-1)
elif char == ",":
t = 2
else: # 此时构建当前节点,并根据标记符处理
p = TreeNode(char)
if not head:
head = p
else:
if t == 1:
stack[-1].left = p
else:
stack[-1].right = p
return head
h = createTree(tree_str)
print(h)
def disTree(head):
if not head:
return ""
tree_dis = ""
tree_dis += head.data
if head.left or head.right:
tree_dis += "("
tree_dis += disTree(head.left) # 递归解析左子树
if head.right:
tree_dis += ","
tree_dis += disTree(head.right)
tree_dis += ")"
return tree_dis
s = disTree(h)
print(s)
print(s==tree_str) # 这两个值肯定是一样的
思考:用顺序存储法表示的树,上述问题如何解决?(通过顺序表示法节点标号的性质,十分简单)
二叉树的遍历
二叉树的遍历是指按照一定次序访问树中所有节点,并且每个节点仅被访问一次的过程。遍历是二叉树最基本的运算,是二叉树中其他运算的基础。参考文章 二叉树及其三种遍历
在遍历时根据访问根节点及遍历子树的先后关系,对于非空二叉树,我们有3种遍历方法:先序遍历(PreOrder)、中序遍历(InOrder)、后序遍历(PostOrder),根据访问根节点的次序划分,这3类遍历方法我们也称之为深度优先遍历(Depth First Search),因为遍历时沿着树的深度遍历树的节点,尽可能深的搜索树的分支。
此外,还有一种层序遍历(LevelOrder),我们称之为广度优先遍历(Breadth First Search),从根结点开始沿着树的宽度搜索遍历。
下面我们先看一下深度优先遍历的递归写法,只是调整访问根节点语句的顺序即可,十分简单:
def SearchTree(head):
if not head:
return None
print(head.data) # 先序
SearchTree(head.left)
# print(head.data) # 中序
SearchTree(head.right)
# print(head.data) # 后序
深度优先的非递归写法是本节需要掌握的重点,算法的核心就是使用栈(stack),可见之前线性结构的知识是重要基础。
三种遍历非递归方法的代码就没有递归的那么一致了,我们要分别进行解析(经典数据结构教材中有更详细的解析,不理解的认真学习教材):
1)先序遍历
先序遍历是最基础的深度优先的遍历方式,代码也是最简单的,访问根节点后,有右压右,有左压左。由于栈后进先出,右子树先进栈。应当注意 前面括号表示法如 "A(B(E,T(W,C)),D(,F(P(S(I),V))))" 中字母的顺序即为前序遍历的顺序,可见前序遍历是比较符合正常思维的表达方式。
def PreOrder(head):
if not head:
return None
stack = []
stack.append(head) # 根节点进栈
while stack: # 栈不空时循环
p = stack.pop(-1)
print(p.data)
if p.right: # 右结点先进栈
stack.append(p.right)
if p.left: # 左结点进栈
stack.append(p.left)
2)中序遍历
从一棵二叉树的最左下结点开始,根结点到左下结点一一进栈;出栈一个结点时访问他(每一个出栈的结点或者无左结点或者左结点已访问,此时才可访问),再处理右子树,即该结点的左结点依次进栈......
def InOrder(head):
if not head:
return None
stack = []
p = head
while p or stack: # 循环条件
while p: # p的所有左结点进栈
stack.append(p)
p = p.left
if stack: # 栈顶元素 没有左结点或左结点已访问
p = stack.pop(-1)
print(p.data) # 访问该结点
p = p.right # 访问后该结点的右子树进栈
3)后序遍历
不起眼的后序遍历是最复杂的遍历,与中序遍历相似的是,先访问左子树,从最左下的结点开始。由于我们要首先访问该结点的左右子树后才访问该结点,所以我们需要一个标记,表示左右子树是否访问过。此外,我们还需要一个变量pre保存刚刚访问过的结点,此时分三种情况 pre是当前的父节点,则此时在向下遍历;pre是当前的左子树,说明我们在从左到右遍历;最后就是其他情况,pre等于当前,表示上回合未将任何节点入栈,此次轮到它自身出栈,或是pre是当前的右节点,那么也轮到它自身出栈。
def postorderTraversal(root):
if not root:
return
stack = [root]
res = []
pre_node = None
while stack:
p = stack[-1]
if not pre_node or pre_node.left == p or pre_node.right == p:
# 在向下遍历
if p.left:
stack.append(p.left)
elif p.right:
stack.append(p.right)
elif pre_node == p.left: # 已遍历左子树
if p.right:
stack.append(p.right)
else: # leaf叶子节点时 pre==p / 左右都遍历的,轮到父节点了 pre==p.right
res.append(p.val)
stack.pop()
pre_node = p
return res
上述写法看起来很麻烦,网上看到了其他的写法,参考 二叉树先序、中序、后序、层次遍历的非递归实现(python),通过两个栈倒腾一下,实现了后序遍历,十分优秀。
实际上,后序遍历的顺序是左-右-根,在stack里我们将树的结点按根-右-左的顺序放入stack2内,然后stack2依次出栈访问结点内容,就形成了左-右-根的后序遍历方式。
def PostOrder2(root):
stack = [root]
stack2 = []
result = []
while not stack:
root = stack.pop()
stack2.append(root)
if root.left:
stack.append(root.left)
if root.right:
stack.append(root.right)
while not stack:
node = stack2.pop()
result.append(node.data)
return result
然后我们看一下层次遍历的方法:
二叉树的层次遍历即从上往下、从左至右依次打印树的节点。 先访问的结点其左、右子结点也要先访问,很符合队列的操作原则。其思路就是将二叉树的节点加入队列,出队时将其非空左右孩子依次入队,到队列为空即完成遍历。这是队列的重要应用。需要说明的是,教材上使用的是环形队列,充分利用空间;此外用栈也可解决。
一年前的博客 2.3 python数据结构之队列——应用,简单的两个队列的应用栗子,在python中队列可用deque模块,详细使用可参考 python3 deque(双向队列),主要函数是 append/appendleft, extend/extendleft, pop/popleft, index, insert, reverse, remove;当然,用list也可实现队列的功能,append入队,pop(0)出队,只是pop(0)的时间复杂度是O(n)的中看不中用。
下面我们看用队列实现的二叉树层次遍历:
from collections import deque
def levelOrder(head):
if not head:
return None
queue = deque()
queue.append(head)
while queue:
node = queue.popleft()
print(node.data)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
如何按层输出呢?很简单,按层遍历,下一层的结点归入另一个队列里即可,道理类似杨辉三角那道题。
def levelOrder(root: TreeNode) -> List[List[int]]:
if not root:
return []
res = []
queue = [root]
while queue:
next_level = []
vals = []
while queue:
p = queue.pop(0)
vals.append(p.val)
if p.left:
next_level.append(p.left)
if p.right:
next_level.append(p.right)
res.append(vals)
queue = next_level
return res
最后我们提出几个问题总结一下这一节:
我们在熟悉二叉树的基本运算特点之上,重点掌握二叉树的遍历方式,深度优先级及广度优先的遍历分别用于解决什么类型的问题? 在深度优先遍历中, 前序遍历、中序遍历及后序遍历分别提供了什么样的信息,分别解决什么问题?
同时,需要强调的一点是,二叉树的遍历,分为结果和过程两类,以后序遍历为例,想要后序遍历的结果,可以用方法二,如果是一道强调过程的题目,那么还是老老实实用方法一。
那么何为强调过程呢?遍历过程中除了包含结点的访问顺序,还有访问该结点时该结点栈中元素等“半路”信息,对于后序遍历,这种信息十分重要(后序遍历是二叉树路径问题的关键),要学会方法一;方法二中stack2只剩下后续遍历的结点的顺序了,不含有任何中间过程。
我们看下一节中更多二叉树的问题来寻找答案。