数据结构之二叉树——遍历(递归与非递归)

定义

二叉树,一个有穷的结点集合。这个集合能够为空,若是不为空,则它是由根结点和称其为左子树右子树的两个不相交的二叉树组成。node

二叉树有五种基本形态:python

顺序存储

彻底二叉树可使用顺序存储结构,按从上至下,从左到右顺序存储,若是一颗彻底二叉树以下:算法

那么顺序存储能够为:

由上面的结构,能够获得,n个结点的彻底二叉树的结点父子关系:

  • 非根结点(序号i>1)的父节点的序号为⌊i/2⌋
  • 结点(序号为i)的左孩子结点的序号为2i(若是2i>n,则没有左孩子)
  • 结点(序号为i)的右孩子结点的序号为2i+1(若是2i+1>n,则没有右孩子)

通常二叉树也可使用顺序存储,只是会形成空间的浪费。数组

链式存储

因为通常的二叉树使用顺序存储结构,容易形成空间的浪费,所以可使用链式存储。其结构以下app

class TreeNode:
    def __init__(self, x, left=None, right=None):
        self.val = x  # 值
        self.left = left  # 左孩子
        self.right = right  # 右孩子
复制代码

遍历

因为二叉树不是线性结构,所以它的遍历也就不像数组或者链表那么简单,它须要沿着某条搜索路线,依次对树中每一个结点均作一次且仅作一次访问。post

前序遍历

前序遍历的遍历过程:spa

  1. 访问根结点
  2. 先序遍历其左子树
  3. 先序遍历其右子树

使用递归的方式实现以下:3d

def pre_order_traversal(root: TreeNode):
    if root:
        print(root.val)
        pre_order_traversal(root.left)
        pre_order_traversal(root.right)
复制代码

该树的前序遍历为:A B D F E C G H I

中序遍历

中序遍历的遍历过程:code

  1. 中序遍历其左子树
  2. 访问根结点
  3. 中序遍历其右子树

递归代码cdn

def in_order_traversal(root: TreeNode):
    if root:
        in_order_traversal(root.left)
        print(root.val)
        in_order_traversal(root.right)
复制代码

该树的中序序遍历为:D B E F A G H C I

后序遍历

后序遍历的遍历过程:

  1. 后序遍历其左子树
  2. 后序遍历其右子树
  3. 访问根结点

递归代码

def post_order_traversal(root: TreeNode):
    if root:
        post_order_traversal(root.left)
        post_order_traversal(root.right)
        print(root.val)
复制代码

该树的后序序遍历为:D E F B H G I C A

遍历的思考

从上面三幅图的访问路径,能够看出,不管是前,中,后序遍历,在遍历过程当中通过结点的路线都是同样的,只是访问各结点的时机不一样

下图使用ⓧ,☆,△三种符号分别标记出,前序,中序,后序访问各结点的时刻

非递归中序遍历

使用递归的方式能够比较容易的写出遍历算法,那么若是不用递归呢?

咱们知道,递归的实现须要借助栈,那么能够用栈+循环的方式来实现遍历算法。

以中序遍历为列:

  • 遇到一个结点,由于不知道是否还有左子树,所以将它压栈,并去遍历它的左子树
  • 当它的左子树遍历完了,从栈顶弹出这个结点并访问它
  • 而后转向这个结点的右子树继续遍历,至关于又回到第一步,直到遍历完整棵树

仍然以上面的二叉树为例,来看看非递归的中序遍历的运行过程:

  1. 结点A,压栈
  2. A有左结点B,B压栈
  3. B有左结点D,D压栈
  4. 结点D,没有左结点,则弹出栈顶元素D,并访问
  5. 转向D的右子树,可是D没有右子树,所以继续弹出栈顶元素B并访问,
  6. 转向B的右结点F,F压栈
  7. F有左结点E,E压栈
  8. E没有左结点,弹出栈顶元素E并访问
  9. 转向E的右结点,没有,继续弹出F
  10. 转向F的右结点,没有,继续弹出A
  11. 转向A的右结点C,C压栈
  12. C有左结点G,G压栈
  13. G没有左结点,弹出G并访问
  14. 转向G的右结点H,H压栈
  15. H没有左结点,弹出H并访问
  16. H没有右结点,继续弹出,弹出C访问
  17. 转向C的右结点I,I压栈
  18. I没有左结点,弹出I访问
  19. 转向I的右结点,没有,栈也为空,遍历结束
def in_order_traversal(root: TreeNode):
    stack = []
    while root or stack:
        while root:
            stack.append(root)
            root = root.left
        if stack:
            node = stack.pop()
            print(node.val)
            root = node.right
复制代码

非递归前序遍历

中序遍历是在第二次通过结点的时候,才访问该结点的,所以参照中序遍历的非递归算法,把print语句移到第一次通过结点时,就访问该结点,那么非递归前序遍历的实现也就出来了。

def pre_order_traversal(root: TreeNode):
    stack = []
    while root or stack:
        while root:
            stack.append(root)
            print(node.val)
            root = root.left
        if stack:
            node = stack.pop()
            root = node.right
复制代码

非递归后序遍历

非递归后序遍历比较复杂,并且实现的方式也有多种,这里提供一个比较好理解的标记法。根据后序遍历的定义,要先访问完左子树,再访问完右子树,最后才访问根结点,那么仍是套以前的代码结构,可是作个标记,在弹出元素的时候,判断是否有右结点或者右结点是否被访问过,若是知足则访问该结点,不知足就将它再次压回栈中,并转向它的右结点

def post_order_traversal(root: TreeNode):
    stack = []
    visited_node = None # 前一个被访问的结点
    while root or stack:
        while root:
            stack.append(root)
            root = root.left
        if stack:
            node = stack.pop()
            if not node.right or node.right == visited_node:
                # 没有右孩子或者右孩子已经被访问了,才访问该结点
                print(node.val)
                visited_node = node
            else:
                # 不然就将该结点从新压回栈了,并转向它的右结点
                stack.append(node)
                root = node.right
复制代码

层次遍历

二叉树的遍历,除了上面三种以外,还有一种层次遍历,即一层一层的访问

该树的层次遍历:A B C D F G I E H

算法实现能够借助队列实现,先根结点入队,而后:

  • 从队列中取出一个元素
  • 访问该元素
  • 若是该元素有左、右结点,则将其左右结点顺序入队
  • 直到队列为空
def level_order_traversal(root: TreeNode):
    queue = [root]
    while queue:
        node = queue.pop(0)
        print(node.val)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)
复制代码

总结

实际上二叉树的遍历核心问题:二维结构的线性化,当访问一个结点的时候,还须要访问其左右结点,可是访问左结点以后,若是再返回访问其右结点? 所以须要一个存储结构保存暂时不访问的结点,那么存储结构能够为栈,或者队列,对应的就有前,中,后,层次遍历的出现。

二叉树的遍历有许多应用,好比:输出二叉树中的叶子结点,求二叉树的高度等等,所以遍历对二叉树来讲是十分重要的。

Thanks!

相关文章
相关标签/搜索