把二叉树的学习历程记录一下。
遍历二叉树
如上图所示,二叉树是由父节点与子节点组成的,每个父节点最多有一左一右两个子节点,子节点不能脱离父节点单独存在。
前序遍历:前序遍历先访问根节点,再遍历左子树,最后遍历右子树。简记为根->左->右
。上图中的二叉树根据前序遍历得到的结果为:
F、B、A、D、C、E、G、I、H
从遍历的过程中可以直观地看到,前序遍历和其名称一样,在左右两侧分别严格进行从上至下的遍历。
中序遍历:中序遍历先遍历左子树,再访问根节点,最后遍历右子树。简记为左->根->右
。上图中的二叉树根据中序遍历得到的结果为:
A、B、C、D、E、F、G、H、I
从遍历的过程中可以看到,中序遍历的形式接近于将整个二叉树进行从左至右一层层地遍历。
后序遍历:后序遍历先遍历左子树,再遍历右子树,最后访问根节点。简记为左->右->根
。这里可以看出无论是前序,中序还是后续遍历,左子树一定是优先于右子树的。上图中的二叉树根据后序遍历得到的结果为:
A、C、E、D、B、H、I、G、F
从遍历的过程中可以看到,后序遍历的形式接近于在左右两个子树中从后向前遍历。
在程序中,可以使用递归和迭代两种方式来实现二叉树遍历。
递归
由于二叉树中相连接的节点互为亲子代,因此二叉树是个天然适合递归的数据结构,实际上在代码中二叉树的递归也比迭代写起来更方便。
以较为简单的前序遍历为例。在算法书中,递归函数需要有三个组成部分:
递归函数的返回值与参数,递归结束的终止条件,单次递归中执行的功能。
这里以python3举例,构建一个前序遍历二叉树的递归函数。
递归函数的返回值与参数:
由于我们是将遍历的结果存放在列表里面的,每次递归只是将遍历到的节点存放到列表之中,因此该函数不需要返回值。至于函数的参数,由于我们要遍历二叉树,因此肯定是要将二叉树的根节点传进来的,因此参数就应当是一个二叉树的节点形式。
def preorder(root:TreeNode):
递归结束的终止条件:
递归是按照一定的逻辑进行不断操作的,因此我们需要为其增加一个终止条件。显然递归的过程中,当前节点若是不存在,就应该跳出该层递归。
if root == None:
return
单次递归中执行的功能:
每一次递归中需要按照顺序执行前向递归的逻辑。
res.append(root.val)
inorder(root.left)
inorder(root.right)
这样前序遍历的递归函数就写好了:
def preorder(root:TreeNode):
if root == None:
return
res.append(root.val)
inorder(root.left)
inorder(root.right)
同理,中序遍历的递归函数:
def inorder(root:TreeNode):
if root == None:
return
inorder(root.left)
res.append(root.val)
inorder(root.right)
后序遍历的递归函数:
def postorder(root:TreeNode):
if root == None:
return
postorder(root.left)
postorder(root.right)
res.append(root.val)
遍历法之所以很简洁,因为其隐式地维护了一个栈,所以代码量比较少。
但是,遍历法的代码太定式了,有点偷懒,因此还需要学习迭代法。
迭代法
迭代法的本质是将递归法中的栈显式地人工写出来,因此代码量明显增加。
首先以前序遍历为例子。前序遍历的逻辑是存放根节点,遍历左子树,再遍历右子树。按照这样的逻辑,我们应当在栈中先放入根节点的右子节点,再放入根节点的左子节点,如此,在出栈时,先弹出左子节点(先进先出),再弹出右子节点,顺序放入记录用的列表即可。
完整代码如下所示:
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = list() # 存结果的列表
stack = [root] # 栈,先将根节点放入
if root == None: # 如果二叉树是空的就直接返回
return res
while stack: # 如果栈内一直有节点就循环
node = stack.pop() # 将栈顶的节点取出
if node != None:
res.append(node.val) # 将栈顶节点的值记录到列表中
if node.right: # 如果栈顶节点有右子树
stack.append(node.right) # 将栈顶节点的右子节点放入栈
if node.left: # 如果栈顶节点有左子树
stack.append(node.left) # 将栈顶节点的左子节点放入栈
return res
在常规迭代算法的基础上衍生了模板流迭代算法。
模板流的思路与常规迭代的不同,模板流的思路为,从根节点开始,遍历所有的孩子,并将它们都存放在栈中。
随后将栈内节点依次弹出,每弹出一个节点,就将该节点的右子节点视作当前节点,并继续上面的操作。
模板的前序遍历完整代码如下:
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = list() # 存放结果的列表
stack = list() # 栈
cur = root # 当前节点,初始化为根节点
if root == None: # 如果根节点为空直接返回
return res
while stack or cur: # 如果栈内有节点或者当前节点存在,继续循环,注意如果不考虑当前节点存在,将无法开始循环(初始栈为空)
while cur: # 对于每个当前节点,都需要执行该循环
res.append(cur.val) # 将当前节点的值存入列表
stack.append(cur) # 将当前节点入栈
cur = cur.left # 当前节点不断在左子树遍历
temp = stack.pop() # 当前节点的左子树遍历完成后,将栈顶节点取出
cur = temp.right # 将栈顶节点的右子节点当做当前节点
return res
迭代法还可以用于层序遍历,层序遍历是将二叉树按照每层的形式进行记录,属于广度优先搜索。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
res = list() # 存放结果的列表
if root == None:
return res
q = [root] # 队列,先存放根节点
while q: # 如果队列中存在节点
n = len(q) # 当前队列中的节点数量
level = [] # 存放每层节点的列表
for i in range(n): # 当前队列中的每个节点都执行一次循环体内的内容
node = q.pop(0) # 队列形式,先进先出,取队列中节点出来
level.append(node.val) # 将当前节点记录到该层里
if node.left: # 如果该节点还有左孩子,就将左孩子放入队列中,因为队列先进先出,所以这一层不会包含该左孩子
q.append(node.left)
if node.right: # 如果该节点还有右孩子,就将右孩子放入队列中,因为队列先进先出,所以这一层不会包含该右孩子
q.append(node.right)
res.append(level) # 将该层列表加入记录列表中
return res