数据结构与算法----复习Part 14 (树与二叉树)

本系列是算法通关手册LeeCode的学习笔记

算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)

目录

一,树(Tree)

树的相关术语

节点间关系

树的其他术语

二,二叉树(Binary Tree)

满二叉树(Full Binary Tree)

完全二叉树(Complete Binary Tree)

二叉搜索树(Binary Search Tree)

平衡二叉搜索树(Balanced Binary Tree)

三,二叉树的存储结构

顺序存储结构

链式存储结构

四,二叉树的遍历

二叉树的前序遍历

二叉树的中序遍历

二叉树的后序遍历

二叉树的层序遍历

五,二叉树的还原

从前序和中序序列构造二叉树:

从中序和后序序列构造二叉树

从前序和后序序列构造二叉树


 一,树(Tree)

        由 n >= 0 个节点与节点之间的关系组成的有限集合。 n = 0 时称空树,n > 0 时称非空树。

        树 具有以下特点:

                有且仅有一个节点没有前驱节点,该节点称为树的【根节点(Root)】;

                除了根节点之外,每个节点有且仅有一个直接前驱节点;

                包括根节点在内,每个节点可以有多个后继节点(一对多的关系);

                当 n > 1 时,除了根节点之外的其他节点与其后继节点组成的也是一棵树,称为根的

        【子树(Sub Tree)】;

树的相关术语

        

树的节点:由一个数据元素和若干个指向其子树的分支组成(节点 加 指向子树的箭头);

节点的度:一个节点所含有的子树个数;

叶子节点:度为 0 的节点,如图中的 C、F、H、I、G、K;

分支节点:度不为 0 的节点,如图中的 A、B、D、E、G;

数的度:树中节点的最大度数,如图中树的度为 3;

节点间关系

子节点:一个节点含有的【子树的根节点】称为该节点的子节点;

父节点:如果一个节点含有子节点,则称该节点为其子节点的父节点;

兄弟节点:具有相同父节点的节点互称为兄弟节点;

树的其他术语

节点的层次:从根节点开始定义,根为第 1 层,根的子节点为第 2 层,以此类推;

树的深度(高度):所有节点中最大的层数;

堂兄弟节点:父节点在同一层的节点互为堂兄弟;

路径:树中两个节点之间所经过的节点序列。如图中 E 到 G 的路径为 E-B-A-D-G;

路径长度:两个节点之间路径上经过的边数。如图中 E 到 G 的路径长度为 4;

节点的祖先:从该节点到根节点所经过的所有节点,被称为该节点的祖先。如图中 H 的祖先为 E、B、A;

节点的子孙:节点的子树中所有节点被称为该节点的子孙。例如图中 D 的子孙为 F、G、K

有序树:节点的各个子树从左至右有序,不能呼唤位置;

无序树:节点的各个子树可互换位置。

二,二叉树(Binary Tree)

        树中各个节点的度不大于 2 个的有序树,称为二叉树。通常树中的分支节点被称为【左子树】或【右子树】。二叉树的分支具有左右次序,不能随意互换位置。

        二叉树可以用递归的方式来定义,即二叉树满足以下两个要求之一:

                空树:二叉树是一个空树;

                非空树:二叉树是有一个根节点和两棵互不相交的子树,分别称为根节点的左子树、右

        子树组成的非空树,并且其本身都是二叉树。

        二叉树是种特殊的树,它最多有两个子树,分别为左子树和右子树,并且两个子树是有序的,不可以互换。也就是说,在二叉树种不存在度大于 2 的节点。

        二叉树在逻辑上可以分为 5 种基本形态

满二叉树(Full Binary Tree)

        如果所有分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,则称该二叉树为满二叉树。

        满二叉树满足:

                叶子节点只出现在最下面一层;

                非叶子节点的度一定为 2 ;

                在同等深度的二叉树中,满二叉树的节点个数最多,叶子节点个数最多。

        如果我们对满二叉树的节点进行编号。根节点编号为 1,然后按照层次依次向下,每一层从左到右的顺序进行编号,则深度为 k 的满二叉树最后一个节点的编号为 2^k - 1。

完全二叉树(Complete Binary Tree)

        如果叶子节点只能出现在最下面两层,并且最下层的叶子节点都依次排列在该层最左边的位置上,称为完全二叉树。

        完全二叉树满足:

                叶子节点只能出现在最下面两层;

                最下层的叶子节点一定集中在该层最左边的位置上;

                倒数第二层如果有叶子节点,则一定集中在右边位置上;

                如果节点的度为一,则该节点只有做孩子节点;

                

二叉搜索树(Binary Search Tree)

        也叫二叉查找树,是指一棵空树或具有以下性质的二叉树:

                如果任意节点的左子树不为空,则左子树上所有节点的值均小于它根节点的值;

                如果任意节点的右子树不为空,则右子树上所有节点的值均大于它根节点的值;

                任意节点的左子树、右子树均为二叉搜索树

平衡二叉搜索树(Balanced Binary Tree)

        一种结构平衡的二叉搜索树。即叶节点的高度差绝对值不超过 1,并且左右两个子树都是一棵平衡二叉搜索树。

三,二叉树的存储结构

        二叉树的存储结构分为两种:【顺序存储结构】和【链式存储结构】。

顺序存储结构

        其实,堆排序、优先队列中的二叉堆结构,采用的就是二叉树的顺序存储结构。

        二叉树的顺序存储结构使用一维数组在存储二叉树中的节点,节点存储位置则采用完全二叉树的节点层次编号,按照层次从上到下,每一层从左到右的顺序依次存放二叉树的数据元素。在进行顺序存储时,如果对应的二叉树节点不存在,则设置为空节点。

        可以看出节点之间的逻辑关系:

                如果某二叉树节点(非叶子节点)的下标为 i,那么其左孩子节点的下标为 i * i + 1,右

        孩子节点的下标为 2 * i + 2;

                如果某二叉树节点(非根节点)的下标为 i,那么其根节点的下标为 (i - 1) // 2

        对于完全二叉树(尤其是满二叉树)来说,采用顺序存储结构比较合适,它能充分利用存储空间,而对于一般二叉树,如果需要设置很多的【空节点】,则采用顺序存储结构就会浪费很多存储空间,并且由于顺序存储结构固有的一些缺陷,会使得二叉树的插入,删除等操作不方便,效率也比较低,对于二叉树来说,当数的形态和大小经常发生动态变化时,更适合采用链式存储结构。

链式存储结构

        二叉树采用链式存储结构时,每个节点包含一个用于数据域 val ,存储节点信息,还包含两个指针域 left 和 right ,分别指向左右两个孩子节点,当左孩子或右孩子不存在,相应指针域为空。

对应代码:

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

        二叉树的链表存储结构具有灵活、方便的特点。节点的最大数目只受系统最大可存储空间的限制。一般情况下,二叉树的链表存储结构比顺序存储结构更省空间,而对于二叉树实施相关操作也很方便,因此,一般使用链式存储结构来存储二叉树。

四,二叉树的遍历

        从根节点出发,按照某种次序依次访问二叉树中的所有节点,使得每个节点被访问依次且仅被访问一次。

        使用深度优先的方式遍历二叉树,分为二叉树的前、中、后序遍历。

        使用广度优先遍历二叉树,则是二叉树的层序遍历。

二叉树的前序遍历

        如果二叉树为空,则返回;

        如果二叉树不为空,则:

                访问根节点;

                以前序遍历的方式遍历根节点的左子树;

                以前序遍历的方式遍历根节点的右子树;

        可见前序遍历过程是一个递归过程,在遍历任何一棵子树时,仍然是按照先访问根节点,然后遍历子树根节点的左子树,最后遍历根节点的右子树的顺序进行遍历。

注意这里的访问,指的是得到该节点的 val 值,而不是进入该节点。

递归实现

class Solution:
    def preorderTraversal(self, root: TreeNode):
        res = []
        
        def preorder(root):
            if not root:
                return
            res.append(root.val)
            preorder(root.left)
            preorder(root.right)
        
        preorder(root)
        return res

前序遍历的显示栈实现:

        根据栈先入后出的特点,先放入右子树,再放入左子树。

        判断二叉树是否为空,为空则直接返回;

        初始化维护一个栈,将根节点入栈;

        当栈不为空时:

                弹出栈顶元素 node,并访问该元素;

                如果 node 的右子树不为空,则将 node 的右子树入栈;

                如果 node 的左子树不为空,则将 node 的左子树入栈;

    def preorderTraversalStack(self, root):
        if not root:
            return []

        res = []
        stack = [root]

        while stack:
            node = stack.pop()
            res.append(node.val)
            if node.right:
                stack.append(node.right)
            if node.left:
                stack.append(node.left)
        
        return res

二叉树的中序遍历

        如果二叉树为空,则返回;

        如果二叉树不为空,则:

                以中序遍历的方式遍历根节点的左子树;

                访问根节点;

                以中序遍历的方式遍历根节点的右子树;

递归实现:

    def inorderTraversal(self, root: TreeNode):
        res = []
        
        def inorder(root):
            if not root:
                return
            inorder(root.left)
            res.append(root.val)
            inorder(root.right)
        
        inorder(root)
        return res

中序遍历的显式栈实现:

        访问根节点要在左子树遍历完之后,因此我们应该从根节点开始,循环遍历左子树,不断将当前子树的根节点放入栈中,直到当前节点无左子树时,从栈中弹出该节点并处理;

        判断二叉树是否为空,为空则直接返回;

        初始化维护一个空栈;

        当根节点或者栈不为空时,

                如果当前节点不为空,则遍历左子树,并不断把当前子树的根节点入栈;

                如果当前节点为空,则说明当前节点无左子树,则弹出栈顶元素 node,并访问,然后尝

        试访问该节点的右子树;

    def inorderTraversalStack(self, root):
        if not root:
            return []
        
        res = []
        stack = []
        
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            
            node = stack.pop()
            res.append(node.val)
            root = node.right
        
        return res

二叉树的后序遍历

        如果二叉树为空,则返回;

        如果二叉树不为空,则

                以后序遍历的方式遍历根节点的左子树;

                以后序遍历的方式遍历根节点的右子树;

                访问根节点

递归实现:

    def postorderTraversal(self, root: TreeNode):
        res = []
        
        def postorder(root):
            if not root:
                return 
            postorder(root.left)
            postorder(root.right)
            res.append(root.val)
        
        postorder(root)
        return res

后序遍历的显式栈实现

        后序遍历中,根节点的访问在左右子树访问之后。

        判断二叉树是否为空,为空则直接返回;

        初始化维护一个空栈,使用 prev 保存前一个访问的节点,用于确定当前节点的右子树是否已经访问完毕;

        当根节点或者栈不为空时,从当前节点开始:

                如果当前节点右左子树,则不断遍历左子树,并将当前根节点压入栈中;

                如果当前节点无左子树,则弹出栈顶元素 node;

                如果栈顶元素 node 无右子树或者右子树已经访问完毕,则访问该元素,然后记录前一

        节点,并将当前节点标记为空节点,意为需要栈弹出一个栈顶元素;

                如果栈顶元素 node 有右子树,则将栈顶元素重新压入栈中,继续访问栈顶元素的右子

        树。

    def postorderTraversalStack(self, root):
        res = []
        stack = []
        prev = None
        
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            
            node = stack.pop()
            
            if not node.right or node.right == prev:
                res.append(node.val)
                prev = node
                root = None     # 需要弹出新的栈顶元素
            else:
                stack.append(node)
                root = node.right
        
        return res

二叉树的层序遍历

        如果二叉树为空,则返回;

        如果二叉树不为空,则:

                先依次访问二叉树第一层的节点;

                然后依次访问二叉树第二层的节点;

                ...

        层序遍历过程是一个广度优先搜索的过程

二叉树的层序遍历通过队列实现:

        判断二叉树是否为空,为空则直接返回;

        令根节点入队;

        当队列不为空时,求出当前队列长度 s;

        依次从队列中取出这 s 个元素,并对这 s 个元素依次进行访问,然后将其左右孩子节点入队,然后继续遍历下一层节点;

        当队列为空时,结束遍历;

实现代码:

    def levelOrder(self, root: TreeNode):
        if not root:
            return []
        
        queue = [root]
        order = []
        
        while queue:
            level = []
            size = len(queue)
            for _ in range(size):
                cur = queue.pop(0)
                level.append(cur.val)
                if cur.left:
                    queue.append(cur.left)
                if cur.right:
                    queue.append(cur.right)
            if level:
                order.append(level)
        return order

104. 二叉树的最大深度

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if not root :
            return 0
        return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
        

103. 二叉树的锯齿形层序遍历

import collections
class Solution:
    def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root:
            return []
        queue = [root]
        order = []
        odd = True
        while queue:
            level = collections.deque()
            size = len(queue)
            for _ in range(size):
                curr = queue.pop(0)
                if odd:
                    level.append(curr.val)
                else:
                    level.appendleft(curr.val)
                if curr.left:
                    queue.append(curr.left)
                if curr.right:
                    queue.append(curr.right)
            if level:
                order.append(list(level))
            odd = not odd
        return order

111. 二叉树的最小深度

# 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 minDepth(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        queue = [root]
        dep = 1
        while queue:
            n = len(queue)
            for i in range(n):
                node = queue.pop(0)
                if node.left == None and node.right == None:
                    return dep
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            dep += 1
        return dep

        与最大不同,找树的最小深度算是广度遍历的经典应用场景,逐层的排查节点是否是叶子节点,能够找到树的最小深度。

五,二叉树的还原

        指的是通过二叉树的遍历序列,还原出对应的二叉树。

        这个部分比较抽象,所以建议找视频资源进行学习:

        比如:【已知二叉树中序、后序序列,如何还原二叉树?一道题搞定】 https://www.bilibili.com/video/BV1624y1t7Cg/share_source=copy_web&vd_source=87ace2077a752fc4561aff19bae87dac

         简单来说就是根据遍历过程中的规则,不断查找两个遍历结果,实现二叉树的还原,我们的重心在代码实现。而代码实现的重点在于充分明白所知序列能够发挥的作用。

前序序列作用,前序的遍历顺序是【根左右】,因此我们可以从中知道根节点的位置;

中序序列作用,中序遍历的顺序是【左根右】,因此我们知道根节点后,可以知道左子树与右子树的中序序列

后序序列作用,后序遍历的顺序是【左右根】,我们可以知道根节点的位置。

        开始代码实现之前,我们要明确的是,只有前序和后序无法唯一确定二叉树。

从前序和中序序列构造二叉树:

        从前序遍历序列中得到当前根节点位置在 preorder[ 0 ];

        通过在中序遍历中查找上一步根节点对应位置 inorder[ k ],从而将二叉树左右子树分隔开,并得到左右子树节点个数;

        从上一步得到的左右子树个节点个数,将前序序列中的左右子树分开;

        构造当前节点,并递归建立左右树,在左右树的对应位置继续递归遍历并执行上述三步,直到节点为空

    def buildTreeFromPreIn(self, preorder, inorder):
        def createTree(preorder, inorder, n):
            if n == 0:
                return None

            k = 0

            while preorder[0] != inorder[k]:
                k += 1  #   找到中序遍历的分隔位置
            node = TreeNode(inorder[k])
            node.left = createTree(preorder[1: k + 1], inorder[0: k], k)
            node.right = createTree(preorder[k + 1: ], inorder[k + 1: ], n - k - 1)
            return node

        return createTree(preorder, inorder, len(inorder))

从中序和后序序列构造二叉树

        从后序序列中得到当前根节点的位置在 postorder[ n - 1 ];

        通过在中序遍历中查找根节点对应的位置 inorder[k],从而将二叉树的所有子树分隔开,并得到左右子树的节点个数;

        从上一步得到的左右子树节点个数,将后序序列中的左右子树分开;

        构造当前节点,并递归建立左右子树,并在左右子树对应位置继续递归执行上述三步,直到节点为空

    def buildTreeFromInPost(self, inorder, postorder):
        def createTree(inorder, postorder, n):
            if n == 0:
                return None

            k = 0

            while postorder[n - 1] != inorder[k]:
                k += 1
            node = TreeNode(inorder[k])
            node.right = createTree(inorder[k + 1: n], postorder[k: n - 1], n - k - 1)
            node.left = createTree(inorder[0: k], postorder[0: k], k)
            return node
        return createTree(inorder, postorder, len(postorder))

从前序和后序序列构造二叉树

        我们可以默认指定前序序列的第 2 个值为左子树的根节点,由此递归划分左右子树。

        从前序遍历序列中可知当前根节点的位置在 preorder[ 0 ];

        从前序序列的第二个值为左子树的根节点,通过在后序序列中查找上一步根节点对应的位置,postorder[ k ]。从而将二叉树的左右子树分隔开,从得到左右子树节点的个数.

        从上一步得到的左右子树个数,将后序序列结果中的左右子树分开;

        构造当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。

 def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> TreeNode:
        def createTree(preorder, postorder, n):
            if n == 0:
                return None
            node = TreeNode(preorder[0])
            if n == 1:
                return node

            k = 0

            while postorder[k] != preorder[1]:
                k += 1
            node.left = createTree(preorder[1: k + 2], postorder[: k + 1], k + 1)
            node.right = createTree(preorder[k + 2: ], postorder[k + 1: -1], n - k - 2)
            return node
        return createTree(preorder, postorder, len(preorder))

算法通关手册(LeetCode) | 算法通关手册(LeetCode)

原文内容在这里,如有侵权,请联系我删除。

  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值