前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >总结了一些二叉树操作的干货……

总结了一些二叉树操作的干货……

作者头像
luanhz
发布2020-04-01 09:47:58
2690
发布2020-04-01 09:47:58
举报
文章被收录于专栏:小数志小数志

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

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

01 二叉树遍历

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

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

  • 前序:1-2-4-5-3-6
  • 中序:4-2-5-1-3-6
  • 后序:4-5-2-6-3-1
  • 层级遍历:1-2-3-4-5-6

前中后序的遍历操作最简单的写法是用递归,而且几乎是相同的模板:

代码语言:javascript
复制
class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        self.res = []
        self.dfs(root)
        return self.res 

    def dfs(self, node):
        if not node: return
        self.res.append(node.val)
        self.dfs(node.left)
        self.dfs(node.right)

甚至可以直接一行返回:

代码语言:javascript
复制
class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        return [root.val]+self.preorderTraversal(root.left)+self.preorderTraversal(root.right) if root else []

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

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

为了实现DFS遍历,那么一般要用到栈的数据结构。对栈的特点理解到位的话,那么前序遍历就很容易写出,仅需一层循环:

代码语言:javascript
复制
class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        stack = [root]
        values = []
        while stack:
            s = stack.pop()
            if s:
                values.append(s.val)
                stack.append(s.right)
                stack.append(s.left)
        return values

相比而言,中序遍历的迭代写法稍显复杂和更难理解,因为是,需要用到两层嵌套循环,相当于对每一个节点,都首先要进行压栈操作直至将其所有左子节点部分都遍历完成才处理当前节点,理解起来会有点绕

代码语言:javascript
复制
class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        stack = []
        p = root
        while p or stack:
            while p:
                stack.append(p)
                p = p.left
            p = stack.pop()
            res.append(p.val)
            p = p.right
        return res

后序遍历,如果按照常规理解的话,应该这样想:对于每个当前节点,都要先压栈完成左侧节点遍历,再压栈完成右侧遍历,最后再处理当前节点。这个大体思路当然是没错的,但实际上如果发现后续遍历是一种半镜像式的前序遍历,那么就很容易类似前序遍历写出其迭代方式,只需一层循环即可。

代码语言:javascript
复制
class Solution:
    def postorderTraversal(self, root):
        if root is None:
            return []
        result = []
        stack = [root]
        while stack:
            p = stack.pop()
            result.append(p.val)
            if p.left:
                stack.append(p.left)
            if p.right:
                stack.append(p.right)
        return result[::-1]

然而,如果二叉树三种遍历的迭代写法就这样结束的话,那么还难以称之为精妙。三种遍历有通用的递归模板实现,实际上也有通用的迭代模板,简单调换顺序即可实现任意遍历,简直叫人拍手称快,那就是带标记的压栈操作,自行体会下个中精妙:

代码语言:javascript
复制
class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        nodes = [(root, False)]
        res = []
        while nodes:
            node, seen = nodes.pop()
            if not node:
                continue
            elif seen:
                res.append(node.val)
            else:
                nodes.append((node.right, False))
                nodes.append((node.left, False))
                nodes.append((node, True))
        return res

相比三种深度优先遍历方式,层级遍历就没有那么多花样了。层级遍历实际上是一种BFS,一般要用到队列的性质,保证先入先出的出场顺序

代码语言:javascript
复制
class Solution:
    def levelOrder(self, root: TreeNode) -> List[List[int]]:
        nodes, res = [root], []
        while nodes:
            res.append([node.val for node in nodes if node])
            nodes = [n for node in nodes for n in [node.left, node.right]]
        return res

02 对称二叉树

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

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

  • 左右节点取值相等
  • 左节点的左子树与右节点的右子树镜像
  • 左节点的右子树与右节点的左子树镜像

其中第2、3条的镜像判断自然又要求递归调用。所以基本的递归写法:

代码语言:javascript
复制
class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        def helper(l_tree, r_tree):
            if l_tree and r_tree:
                return l_tree.val == r_tree.val and helper(l_tree.left, r_tree.right) and helper(l_tree.right, r_tree.left)
            if not l_tree and not r_tree:
                return True
            else:
                return False
        if not root: return True
        return helper(root.left ,root.right)

如果镜像二叉树的递归写法就此为止,那么也不足以称之为精妙,最妖的写法居然是无中生"树":不拿根节点的左子树和右子树来判断是否镜像,而是直接让自己与自己判断,多了一层递归,却大大简化了程序设计。实际上,这也是判断回文的一种常用做法:需要判断一个数字或者一个字符串是否是具备回文特点,不拿其左一半和其右一半比较,而是直接将它和它的逆序比较,会减少很多操作。

代码语言:javascript
复制
class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        def check(l, r):
            if l and r and l.val == r.val:
                return check(l.left, r.right) and check(l.right, r.left)
            return l == r
        return check(root, root)

当然,对称二叉树也有其迭代写法。迭代写法可以最直接将每一层数值遍历,如果当前层的节点取值是对称的,则继续判断下一层,否则直接返回false

代码语言:javascript
复制
class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        nodes,vals = [root], []
        while nodes:
            vals = [node.val if node else '' for node in nodes]
            if vals != vals[::-1]:
                return False
            nodes = [n for node in nodes if node for n in [node.left, node.right]]
        return True

还可以逐个判断。如果说递归写法是从宏观入手的话,那么其迭代写法实际上是从微观入手,即保证具有对称位置的值成对判断

代码语言:javascript
复制
class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        def check(p,q):
            if not p and not q:
                return True
            if not p or not q or p.val!=q.val:
                return False
            return True

        if not root:
            return True
        queue=deque()
        queue.append((root.left,root.right))
        while queue:
            p,q=queue.popleft()
            if not check(p,q):
                return False
            if p:
                queue.append((p.left,q.right))
                queue.append((p.right,q.left))
        return True

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

03 恢复二叉树

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

以中序+后序构建二叉树为例:

代码语言:javascript
复制
class Solution:
    def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
        def helper(left=0, right=len(inorder)-1):
            if right<left:
                return None
            rootVal = postorder.pop()
            index = inorder.index(rootVal)
            root = TreeNode(rootVal)
            root.right = helper(index+1, right)
            root.left = helper(left, index-1)
            return root
        return helper()

考虑到递归调用期间,存在多次在中序中查找某个节点的下标,则可以运用一个字典将其初始化保存起来,多次查找下标在O(1)时间完成,总的时间复杂度是构建字典O(n)+O(lgn)次查找,否则查找总的时间复杂度是O(nlgn)

代码语言:javascript
复制
class Solution:
    def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
        maps = {val:index for index, val in enumerate(inorder)}
        def helper(left=0, right=len(inorder)-1):
            if right<left:
                return None
            rootVal = postorder.pop()
            index = maps[rootVal]
            root = TreeNode(rootVal)
            root.right = helper(index+1, right)
            root.left = helper(left, index-1)
            return root
        return helper()

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

04 寻找公共祖先

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

这里给出其比较巧妙的递归写法:从宏观着眼,两个节点若在根节点的同一侧子树中,则待查找的公共祖先一定是在该子树的某个节点;否则若两个节点分居根节点两侧,则根节点就是唯一公共祖先,自然也就是最早的公共祖先。运用递归的思想,则在同侧的情形中可进一步递归判断

代码语言:javascript
复制
class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        if root is None or root==p or root==q:
            return root
        left=self.lowestCommonAncestor(root.left,p,q)
        right=self.lowestCommonAncestor(root.right,p,q)
        if left==None and right==None:
            return None
        elif left!=None and right!=None: 
            return root 
        else:
            if left==None:
                return right
            if right==None:
                return left

05 序列化与反序列化

序列化和反序列是模拟二叉树结构数据的编码和解码的问题,即将一个二叉树结构的数据按一定形式存储为连续序列,反序列化则是根据给定的连续序列形式反推二叉树结构。按照层级遍历的方式进行处理,会比较简单

代码语言:javascript
复制
class Codec:
    def serialize(self, root):
        data = []
        nodes = [root]
        while nodes:
            data.append([node.val if node else 'null' for node in nodes])
            nodes = [n for node in nodes if node for n in [node.left, node.right]]
        return data

    def deserialize(self, data):
        root = TreeNode(data[0][0]) if len(data)>1 else None
        if not root:
            return root
        nodes = [root]
        for vals in data[1:]:
            for i in range(len(nodes)):
                nodes[i].left = TreeNode(vals[i*2]) if vals[i*2] != 'null' else None
                nodes[i].right = TreeNode(vals[i*2+1]) if vals[i*2+1] != 'null' else None
            nodes = [n for node in nodes  for n in [node.left, node.right] if n]
        return root

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小数志 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档