【二叉树】总结

1、二叉树基础概念

参考Carl哥的『代码随想录』

特殊二叉树

满二叉树

如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。如果深度为k,则有 2 k − 1 2^{k}-1 2k1个节点的二叉树。
在这里插入图片描述

完全二叉树

在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2 h − 1 2^{h-1} 2h1 个节点
在这里插入图片描述

二叉搜索树

二叉搜索树是一个有序树。

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉搜索树
    在这里插入图片描述

平衡二叉搜索树

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
在这里插入图片描述
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn。

用数组存储并构建二叉树

在这里插入图片描述

def buildtree(self,nums):
    def buildnums(index):
        if index>len(nums)-1:
            return
        if nums[index] is not None:
            root = Treenode(nums[index])
            root.left = buildnums(2*index+1)
            root.right = buildnums(2*index+2)
            return root
        return None

    root = buildnums(0)
    # print(root)
    return root

2、遍历二叉树

2.1 递归法

2.1.1 前序遍历(中左右)
class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        def traversal(root):
            if root==None:
                return None
            res.append(root.val)     # 中
            traversal(root.left)    # 左
            traversal(root.right)   # 右
        traversal(root)
        return res
2.1.2 中序遍历(左中右)
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        def traversal(root):
            if root == None:
                return None
            traversal(root.left)   # 左
            res.append(root.val)    # 中
            traversal(root.right)  # 右
        traversal(root)
        return res
2.1.3 后序遍历(左右中)
class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        def traversal(root):
            if root == None:
                return None
            traversal(root.left)  # 左
            traversal(root.right) # 右
            res.append(root.val)   # 中
        traversal(root)
        return res

2.2 迭代遍历【深度优先】

2.2.1 统一迭代法

2.2.1.1 前序遍历(中左右)
class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        stack = [root]
        while stack:
            cur_tree = stack.pop()
            if cur_tree:
            	# 遍历中结点且输出中结点,最早输出的
                res.append(cur_tree.val)
                # 右子树先进栈,所以后输出
                if cur_tree.right:
                    stack.append(cur_tree.right)
                # 左子树后进栈,所以先输出
                if cur_tree.left:
                    stack.append(cur_tree.left)
            # print(res)
        return res

还有另一种写法,与下面的中序遍历和后序遍历统一。相比于上面的写法,让中结点先出栈再进栈,并通过一个None标记,之后再出栈时不需要再次遍历,直接输出。

class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        res = []
        stack = [root]
        while stack:
            cur = stack.pop()
            if cur:
                # 右子树先进栈,所以最后输出
                if cur.right:
                    stack.append(cur.right)
                # 左子树后进栈,所以其次输出
                if cur.left:
                    stack.append(cur.left)
                # 中结点最后进栈,所以最先输出,增加一个None,表示该结点已经被遍历过,不需要再遍历了,之后直接出栈
                stack.append(cur)
                stack.append(None)
            else:
                rt = stack.pop()
                res.append(rt.val)
        return res
2.2.1.2 中序遍历(左中右)

首先要明确,遍历该节点需要将其出栈并把左右子树入栈,而处理节点只需要输出即可,由于中序遍历时,需要先遍历中节点才能遍历子结点,但是输出顺序上又不能遍历中结点的时候就把它“处理”,要优先输出左子树。因此采用这种方法把中结点先出栈再入栈,并标记起来,表示已经遍历过了,直接处理即可。在这里是将其入栈后,再入栈一个NULL,这样出栈时先遇到NULL,再遇到遍历过的中结点,就直接把中结点出栈并加到输出里,见else部分。
唯一需要注意的就是入栈顺序,要与左中右反过来。

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        res = []
        stack = [root]
        while stack:
            cur = stack.pop()
            if cur:
                if cur.right:      # 右
                    stack.append(cur.right)
                # cur_node 后面加了None,表示该节点已被遍历过(出过栈),后续直接输出
                stack.append(cur)  # 中
                stack.append(None) # 左
                if cur.left:
                    stack.append(cur.left)
            else:
                rt = stack.pop()
                res.append(rt.val)
        return res
2.2.1.2 后序遍历(左右中)
class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        res = []
        stack = [root]
        while stack:
            cur = stack.pop()
            if cur:
                stack.append(cur)  # 中
                stack.append(None)
                if cur.right:  
                    stack.append(cur.right)  # 右
                if cur.left:
                    stack.append(cur.left)   # 左
            else:
                rt = stack.pop()
                res.append(rt.val)
        return res

每次遍历一层结点,把该层所有子节点存在下一层的队列中,循环遍历直到所有层遍历完

2.2.2 模板迭代法

该方法是先把所有左结点都入栈,然后再一个一个出栈,如果出栈的结点有右子树,那么再用同样的方法把他的所有左节点全部入栈。

2.2.2.1 前序遍历(中左右)
class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        cur = root
        stack = []
        while stack or cur:
            # 把所有当前根结点的左子树(可以理解为根节点)入栈
            while cur:
                res.append(cur.val)  # 所有左子树的根结点入栈
                stack.append(cur)
                cur = cur.left     # 这里是左子树
            node = stack.pop()  # 出栈
            # 如果当前结点有右子树,cur指针就指向右子树,后续会把右子树的全部左子树入栈
            # if node and node.right:
            # 无论有没有右子树都可以用cur指向,即使是空,在下一轮循环中也会忽略
            cur = node.right
        return res
2.2.2.2 中序遍历(左中右)
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        cur = root
        res, stack = [], []
        while stack or cur:
            # 遍历所有左子树,并入栈
            while cur:
                stack.append(cur)
                cur = cur.left    # 这里是左子树
            # 取出栈顶元素
            node = stack.pop()
            res.append(node.val)   
            # 如果有右子树,cur指向右子树,后续会继续遍历该结点的所有左子树
            cur = node.right
        return res
2.2.2.3 后序遍历(左右中)

实际上是中右左,类似前序遍历的方式,最后再反转即可

class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        cur = root
        res, stack = [], []
        while stack or cur:
            while cur:
                res.append(cur.val)
                stack.append(cur)
                cur = cur.right  # 注意这里是右子树
            node = stack.pop()
            cur = node.left
        return res[::-1]

总体上感觉模板迭代法没有统一迭代法好理解。

2.3 层序遍历【宽度优先】

每次遍历一层结点,把该层所有子节点存在下一层的队列中,循环遍历直到所有层遍历完。比迭代法好理解一些。

class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if root is None:
            return []
        res = []
        # 使用deque比list更快,pop(0)的时间复杂度:deque是O(1),list是O(n)
        from collections import deque
        que = deque([root])
        while que:
            n = len(que)
            res_level = []
            # 只遍历当前层的结点,一共有n个
            for _ in range(n):
                cur = que.popleft()
                ##########################
                # 这步就是想要输出的东西,可以根据题意替换
                # res_level.append(cur.val)
                ##########################
                if cur.left:
                    que.append(cur.left)
                if cur.right:
                    que.append(cur.right)
            res.append(res_level)
        return res

3、二叉树的高度和深度

3.1 概念区分

首先需要分清二叉树的深度和高度。这里借用Carl哥的图片来举例:

深度:指从根节点到该节点的最长简单路径边的条数
高度:指从该节点到叶子节点的最长简单路径边的条数

简单说深度自顶向下,起点是根节点高度自底向上,起点是最底层的叶子节点

在这里插入图片描述

3.2 计算深度

我一般在计算深度时,通常采用层序遍历的方法,每遍历一层深度就+1,思路比较简单。

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if root is None:
            return 0
        from collections import deque
        que = deque([root])
        depth = 0
        while que:
            n = len(que)
            for _ in range(n):
                cur = que.popleft()
                if cur.left: que.append(cur.left)
                if cur.right: que.append(cur.right)
            # 每遍历一层,深度就加1
            depth += 1
        return depth

3.3 计算高度

高度计算一般采用递归法,从下层逐渐往上+1
一般用后序遍历,即左右中。
H e i g h t ( x ) = { 0 , x = = N U L L m a x ( H e i g h t ( x . l e f t ) , H e i g h t ( x . r i g h t ) ) + 1 , x ! = N U L L Height(x) = \begin{cases} 0, & x == NULL \\ max(Height(x.left),Height(x.right))+1, & x != NULL \\ \end{cases} Height(x)={0,max(Height(x.left),Height(x.right))+1,x==NULLx!=NULL

def getheight(root):
    if not root: return 0
    # 先递归左子树,求左子树高度
    leftheight = getheight(root.left)
    # 再递归右子树,求右子树高度
    rightheight = getheight(root.right)
    ####################################################################
    # 这里可以加上剪枝的东西
    # if leftheight<0 or rightheight<0 or abs(leftheight-rightheight)>1:
    #     return -1
    # else:
    ####################################################################
    	# 最后中节点的高度则为左右子树的最大值+1(自身这层)
    return max(leftheight,rightheight) + 1
rootheight = getheight(root)

4、深度优先搜索【递归法】

leetcode113.路径总和Ⅱ为例,具体介绍应该怎么写,以及注意事项。

递归三部曲:

  • 确定参数和返回值:中节点和targetSum-累加和的差,不需要返回值
  • 确定终止条件:遍历到叶子节点(没有子节点),如果满足累加和==targetSum,就把结果存下来。存的时候必须保存值,不是地址!存的时候必须保存值,不是地址!存的时候必须保存值,不是地址!
    必须要拷贝:stack.copy(),再保存。找了好久的错误,发现是这里有问题T_T
  • 确定单层递归逻辑:如果有左/右子树就继续遍历左/右子树

递归法需要注意的是:
如果搜索时候,函数的参数包括状态参数,比如累加和或者,那么就不需要回溯这部分状态了,因为在traversal的时候就已经记录了当前节点的状态,无论向下遍历还是向上回溯,这些状态量都与当前节点相关的;
反之,如果函数的参数不包括,这些状态以全局变量的形式存储,那么回溯的时候就要恢复到之前的状态,体现在出栈、累加和减子树的值。

Carl哥在代码随想录提到了回溯的隐藏,就与我上面说的有关,一开始不太理解,做了几道题之后才发现其中的奥妙所在~,借用Carl哥的图,举个例子。如果是包含了累加和的形式:def traversal(root,target),那么在不断遍历子树的过程中,每一个节点就对应了自己的状态。比如遍历根节点,他的状态时22,遍历到左子树4的时候,对应的状态为17。那么当我遍历完左子树后,想要遍历右子树的节点,右子树的节点的状态还是17,只与它本身的状态有关,而它的状态只取决于他的父节点。

【省流】:一个节点对应一个状态,回溯的时候不需要对这个状态进行修改,因为这个状态只取决于他的父节点,并且一直跟着该节点。

在这里插入图片描述

下面我提供了三个代码,分别对应没有状态参数,有一部分状态参数,包括所有状态参数。可以在traversal函数的参数看出区别。

# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        stack = []
        res = []
        self.sum = 0
        if not root:
            return res
        def traversal(root):
        	# 不包含任何状态参数,状态量以全局变量形式保存
            stack.append(root.val)
            self.sum += root.val
            if not root.left and not root.right and self.sum==targetSum:
                res.append(stack.copy())
            if root.left:
                traversal(root.left)
                # 栈和累加和都要回溯
                stack.pop()
                self.sum -= root.left.val
            if root.right:
                traversal(root.right)
                # 栈和累加和都要回溯
                stack.pop()
                self.sum -= root.right.val
                
        traversal(root)
        return res

# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        stack = []
        res = []
        if not root:
            return res
        def traversal(root,target):
        	# 包含累加和这个状态变量,栈需要回溯
            stack.append(root.val)
            target -= root.val
            if not root.left and not root.right and target==0:
                res.append(stack.copy())
            if root.left:
                traversal(root.left,target)
                # 回溯栈
                stack.pop()
            if root.right:
                traversal(root.right,target)
                # 回溯栈
                stack.pop()
                
        traversal(root,targetSum)
        return res
# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        stack = []
        res = []
        if not root:
            return res
        def traversal(root,target,st):
            # 因为每一个节点对应一个target和栈,所以这些值不需要回溯,只需要维持自己的状态就可以
            st.append(root.val)
            target -= root.val
            if not root.left and not root.right and target==0:
                res.append(st.copy())
            if root.left:
                traversal(root.left,target,st.copy())
            if root.right:
                traversal(root.right,target,st.copy())

        traversal(root,targetSum,stack)
        return res

相信理解了上面三种写法之后,你肯定可以掌握二叉树的递归写法!

5、重建二叉树

5.1 思路

根据中序与前序(或后序)遍历的结果构造二叉树。
回忆一遍遍历顺序:
中序:左/中/右
前序:中/左/右
后序:左/右/中

通过上面的遍历顺序可以发现,前序或者后序遍历可以很容易找到根节点,而中序遍历很容易分割左右子树

因此,构造二叉树的步骤就是:
(1)从前序/后序遍历中找到当前二叉树的根节点
(2)在中序遍历中找到根节点的下标,然后分割左右子树
(3)根据中序遍历的左子树的长度,分割出前序/后序遍历的左右子树
(.)剩下的就递归遍历左右子树咯

5.2 从中序与后序遍历序列构造二叉树

# 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 buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
        def rebuild(ino,posto):
            if not ino or not posto:
                return None
                
            # if len(posto)==1:
            #     return TreeNode(posto.pop())

            root = TreeNode(posto.pop())
            i = ino.index(root.val)
            in_left = ino[:i]
            in_right = ino[i+1:]

            n = len(in_left)
            post_left = posto[:n]
            post_right = posto[n:]
            
            root.left = rebuild(in_left, post_left)
            root.right = rebuild(in_right, post_right)
            
            return root

        root = rebuild(inorder, postorder)
        return root

5.3 从前序与中序遍历序列构造二叉树

# 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 buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:

        if not preorder:
            return None

        root = TreeNode(preorder[0])
        
        i = inorder.index(root.val)
        in_left = inorder[:i]
        in_right = inorder[i+1:]

        n = len(in_left)
        pre_left = preorder[1:1+n]
        pre_right = preorder[1+n:]

        root.left = self.buildTree(pre_left, in_left)
        root.right = self.buildTree(pre_right, in_right)

        return root

很抱歉上面两道题没有写成统一的形式T_T

6、二叉搜索树

6.1 性质

二叉搜索树是一个有序树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉搜索树

6.2 遍历

最佳方式是中序遍历左/中/右的遍历顺序恰好符合二叉搜索树的定义,因此遍历出来的结果一定是严格递增数组,利用这个有序数组我们就可以做很多事情啦!

def traversal(root):
    if not root:
        return True
    traversal(root.left)  # 左 - 小
    res.append(root.val)  # 中 - 中
    traversal(root.right) # 右 - 大
traversal(root)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值