vlambda博客
学习文章列表

总结了一些算法二叉树操作的干货 (附Python代码)


导读:二叉树是一种经典的数据结构,其概念本身不难理解,但因其结构的特殊性,许多操作都有着非常精妙的技巧。结合最近LeetCode中的一些相关题目,简要记录一些个人觉得比较巧妙的编程实现。


以下所有案例均来源于LeetCode,部分源码参考他人题解。


01 二叉树遍历

二叉树遍历是最基本和典型的操作,主要分为2大类共4种遍历形式,分别是DFS(深度优先)和BFS(广度优先,即按层级遍历),其中DFS又具体分为前序、中序和后序遍历。这里的前中后序实际上是指根节点相对左右子节点的位置来描述的,如前序遍历就是指根节点在左右节点之前,中序则是根节点在左右子节点之间,后序则是根节点在最后。

总结了一些算法二叉树操作的干货 (附Python代码)

如图中的二叉树所示,则其不同遍历方式的结果分别为:

  • 前序:1-2-4-5-3-6

  • 中序:4-2-5-1-3-6

  • 后序:4-5-2-6-3-1

  • 层级遍历:1-2-3-4-5-6


前中后序的遍历操作最简单的写法是用递归,而且几乎是相同的模板:
 1class Solution:
2    def preorderTraversal(self, root: TreeNode) -> List[int]:
3        self.res = []
4        self.dfs(root)
5        return self.res 
6
7    def dfs(self, node):
8        if not node: return
9        self.res.append(node.val)
10        self.dfs(node.left)
11        self.dfs(node.right)


甚至可以直接一行返回:
1class Solution:
2    def preorderTraversal(self, root: TreeNode) -> List[int]:
3        return [root.val]+self.preorderTraversal(root.left)+self.preorderTraversal(root.right) if root else []


上面代码中,只要稍微更改主程序中递归调用的先后顺序,即可很容易改写成其他遍历方式。


用递归可以实现的操作,一般来说迭代也可以,二叉树的遍历也不例外。

为了实现DFS遍历,那么一般要用到栈的数据结构。对栈的特点理解到位的话,那么前序遍历就很容易写出,仅需一层循环:
 1class Solution:
2    def preorderTraversal(self, root: TreeNode) -> List[int]:
3        stack = [root]
4        values = []
5        while stack:
6            s = stack.pop()
7            if s:
8                values.append(s.val)
9                stack.append(s.right)
10                stack.append(s.left)
11        return values


相比而言,中序遍历的迭代写法稍显复杂和更难理解,因为是,需要用到两层嵌套循环,相当于对每一个节点,都首先要进行压栈操作直至将其所有左子节点部分都遍历完成才处理当前节点,理解起来会有点绕
 1class Solution:
2    def inorderTraversal(self, root: TreeNode) -> List[int]:
3        res = []
4        stack = []
5        p = root
6        while p or stack:
7            while p:
8                stack.append(p)
9                p = p.left
10            p = stack.pop()
11            res.append(p.val)
12            p = p.right
13        return res


后序遍历,如果按照常规理解的话,应该这样想:对于每个当前节点,都要先压栈完成左侧节点遍历,再压栈完成右侧遍历,最后再处理当前节点。这个大体思路当然是没错的,但实际上如果发现后续遍历是一种半镜像式的前序遍历,那么就很容易类似前序遍历写出其迭代方式,只需一层循环即可。
 1class Solution:
2    def postorderTraversal(self, root):
3        if root is None:
4            return []
5        result = []
6        stack = [root]
7        while stack:
8            p = stack.pop()
9            result.append(p.val)
10            if p.left:
11                stack.append(p.left)
12            if p.right:
13                stack.append(p.right)
14        return result[::-1]


然而,如果二叉树三种遍历的迭代写法就这样结束的话,那么还难以称之为精妙。三种遍历有通用的递归模板实现,实际上也有通用的迭代模板,简单调换顺序即可实现任意遍历,简直叫人拍手称快,那就是带标记的压栈操作,自行体会下个中精妙:
 1class Solution:
2    def preorderTraversal(self, root: TreeNode) -> List[int]:
3        nodes = [(root, False)]
4        res = []
5        while nodes:
6            node, seen = nodes.pop()
7            if not node:
8                continue
9            elif seen:
10                res.append(node.val)
11            else:
12                nodes.append((node.right, False))
13                nodes.append((node.left, False))
14                nodes.append((node, True))
15        return res


相比三种深度优先遍历方式,层级遍历就没有那么多花样了。层级遍历实际上是一种BFS,一般要用到队列的性质,保证先入先出的出场顺序
1class Solution:
2    def levelOrder(self, root: TreeNode) -> List[List[int]]:
3        nodes, res = [root], []
4        while nodes:
5            res.append([node.val for node in nodes if node])
6            nodes = [n for node in nodes for n in [node.left, node.right]]
7        return res



02 对称二叉树

对称二叉树是指二叉树具有镜像的特点,即对称位置的节点具有相同值。


最直观的想法是递归判断:若一棵二叉树是镜像的,那么要求:

  • 左右节点取值相等

  • 左节点的左子树与右节点的右子树镜像

  • 左节点的右子树与右节点的左子树镜像


其中第2、3条的镜像判断自然又要求递归调用。所以基本的递归写法:
 1class Solution:
2    def isSymmetric(self, root: TreeNode) -> bool:
3        def helper(l_tree, r_tree):
4            if l_tree and r_tree:
5                return l_tree.val == r_tree.val and helper(l_tree.left, r_tree.right) and helper(l_tree.right, r_tree.left)
6            if not l_tree and not r_tree:
7                return True
8            else:
9                return False
10        if not root: return True
11        return helper(root.left ,root.right)


如果镜像二叉树的递归写法就此为止,那么也不足以称之为精妙,最妖的写法居然是无中生"树":不拿根节点的左子树和右子树来判断是否镜像,而是直接让自己与自己判断,多了一层递归,却大大简化了程序设计。实际上,这也是判断回文的一种常用做法:需要判断一个数字或者一个字符串是否是具备回文特点,不拿其左一半和其右一半比较,而是直接将它和它的逆序比较,会减少很多操作。
1class Solution:
2    def isSymmetric(self, root: TreeNode) -> bool:
3        def check(l, r):
4            if l and r and l.val == r.val:
5                return check(l.left, r.right) and check(l.right, r.left)
6            return l == r
7        return check(root, root)


当然,对称二叉树也有其迭代写法。迭代写法可以最直接将每一层数值遍历,如果当前层的节点取值是对称的,则继续判断下一层,否则直接返回false
1class Solution:
2    def isSymmetric(self, root: TreeNode) -> bool:
3        nodes,vals = [root], []
4        while nodes:
5            vals = [node.val if node else '' for node in nodes]
6            if vals != vals[::-1]:
7                return False
8            nodes = [n for node in nodes if node for n in [node.left, node.right]]
9        return True

还可以逐个判断。如果说递归写法是从宏观入手的话,那么其迭代写法实际上是从微观入手,即保证具有对称位置的值成对判断
 1class Solution:
2    def isSymmetric(self, root: TreeNode) -> bool:
3        def check(p,q):
4            if not p and not q:
5                return True
6            if not p or not q or p.val!=q.val:
7                return False
8            return True
9
10        if not root:
11            return True
12        queue=deque()
13        queue.append((root.left,root.right))
14        while queue:
15            p,q=queue.popleft()
16            if not check(p,q):
17                return False
18            if p:
19                queue.append((p.left,q.right))
20                queue.append((p.right,q.left))
21        return True


一个对称二叉树的判断就有这么多思路,不得不说二叉树的操作实在是太精妙了。


03 恢复二叉树

前面提到3种遍历方式会产生不同的遍历结果,那么根据遍历结果也可以反向构建相应的二叉树。这里,要求至少具有中序遍历,外加前序或者后序方可反向构建。否则会存在歧义。


以中序+后序构建二叉树为例:
 1class Solution:
2    def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
3        def helper(left=0, right=len(inorder)-1):
4            if right<left:
5                return None
6            rootVal = postorder.pop()
7            index = inorder.index(rootVal)
8            root = TreeNode(rootVal)
9            root.right = helper(index+1, right)
10            root.left = helper(left, index-1)
11            return root
12        return helper()


考虑到递归调用期间,存在多次在中序中查找某个节点的下标,则可以运用一个字典将其初始化保存起来,多次查找下标在O(1)时间完成,总的时间复杂度是构建字典O(n)+O(lgn)次查找,否则查找总的时间复杂度是O(nlgn)
 1class Solution:
2    def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
3        maps = {val:index for index, val in enumerate(inorder)}
4        def helper(left=0, right=len(inorder)-1):
5            if right<left:
6                return None
7            rootVal = postorder.pop()
8            index = maps[rootVal]
9            root = TreeNode(rootVal)
10            root.right = helper(index+1, right)
11            root.left = helper(left, index-1)
12            return root
13        return helper()


稍加改写即可实现前序+中序反向构建的写法。


04 寻找公共祖先
寻找公共祖先是指在一颗二叉树中找到给定两个节点的最早公共节点,也存在递归和迭代两种写法。在迭代写法中,首先遍历整个树构建每个节点的前溯节点,进而根据两个节点的前溯节点反向对比,直至找到第一个公共节点。

这里给出其比较巧妙的递归写法:从宏观着眼,两个节点若在根节点的同一侧子树中,则待查找的公共祖先一定是在该子树的某个节点;否则若两个节点分居根节点两侧,则根节点就是唯一公共祖先,自然也就是最早的公共祖先。运用递归的思想,则在同侧的情形中可进一步递归判断
 1class Solution:
2    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
3        if root is None or root==p or root==q:
4            return root
5        left=self.lowestCommonAncestor(root.left,p,q)
6        right=self.lowestCommonAncestor(root.right,p,q)
7        if left==None and right==None:
8            return None
9        elif left!=None and right!=None
10            return root 
11        else:
12            if left==None:
13                return right
14            if right==None:
15                return left



05 序列化与反序列化
序列化和反序列是模拟二叉树结构数据的编码和解码的问题,即将一个二叉树结构的数据按一定形式存储为连续序列,反序列化则是根据给定的连续序列形式反推二叉树结构。按照层级遍历的方式进行处理,会比较简单
 1class Codec:
2    def serialize(self, root):
3        data = []
4        nodes = [root]
5        while nodes:
6            data.append([node.val if node else 'null' for node in nodes])
7            nodes = [n for node in nodes if node for n in [node.left, node.right]]
8        return data
9
10    def deserialize(self, data):
11        root = TreeNode(data[0][0]) if len(data)>1 else None
12        if not root:
13            return root
14        nodes = [root]
15        for vals in data[1:]:
16            for i in range(len(nodes)):
17                nodes[i].left = TreeNode(vals[i*2]) if vals[i*2] != 'null' else None
18                nodes[i].right = TreeNode(vals[i*2+1]) if vals[i*2+1] != 'null' else None
19            nodes = [n for node in nodes  for n in [node.left, node.right] if n]
20        return root


看完这些案例,不得不再次惊叹于二叉树操作的精妙,而且这还只是其中的冰山一角而已,其精妙程度还大有潜力空间。