算法训练营day15 110.平衡二叉树、257. 二叉树的所有路径、404.左叶子之和、222.完全二叉树的节点个数

        这是二叉树章节的第三篇博客了,插一句嘴,前面两篇博客的排版没有整理好就发了,后面也忘记修改了,发这篇博客之前已经修改好了,回到正题还是老样子,每道题都会有递归法和迭代法两种解题思路,今天尝试自己独立写出递归法,同时要深入了解递归和迭代,二叉树中的递归和迭代是很常用的,一定要掌握递归的算法和代码

  • 递归:函数直接或间接调用自身,每次调用时问题规模缩小,直到达到终止条件。通过函数调用栈实现,每次递归调用会在栈上分配新的局部变量,直到触发终止条件后逐层返回结果。
  • 迭代:使用循环(如 for、while)重复执行一段代码,逐步更新变量值直到满足终止条件。通过循环结构实现,通常使用计数器或状态变量控制循环次数,不需要额外的函数调用开销。

110.平衡二叉树

        高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

        这道题有个问题是我如何写出,包含true false判断和数值计算的返回值的函数呢?还是说写两个?看了答案发现自己还是代码写少了,可以返回 -1 来标记不符合平衡树的规则的节点,同时加入对于 -1 的判断。

递归法

        如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了,可以返回-1 来标记已经不符合平衡树的规则。分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。

# 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 isBalanced(self, root: Optional[TreeNode]) -> bool:
        if self.get_height(root) != -1:
            return True
        else:
            return False
    
    def get_height(self, root : TreeNode) -> int:
        if not root:
            return 0
        if (left_height := self.get_height(root.left)) == -1: # 增加对于-1的判断
            return -1 # 收到返回值之后如果是-1,就可以直接返回
        if (right_height := self.get_height(root.right) == -1):
            return -1
        # 要脑子清楚,前面是收到-1之后的处理
        # 这里还要补充对于-1的判断,高度差
        if abs(left_height - right_height) > 1:
            return -1
        else: # 这个是正常平衡二叉树
            height = 1 + max(left_height, right_height) # max应该怎么写,好好理解
            # 一步步向上搭积木
        return height
        # leftheight = get_height(root)
        # rightheight = get_height(root)
        # 没有写self. 敢于写max, 想最后叶子结点就好理解了
        # 我天, 我节点都写的是root, 我真是个nc啊

介绍——海象运算符:= 

:= 允许你在单个表达式中完成两件事:

  1. 赋值:将值赋给变量。
  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 isBalanced(self, root: Optional[TreeNode]) -> bool:
        st = []
        if root is None:
            return True
        st.append(root)
        while st:
            node = st.pop() # 栈, 这里仍然是后序遍历经过每一个节点
            if abs(self.getDepth(node.left) - self.getDepth(node.right)) > 1:
                return False
            if node.right:
                st.append(node.right)
            if node.left:
                st.append(node.left)
        return True

    def getDepth(self, cur):
        st = []
        if cur is not None:
            st.append(cur)
        depth = 0
        result = 0 
        # 这个位置和之前的最大深度存在差别
        # 这个不是根节点,不可以使用层序遍历
        # 但是可以把子节点置换为子树的根节点来求
        while st:
            node = st[-1]# 记住这个用法,和top一样
            if node is not None:
                st.pop() # 这个地方往下写一定要知道自己的遍历方法
                st.append(node)
                st.append(None)
                depth += 1 # 因为depth初始化为0, 所以每成功遍历一次 加1 记录深度
                if node.right:
                    st.append(node.right)
                if node.left:
                    st.append(node.left)# 模拟后序遍历, 左右中, 
                    # 只给最后的中间节点增加none标志
                    # 遍历顺序就是后序遍历的过程, 无重合节点
                    # 但是会因为添加none标志重新经历if部分
            else:
                # node = st.pop()# 这里不是none吗, 为什么要存储?
                st.pop()
                st.pop() # 把节点取出
                depth -= 1 #这个地方要回溯
            result = max(result, depth)# result记录见到过的最大深度
        return result
                

257. 二叉树的所有路径

        这道题涉及到回溯,第一次引出了回溯的概念,其实递归就会有回溯,我之前想着总是从根节点出发,确实有一些思考上的误区

 递归法+回溯

        需要一个持续更新的路径数组path来存储——根节点到每个节点的路径

# 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 binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        result = []
        path = []
        if not root:
            return result
        self.traversal(root, path, result)
        return result
        
    def traversal(self, cur, path, result):# 确认递归函数参数————当前节点、当前路径、路径列表
        path.append(cur.val) # 加入节点数值
        # 生成加入节点后的实时路径
        if not cur.left and not cur.right:# C++的话需要写for
            sPath = '->'.join(map(str, path)) # 对 iterable 中的每个元素应用 func 函数
            result.append(sPath)
            return 
        if cur.left:
            self.traversal(cur.left, path, result)
            path.pop()# 这个回溯很关键,涉及python中的值传递和引用传递的逻辑
        if cur.right:
            self.traversal(cur.right, path, result)
            path.pop()
    

值传递与引用传递

1. C++ 中的值传递 vs 引用传递

if (cur->left) traversal(cur->left, path + "->", result); 
// 回溯就隐藏在这里

if (cur->left) {
    path += "->";
    traversal(cur->left, path, result); // 左
}
// 没有回溯

关键在于类型定义,注意在函数定义的时候

void traversal(TreeNode* cur, string path, vector<string>& result) 

  • path 是string类型,每次都是复制赋值,通过值传递(复制)给递归函数。每次递归调用都会创建一个新的 path 副本,因此修改不会影响上层调用的 path
  • result 是 vector<string>& 类型,通过引用传递。所有递归调用共享同一个 result 对象,因此对其修改会反映到函数外部。

关键点:C++ 中,引用传递使用 & 符号显式声明,而值传递会复制对象。

2. Python 中的参数传递机制

Python 的参数传递是 "传递对象引用"(Call by Object Reference),但行为需要根据对象是否可变(Mutable)来区分:

  • 不可变对象(如 intstrtuple):传递后无法修改原始对象,类似值传递。
  • 可变对象(如 listdictset):传递后可以修改原始对象,类似引用传递。

示例代码

def modify_value(x):    # x是不可变对象的引用
    x = x + 1           # 创建新对象,不影响原始x

def modify_list(lst):   # lst是可变对象的引用
    lst.append(4)       # 修改原始列表

a = 10
modify_value(a)
print(a)  # 输出: 10(未改变)

my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)  # 输出: [1, 2, 3, 4](已改变)

迭代法

        使用了两个栈,每个节点都存放了相应的路径结果

# 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 binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        stack, path_st, result = [root], [str(root.val)], []
        while stack:
            cur = stack.pop()
            path = path_st.pop()
            # 判断是否是叶子结点, 添加路径到结果中
            # 这里有两个栈 stack负责模拟栈的遍历
            # path_st负责存放当前节点的路径, 供后续便利
            if not cur.left and not cur.right:
                result.append(path)
            if cur.right:
                stack.append(cur.right)
                path_st.append(path + '->' + str(cur.right.val))
            if cur.left:
                stack.append(cur.left)
                path_st.append(path + '->' + str(cur.left.val))
        return result

404.左叶子之和

        首先要注意是判断左叶子,不是二叉树左侧节点,所以层序遍历不是很合适。

        同时,判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子——如果该节点的左节点不为空,该节点的左节点的左节点为空,该节点的左节点的右节点为空,则找到了一个左叶子

递归法

# 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 sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
        if root is None:
            return 0
        # 这里是一个剪枝的操作
        # 只有遍历父节点才能判断子节点是不是左叶子
        # 如果当前遍历的节点是叶子节点, 那其左叶子也必定是0, 直接终止
        if root.left is None and root.right is None:
            return 0
        # 计算左子树和右子树的左叶子的和
        # 注意遍历顺序————后序
        leftvalue = self.sumOfLeftLeaves(root.left)
        if root.left and not root.left.left and not root.left.right:
            leftvalue = root.left.val
        rightvalue = self.sumOfLeftLeaves(root.right)
        sum_val = leftvalue + rightvalue # 这个位置才是最终计算, 属于中间节点
        # 后序遍历的选取需要理解
        return sum_val
        

迭代法

        核心还是在于使用栈来模拟遍历过程,并检查遍历过程中的每一个左叶子节点

# 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 sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
        if root is None:
            return 0
        st = [root]# 模拟遍历过程
        result = 0
        while st:
            node = st.pop()
            if node.left and node.left.left is None and node.left.right is None:
                result += node.left.val # 计算便利到目前左叶子节点的和
            if node.right:
                st.append(node.right)
            if node.left:
                st.append(node.left)
        return result

222.完全二叉树的节点个数

        概念性质:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1)  个节点。

注:图片引用自《代码随想录》

        完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。

  • 对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。
  • 对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。

        这里关键在于如何去判断一个左子树或者右子树是不是满二叉树呢?——在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树。

递归和迭代(以普通二叉树为例)

# 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 countNodes(self, root: Optional[TreeNode]) -> int:
        queue = collections.deque()
        if root:
            queue.append(root) # 这个地方是必要的
        # 当 root 为 None 时, 队列中会包含一个 None 元素。
        # 在后续循环中, 代码尝试处理这个 None 节点, 导致 AttributeError
        # AttributeError表示当程序尝试访问对象不具备的属性或方法时触发的错误
        result = 0
        while queue:
            size = len(queue)
            for i in range(size):
                code = queue.popleft()# 这个取出节点的位置要明白
                result += 1
                if code.left:
                    queue.append(code.left)
                if code.right:
                    queue.append(code.right)
        return result
    

    '''
    def countNodes(self, root: Optional[TreeNode]) -> int:
       return self.getnodesnum(root)
    
    def getnodesnum(self, cur):
        if cur is None:
            return 0      
        leftvalue = self.getnodesnum(cur.left)
        rightvalue = self.getnodesnum(cur.right)
        return leftvalue + rightvalue + 1 # 左右中 中间才是最后一个顺序
        # 所以这个位置不需要判断子节点是否为空, 因为前面有空返回的逻辑
        # +1 的理解
    '''
        

递归和迭代(以完全二叉树为例)

        分解2+1(中间节点 + 左右子树节点数),递归算法这种把大问题抽象成一个个重复操作的子片段的能力很重要:

# 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 countNodes(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        left = root.left
        right = root.right
        leftDepth = 0
        rightDepth = 0
        while left:
            left = left.left
            leftDepth += 1
        while right:
            right = right.right
            rightDepth += 1
        if leftDepth == rightDepth:# 判断子树是否为完全二叉树
            return (2 << leftDepth) - 1
        return self.countNodes(root.left) + self.countNodes(root.right) + 1
        # 理解这个加1, 对于中间节点的处理
        # 理解这种写在return中的递归写法

写在最后

        今天博客偷懒了,感觉这个二叉树很多时候还是不太熟练,不过写多了自然就熟悉了吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值