[面试算法]Python实现二叉树三种遍历的递归与非递归形式

二叉树及其数据结构定义

二叉树是计算机当中最重要的数据结构之一,其应用非常广泛,例如数据库的索引使用的B+树是一种特殊的二叉树,堆排序所使用的堆是一种特殊的二叉树,Java当中HashMap使用的红黑树是一种特殊的二叉树。可见,二叉树在计算机编程当中有着重要地位。二叉树的遍历是二叉树的基本操作,不仅是面试的常考考点,也是程序员用来锻炼思维的小把戏。

二叉树的定义是递归的,即满足如下条件的树是二叉树:

  1. 一棵树当中的每个节点,最多有2棵子树;
  2. 如果一个节点有子树,那么子树必须是二叉树。

我们可以看到,二叉树的定义是递归的,递归的边界是一个节点没有子树。如下图所示,是一棵二叉树:
在这里插入图片描述
可以看到,二叉树当中的每个节点,有值,也可能有左子节点,也可能有右子节点,因此我们经常用二叉链表的形式存储。上述二叉树在内存当中将以以下形式存储。
在这里插入图片描述
图中,每个节点对应三个字段,或者叫域(field),包括:值(val)、左子节点的地址(left)、右子节点的地址(right),地址用ax表示,表示节点x的内存地址(address)。如果一个节点没有左子节点,那么对应的字段为null

二叉树的节点对应的Python类可以写成如下形式:

# 节点的数据结构
# val表示值、left表示当前节点的左子节点地址、right表示右子节点地址
class Node(object):
    def __init__(self, val, left, right):
        self.val = val
        self.left = left
        self.right = right

既然二叉树用上面这种形式存储起来了,那么给定一个根节点(上图当中的第1个节点,值为1的节点),我们就可以知道这个二叉树的所有信息了,这和链表类似:对于一个单链表,只给一个头指针节点,就可以知道这个单链表的所有信息。因此,当我们说给定一个二叉树的时候,其实就是给定一个二叉树的根节点,因为知道根节点就知道这棵二叉树的全部了。

三种递归方式

二叉树的遍历方式可以分为两类:深度优先遍历(Depth First Search, DFS)和广度优先遍历(Breadth First Search, BFS)。广度优先遍历BFS会在后续的博客中再谈,而深度优先遍历又包含前序、中序、后序遍历,是今天的重点。

前序、中序、后序遍历中的“序”指的是在遍历过程中,根节点遍历的时间,相对左子树和右子树的次序。三者的定义分别如下:

  • 前序遍历:先遍历根节点,再遍历左子树,最后遍历右子树。(根节点在前)
  • 中序遍历:先遍历左子树,再遍历根节点,最后遍历右子树。(根节点在中)
  • 后序遍历:先遍历左子树,再遍历右子树,最后遍历根节点。(根节点在后)

说明:我们规定,无论是哪种顺序,左子树和右子树的顺序是,先左子树后右子树。这两个的顺序不是很关键,不重要。

可以看到,上面三种遍历方式的定义,也是递归的,“遍历左子树”、“遍历右子树”,我们知道二叉树的某个节点的子树,也是二叉树,这就是递归,但是问题规模减小了,所以这个递归是可解的。那么,递归边界是什么?边界是:当一棵二叉树的左子节点和右子节点,都不存在,都为null的时候,就不继续往下遍历了。

前序遍历

针对上图的例子,前序遍历的过程如下:

首先遍历根节点1,如下图,将1存储在结果数组的最开始位置。然后发现有两棵子树,先不考虑两棵子树怎么遍历,我们只需要知道,左子树遍历的结果排在数组当中根节点1的后面,右子树的遍历结果存储在左子树遍历结果的后面,如下图所示。
在这里插入图片描述
上图中,绿色长条表示我们编程时候使用的结果数组。遍历分为三大步骤:

  1. 先遍历根节点,把根节点1,存储在数组最开始的位置;
  2. 然后遍历左子树,把左子树进行前序遍历(具体怎么遍历先不考虑),将结果存储在1的后面;
  3. 最后遍历右子树,将右子树进行前序遍历(具体怎么遍历先不考虑),将结果存储在左子树遍历结果的后面。

那么左子树、右子树该怎么遍历呢,采用和上述步骤相同的步骤。拿左子树举例,如下图所示:
在这里插入图片描述

左子树也是一棵二叉树,根节点是2,新的根节点有左右两棵子树。类似地,这棵树的遍历分为三大步骤:

  1. 遍历根节点2,写到结果数组的最开始,注意这里的最开始依然是在上图中上半部分的数组中的节点1的后面;
  2. 遍历2的左子树,将结果写到节点2的位置的后面;
  3. 遍历2的右子树,将结果写到上一步的后面。

因此,这棵树的遍历结果是:2 (4) (5 7),括号表示左子树和右子树,展开就是2 4 5 7

而原二叉树的右子树的遍历结果是:3 () (6),其中括号()表示左子树是空的,没有元素,展开就是3 6

所以将(2 4 5 7)(3 6)追加到根节点1的后面,就得到了原二叉树的前序遍历结果:1 (2 4 5 7) (3 6)

下面再来看中序遍历和后序遍历。方便起见,二叉树的图再贴到下面:
在这里插入图片描述

中序遍历

上面我们已经得到了前序遍历的结果是1 (2 4 5 7) (3 6),其中(2 4 5 7)是左子树的前序遍历的结果,(3 6)是右子树前序遍历的结果。中序遍历的结果就是先遍历左子树,再遍历根节点,最后遍历右子树。那么是不是可以根据前序遍历直接得到中序遍历的结果呢:(2 4 5 7) 1 (3 6)?不能!因为这里要求左子树和右子树也必须是中序遍历的,而(2 4 5 7)是前序遍历的结果。

中序遍历的结果,可以分为三大步骤:

  1. 中序遍历左子树:先遍历4,再遍历2,最后遍历5和75相对7是根节点,7相对5是左子树,应该先遍历7,再遍历5,所以是(7 5))。因此左子树中序遍历的结果是:(4) 2 (7 5)
  2. 遍历根节点:即1
  3. 中序遍历右子树:右子树是36,发现这颗子树当中,根节点3的左子树为空,那么就遍历根节点3,再遍历右子树6,而右子节点6没有子树了,结束了。所以这颗右子树的中序遍历结果是:3 (6)

所以,上述二叉树的中序遍历结果是:(4 2 7 5) 1 (3 6) 。打括号知识为了标记谁是子树,最终结果不需要括号。

后序遍历

后序遍历,将根节点放到最后,那么大致的顺序是(2 4 5 7) (3 6 ) 1,根节点1的位置确定放到最后了。

左子树(2 4 5 7),根节点是2放到最后,剩下左子树4(遍历左子树,发现为空,遍历右子树,发现为空,最后遍历根节点4)和右子树(5 7)(遍历根节点5的左子树7,遍历右子树为空,最后遍历根节点5),因此右子树(5 7)的后续遍历结果是(7 5)。因此这整个左子树的结果是(4 7 5 2)

右子树(3 6)先遍历6,最后遍历3,因此是(6 3)

所以,上述二叉树的后序遍历结果是:(4 7 5 2) (6 3) 1。打括号知识为了标记谁是子树,最终结果不需要括号。

递归实现

根据上面的讨论,我们已经了解了三种遍历方式。三种遍历的定义本身就是递归的,因此用递归的方式编写相对更简单,三种遍历方式的递归实现Python代码如下(经测试有效):

# 全局变量记录遍历的结果
result = []

# 前序
def dfs_before(root):
    if root == None:    # 遇到None,说明不用继续搜索下去
        return
    result.append(root)
    dfs_before(root.left)
    dfs_before(root.right)


# 中序遍历
def dfs_middle(root):
    if root == None:
        return
    dfs_middle(root.left)
    result.append(root)
    dfs_middle(root.right)


# 后序遍历
def dfs_after(root):
    if root == None:
        return
    dfs_after(root
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值