第二轮 Python 刷题笔记二:树

在第二轮刷题一开始,我们是整理了关于数组的五道 LeetCode 题,按照很多算法训练的顺序,一般紧跟数组的是链表、树,那么我们第二篇就集中来整理下树的题目。之前提到,这是我们第二轮刷题,在先前刷到二叉树题目时,曾经整理过两篇关于树的基础知识点和四种遍历方法:

二叉树专题一:基础知识点,前中后序遍历

二叉树专题二:层级遍历及随机题目

如果只是刷一遍题目,可能了解如上的四种遍历代码模版便足够,但这样缺点很明显:只知道这样写代码,别人一问却不知其背后的道理——换言之,只是会解题而已。今天再次回顾整理树的题目,我们要着眼于解决树问题的理念与思路,吃透其背后的解法。

我们之前了解过树的概念:树是一种抽象数据类型或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由 n(n>0) 个有限结点组成一个具有层次关系的集合。

树图示

当树中每个节点最多只有两个分支、或子节点时,便是我们常说的二叉树。

当树是一棵空树;或其作为二叉树,其左子树上所有结点值均小于根结点值,其右子树上所有结点值均大于根结点值,且其左子树和右子树也符合如上规律时,我们称该树为二叉搜索树。

通常关于二叉树题目的解答区域内,也会有如下关于树结点的定义,一个 TreeNode 类,在其实例化时会定义父结点值 val,左子结点 left 和右子结点 right:

# Definition for a binary tree node.
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

定义中子结点均为 None,但真到题目中子结点也会是 TreeNode 的实例。此外,题目中多用根结点 root 来代表一棵树作为函数的输入,因为通过该根结点我们可以向下展开得到树中所有结点;或者用一个遍历各结点的列表来作为输入,我们把它当成其树中各结点组成的列表即可。

了解完如上关于树的基本概念后,我们也要明确树的“意义”。有数组、链表这些数据结构,为什么还要引入树这种结构来存储数据呢?这是因为数组、链表只能一维储存数据,即当前数据往下走,没有分支可选;但我们生活中,很多数据的下一个状态是个多项选择,比如我们漫漫人生路,每次面临抉择,是可以做多项选择的,这种面临选择的时刻我们即处于一个父结点,面对的选项为各种子结点,不过可惜的是树中可以回溯到上一结点,但人生却是单项的。

通过之前第一轮刷题时,也很明显感受到,二叉树的题解中出现递归的频率非常高,这是因为树的结构就是由一个父结点不断重复产生其子结点,与递归不断重复执行相同过程是一致的。所以结合着树的题目来练习递归解法,不仅可以加深对递归理解,也能够对树的结构和特定有更清晰的理解。

接下来我们重新回顾之前的题目,先尝试递归实现,再分析之前发现的套路模版,进一步加深对各题目的印象,这样之后遇到题目就不再畏惧其引申与变形了。

这次我们还是实时记录回顾各题目的思路,5-10 分钟考虑,没头绪就看题解,等消化了再来重新做,直到掌握。

题目一

第 144 题:二叉树的前序遍历

难度:中等

给定一个二叉树,返回它的前序遍历。

示例:

输入: [1,null,2,3]  
   1
    \
     2
    /
   3 

输出: [1,2,3]

自行尝试

首先回顾下前序遍历,这里前指根结点和子结点之间的位置关系,根-左-右,根结点位于子结点前面,所以为前序遍历:

前序遍历图示
如图,从根结点开始,最初根-左-右为 [8,3,10],但紧接着 3 作为左子树的根结点要同样进行前序遍历,这里就相当于对该结点调用前序遍历形成递归,同理对 10 这个右子结点也要进行前序遍历来填充我们的遍历列表。这样分析下来便可以写出递归实现代码:

# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
    	# 定义前序遍历的函数
        def helper(node):
        	# 终止条件,空结点返回空列表
            if not node:
                return []
            # 返回 根-左-右,根为根结点处值所在列表,左和右则递归执行 helper 函数,将三个结果列表加起来即最终结果
            return [node.val] + helper(node.left) + helper(node.right)
		# 从根结点开始执行该函数,将结果返回
        return helper(root)

当然,如果不想额外定义这函数,也可以直接在 preorderTraversal 这方法基础上进行递归:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        if not root:
            return []
        return [root.val] + self.preorderTraversal(root.left) + self.preorderTraversal(root.right)

还记得我们之前看到的颜色标记法模版吗?在二叉树专题一里,我们用其完成了前中后序遍历,此时有了递归实现遍历的经验之后,便可以发现其实现原理是自己建一个栈(可以理解为列表,但特点是后进的先出),来模拟递归过程来实现对各结点的全记录。

比如简单些只有 [8,3,10] 三个结点,我们建立的栈只要按照相反的顺序收录 右-左-根 结点,再通过栈输出,即可得到前序遍历结果。那如何区分哪些结点还要进行前序遍历、那些结点直接输出值呢?这就是颜色标记的作用,初次收录该结点时,该结点是以子结点形式出现的、还未进行前序遍历,当再次遇到该结点时、其已为父结点要输出值了。颜色标记即通过数字 1 和 0 来标记其出现的情况,代码实现:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
    	# 以 (状态,结点) 的元组形式记录每个结点,最初栈中只有根结点
        stack = [(1,root)]
        result = []
        # 若栈非空,则循环
        while stack:
        	# 通过列表的 pop() 来实现栈的后进先出
            s,node = stack.pop()
            # 空结点跳过
            if node is None:
                continue
            # 若标记为 1,要对其进行遍历,按前序的相反顺序记录结点
            if s==1:
            	# 前序的倒序记录,子结点标记为 1,已经标过 1 的父结点状态置为 0
                stack.append((1,node.right))
                stack.append((1,node.left))
                stack.append((0,node))
            # 若标记为 0,将结点值记录到结果列表中
            else:
                result.append(node.val)
        # 返回结果列表
        return result

注意,这道题目要求以列表形式输出结果,如果还有其它操作,完全可以在 result.append(node.val) 这一步中去处理、实现,这样就不必等到所有结果全部拿到才去做处理。目前遇到不少题目都是问到做些判断,如何加速,便可在这里去实现。

学习题解

我们再来翻翻国外大神点赞最高的题解:

def preorderTraversal(self, root):
    ret = []
    stack = [root]
    while stack:
        node = stack.pop()
        if node<
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值