代码随想录算法训练营|Day 15

Day 15

第六章 二叉树part03

迭代法,大家可以直接过,二刷有精力的时候 再去掌握迭代法。

110.平衡二叉树 (优先掌握递归)

再一次涉及到,什么是高度,什么是深度,可以巩固一下。

题目链接/文章讲解/视频讲解:https://programmercarl.com/0110.平衡二叉树.html

img

平衡二叉树的定义:该树所有节点的左右子树的高度相差不超过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 not root:
            return True
        def countHeight(node):
            if not node:
                return 0
            return 1+ max(countHeight(node.left), countHeight(node.right))
        return abs(countHeight(root.left)-countHeight(root.right)) <= 1

错误原因

你的实现只在根节点(root)处判断了左右子树高度差,却没有递归地去保证所有子节点也满足“平衡二叉树(balanced binary tree)”的定义:

对于每一个节点,其左子树高度(left subtree height)和右子树高度(right subtree height)之差都不能超过 1,且它的左右子树也都必须是平衡二叉树。

你的代码缺少对左右子树 isBalanced(root.left)isBalanced(root.right) 的检查,只做了一次 countHeight 的比较,深层不平衡的子树就会漏掉。

另外,每次 countHeight 都是一次完整的子树遍历,导致最坏 O(n²) 的时间复杂度,在节点多时也容易超时(TLE)。

下面给出一个一趟递归(one‑pass traversal)就能同时计算高度(height)和判断平衡(balance)的写法:

class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        def dfs(node):
            # 返回 node 的高度;若发现失衡则返回 -1 作为信号
            if not node:
                return 0
            lh = dfs(node.left)
            if lh == -1:        # 左子树已经失衡
                return -1
            rh = dfs(node.right)
            if rh == -1:        # 右子树已经失衡
                return -1
            if abs(lh - rh) > 1:  # 当前节点失衡
                return -1
            return max(lh, rh) + 1

        return dfs(root) != -1

这样做能保证:

  1. 每个节点都检查了高度差 <= 1;
  2. 一旦发现任何子树失衡,就会向上“短路”(short‑circuit)返回 -1;
  3. 时间复杂度 O(n),只遍历一次。

正确递归

class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        return self.get_hight(root) != -1
    def get_hight(self, node):
        if not node:
            return 0
        left = self.get_hight(node.left)
        right = self.get_hight(node.right)
        if left == -1 or right == -1 or abs(left - right) > 1:
            return -1
        return max(left, right) + 1

迭代法

class Solution:
    def getDepth(self, cur):
        st = []
        if cur is not None:
            st.append(cur)
        depth = 0
        result = 0
        while st:
            node = st[-1]
            if node is not None:
                st.pop()
                st.append(node)                           # 中
                st.append(None)
                depth += 1
                if node.right:
                    st.append(node.right)                 # 右
                if node.left:
                    st.append(node.left)                   # 左

            else:               
                node = st.pop()
                st.pop()
                depth -= 1
            result = max(result, depth)
        return result

    def isBalanced(self, root):
        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

这段代码用的是一种“显式栈(stack)+标记(marker)”的方法,来分别完成:

  1. 计算子树高度getDepth(cur)
  2. 遍历整棵树、对每个节点做高度差检查isBalanced(root)

一、getDepth(cur) 如何工作

def getDepth(self, cur):
    st = []
    if cur is not None:
        st.append(cur)
    depth = 0
    result = 0
    while st:
        node = st[-1]
        if node is not None:
            # ——“进入”这个节点 node
            st.pop()
            st.append(node)   # 先把 node 重新压回去(用于后面回溯时匹配)
            st.append(None)   # 然后压一个 None 作标记
            depth += 1        # 进入一层,深度 +1

            # 按“先右后左”顺序入栈,确保下次循环中先处理左子树
            if node.right:
                st.append(node.right)
            if node.left:
                st.append(node.left)

        else:
            # ——“回溯”:碰到 None,就弹出标记和对应的节点,深度 -1
            st.pop()    # 弹出 None
            st.pop()    # 弹出上面对应的 node
            depth -= 1

        # 不断更新我们到过的最大深度
        result = max(result, depth)
    return result
  • st 里同时存放 TreeNodeNone 标记

    • 栈顶是 TreeNode 时,表示「还没进到它的左右子树」,我们就把它和一个 None 重新压回去,并 depth += 1
    • 栈顶是 None 时,说明它对应的那个节点的左右子树都遍历完了,我们再把 depth -= 1,相当于递归中的回溯。
  • depth 始终记录当前栈顶路径的长度,result 记录“遍历过程中见过的最大 depth”,最终就是这棵子树的高度。


二、isBalanced(root) 如何利用它

def isBalanced(self, root):
    st = []
    if root is None:
        return True
    st.append(root)
    while st:
        node = st.pop()
        # 对当前 node,分别算左右子树高度并比较
        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
  • 用一个栈做前序遍历:每碰到一个 node,都去算它左子树和右子树的高度差;
  • 只要有任何一个节点的高度差超过 1,就判为不平衡,提前返回 False
  • 全部节点都检查完才返回 True

三、优缺点

  • 优点

    • 代码逻辑非常直观:先写一个通用的“算高度”函数,再对每个节点“算左右高度差”。
    • 不用递归函数调用栈,改成了手动维护的 st
  • 缺点

    • 效率较低:每访问一个节点都要重新跑一遍 getDepth,最坏 O(n²)。
    • 空间上,栈里可能最多存 O(h) 个元素(h 是树高),跟递归栈差不多。

实际上,更高效的做法是一次 DFS里边“自底向上”同时算高度和判断平衡性(时间 O(n))。但这个解法对于中小规模的树也完全能通过,只是当节点很多时会比较慢。

depth 在这个算法里,其实就是模拟递归时“调用栈”的深度:

  1. “进入”(enter)节点时,我们相当于在递归里做了一个函数调用,它所在的那一层深度就要 depth += 1
  2. “回溯”(leave)节点时,就相当于从那个递归调用里返回,你要把深度退回去,也就是 depth -= 1

具体到代码流程:

  • 当你第一次看到一个非 Nonenode,就把它和一个 None 标记一起压回栈,并做 depth += 1,表示你“走”到它这一层了。
  • 紧接着又把它的左右子节点(如果有)压进去,继续往更深层走。
  • 直到子节点都处理完,你会再次遇到那个 None,这时弹出 None 以及它对应的 node,就意味着“我已经把这个节点的左右子树都走完了”,要从这一层回到上一层,就要 depth -= 1

如果不做 depth -= 1depth 就只会一直累加,从来不会减少,那得到的最大深度也就不对了。

简单说,每次“进一层”就 +1,每次“出一层”就 −1,才能正确追踪当前遍历路径的长度,最后 result = max(result, depth) 才是真正的树高。

下面我们用一棵小树来演示 getDepthdepth += 1depth -= 1 的过程,假设树结构如下:

      1
     / \
    2   3
   /
  4

也就是:

  • 根节点是 1
  • 1 的左子节点是 2,右子节点是 3
  • 2 的左子节点是 4

调用 getDepth(1) 时,栈和 depthresult 的变化(只列前几步)大致如下(栈顶在右侧):

步骤 栈 内容 操作 depth result
初始化 [1] depth=0,result=0 0 0
1 [1] 看到 1(非 None)→ pop→ push(1)、push(None),depth=1;再按 “右→左” 入栈 3,2 1 max(0,1)=1
[1, None, 3, 2]
2 [1, None, 3, 2] 看到 2→ pop→ push(2)、push(None),depth=2;再按“右→左”入栈(2 无右,左是 4) 2 max(1,2)=2
[1, None, 3, 2, None, 4]
3 [1, None, 3, 2, None, 4] 看到 4→ pop→ push(4)、push(None),depth=3;(4 无子) 3 max(2,3)=3
[1, None, 3, 2, None, 4, None]
4 ...None 在栈顶 看到 None→ pop(None)、pop(对应的 4),相当于“回溯”出 4,depth=2 2 3
5 接着又碰到上一个 None(对应 2) 再次 pop 标记 和 2,depth=1 1 3
最后整个栈空了,返回 result=3 0 3
  • 每次遇到节点(非 None),都相当于“递归进一层”,先 depth += 1
  • 每次遇到 None 标记,都相当于“递归出一层”,再 depth -= 1
  • result 记录遍历过程中 depth 的最大值,最终就是这棵树的高度(本例中高度 = 3)。

这样,depth -= 1 就是为了在“子树遍历完毕、回到父节点”时,正确地把当前深度退回去。

迭代法精简版

class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        if not root:
            return True

        height_map = {}
        stack = [root]
        while stack:
            node = stack.pop()
            if node:
                stack.append(node)              # 中
                stack.append(None)
                # 采用数组进行迭代,先将右节点加入,保证左节点能够先出栈
                if node.right:                  # 右
                    stack.append(node.right)
                if node.left:                   # 左
                    stack.append(node.left)
            else:
                real_node = stack.pop()
                left, right = height_map.get(real_node.left, 0), height_map.get(real_node.right, 0)
                if abs(left - right) > 1:
                    return False
                height_map[real_node] = 1 + max(left, right)
        return True

这个版本用的是 迭代的后序遍历 + 备忘(memoization)自底向上 计算每个节点的高度并顺手检查平衡性。流程可以分两部分来看:


1. 数据结构

  • height_map: Dict[TreeNode, int]
    存放每个节点处理完以后算出的高度:

    height_map[node] = 1 + max(height_map[left], height_map[right])

  • stack: List[TreeNode or None]
    用来模拟递归调用栈,里面既放 TreeNode,也放 None 作为“标记”(marker)。


2. 遍历逻辑

stack = [root]
while stack:
    node = stack.pop()
    if node:  # 正常节点,等同于“递归进入”时的前序逻辑
        stack.append(node)        # —— 中
        stack.append(None)        # —— 标记:之后遇到它就知道要“后序”处理 node  
        if node.right:            # —— 右
            stack.append(node.right)
        if node.left:             # —— 左
            stack.append(node.left)
    else:
        # 遇到 None,就进行“后序”操作:pop 出真实节点 real_node
        real_node = stack.pop()
        left_h  = height_map.get(real_node.left,  0)
        right_h = height_map.get(real_node.right, 0)

        # 平衡性检查
        if abs(left_h - right_h) > 1:
            return False

        # 记录 real_node 的高度
        height_map[real_node] = 1 + max(left_h, right_h)
  • 遇到一个 TreeNode

    1. 先把它再压回去(stack.append(node)
    2. 压一个 None 标记,表示“它的子树出栈完以后要回来处理它”
    3. 再按「先右后左」的顺序把左右子节点压进来,这样出栈时就能先遍历左子树、再右子树
  • 遇到一个 None

    • 说明对应的 TreeNode 的左右子树都已经“后序”遍历完了,
    • 这时 stack.pop() 再拿回那个 TreeNode(我们叫它 real_node),
    • 就可以从 height_map 里拿到它左右子树的高度,先做 abs(left_h - right_h) ≤ 1 的平衡性校验,
    • 校验过后,把 real_node 的高度算出来 1 + max(left_h, right_h) 存到 height_map

3. 优劣对比

  • 优点

    • 一次遍历 就完成:既算高度又判平衡,时间 O(n)。
    • 不会有重复的 getDepth 调用,也不用递归,适合节点多时避免递归深度问题。
  • 缺点

    • 需要额外的 height_map 存储每个节点的高度,空间 O(n)。
    • 代码稍微复杂一些,要理解「None 标记」和「先右后左」的入栈顺序。

总的来说,这是把“递归后序遍历+备忘”改写成了显式栈迭代的版本,效率和递归版一样都是 O(n),但避免了函数调用开销,也很利于定制化控制遍历顺序。

257. 二叉树的所有路径 (优先掌握递归)

这是大家第一次接触到回溯的过程, 我在视频里重点讲解了 本题为什么要有回溯,已经回溯的过程。

如果对回溯 似懂非懂,没关系, 可以先有个印象。

题目链接/文章讲解/视频讲解:https://programmercarl.com/0257.二叉树的所有路径.html

img

递归 到路径终点收割

# 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]:
        #确定是前序
        #中左右
        if not root:
            return []
        res = []
        def dfs(node, res, path):
            if not node.left and not node.right:
                res.append(path)
            if node.left:
                dfs(node.left, res, path+"->"+str(node.left.val))
            if node.right:
                dfs(node.right, res, path+"->"+str(node.right.val))
        dfs(root, res, str(root.val))
        return res

回溯和递归相辅相成,只要有递归就一定有回溯

迭代法

class Solution:

    def binaryTreePaths(self, root: TreeNode) -> List[str]:
        # 题目中节点数至少为1
        stack, path_st, result = [root], [str(root.val)], []

        while stack:
            cur = stack.pop()
            path = path_st.pop()
            # 如果当前节点为叶子节点,添加路径到结果中
            if not (cur.left or 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

这个解法用的是 显式栈(stack)+ 同步路径栈(path_st) 来做一次深度优先搜索(DFS),在遍历时 边走边拼接 从根到当前节点的字符串,遇到叶子节点就把整条路径存到结果里。具体流程如下:

  1. 初始化

    stack   = [root]              # 存节点
    path_st = [str(root.val)]     # 存对应节点的“到这里为止”的路径字符串
    result  = []                  # 最终结果
    
    • stack[i]path_st[i] 保持一一对应。
  2. 循环遍历

    while stack:
        cur  = stack.pop()
        path = path_st.pop()
        ...
    

    每次从栈顶取出一个节点 cur,以及它对应的路径 path

  3. 遇到叶子节点

    if not (cur.left or cur.right):
        result.append(path)
    

    如果 cur 既没有左子树也没有右子树,就把当前完整的 path(形如 "1->2->5")加入 result

  4. 压入子节点,继续“走深”
    为保证「先左子树后右子树」的顺序(虽然结果顺序通常不强制,但这也是典型写法),我们先把右子节点压栈,再把左子节点压栈

    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))
    
    • 对于每个子节点,都在原来的 path 后面加上 "->子节点值",保证 path_st 一直记录“从根到这个子节点”的完整路径。
  5. 结束
    stack 为空,表示所有从根出发的路径都被探索过,result 就包含了所有根到叶子的路径。


举例演示

以这棵树为例:

    1
   / \
  2   3
   \
    5
  • 初始

    stack   = [1]
    path_st = ["1"]
    result  = []
    
  • 弹出 1

    cur = 1, path = "1"
    1 不是叶子,压:
      右节点 3 → stack.append(3), path_st.append("1->3")
      左节点 2 → stack.append(2), path_st.append("1->2")
    stack   = [3, 2]
    path_st = ["1->3", "1->2"]
    
  • 弹出 2

    cur = 2, path = "1->2"
    2 不是叶子,只有右子 5,压:
      右节点 5 → stack.append(5), path_st.append("1->2->5")
    stack   = [3, 5]
    path_st = ["1->3", "1->2->5"]
    
  • 弹出 5

    cur = 5, path = "1->2->5"
    5 是叶子 → result.append("1->2->5")
    stack   = [3]
    path_st = ["1->3"]
    result  = ["1->2->5"]
    
  • 弹出 3

    cur = 3, path = "1->3"
    3 是叶子 → result.append("1->3")
    stack   = []
    path_st = []
    result  = ["1->2->5", "1->3"]
    

结束,返回 ["1->2->5", "1->3"]


复杂度

  • 时间:O(n·h)
    每个节点进出栈一次,拼接字符串需要 O(h)(h 是树高),最坏退化为 O(n²);
  • 空间:O(n·h)
    栈和路径栈各占 O(n),每条路径字符串占 O(h);

这个迭代写法和递归版的思路完全等价,优点是不依赖系统递归栈,方便掌控遍历顺序。

递归法+回溯

# Definition for a binary tree node.
class Solution:
    def traversal(self, cur, path, result):
        path.append(cur.val)  # 中
        if not cur.left and not cur.right:  # 到达叶子节点
            sPath = '->'.join(map(str, path))
            result.append(sPath)
            return
        if cur.left:  # 左
            self.traversal(cur.left, path, result)
            path.pop()  # 回溯
        if cur.right:  # 右
            self.traversal(cur.right, path, result)
            path.pop()  # 回溯

    def binaryTreePaths(self, root):
        result = []
        path = []
        if not root:
            return result
        self.traversal(root, path, result)
        return result

这种写法是典型的「递归 DFS + 回溯」——用一条可变的 path 列表自顶向下维护当前遍历到的节点值,遇到叶子就把它拼成一条完整路径,回退时再把多加的节点去掉。具体流程:

  1. 进入节点(pre‑order “中”)

    path.append(cur.val)
    

    把当前节点值加到 path 末尾。

  2. 到达叶子

    if not cur.left and not cur.right:
        sPath = '->'.join(map(str, path))
        result.append(sPath)
        return
    

    如果左右子都空,就把 path"->" 拼成字符串,存到 result,然后直接 return 不再深入。

  3. 递归左子树(“左”)

    if cur.left:
        self.traversal(cur.left, path, result)
        path.pop()   # 回溯:把刚才加的 cur.left.val 移除
    
  4. 递归右子树(“右”)

    if cur.right:
        self.traversal(cur.right, path, result)
        path.pop()   # 回溯:把刚才加的 cur.right.val 移除
    
  5. 初始调用
    binaryTreePaths 里先判断空树,然后

    self.traversal(root, [], result)
    

    最终 result 就包含了所有从根到叶子的路径字符串。


关键点

  • path 里始终保存「从根走到当前节点」的值序列。
  • 每次深入一个子节点,就 append回退到父节点时,就 pop,保证下一条分支的 path 正确。
  • 时间复杂度 O(n·h),其中 h 是树高(因为每到叶子要做一次 join);空间复杂度 O(h) 用于递归栈和 path 列表。

我的递归法不体现回溯的原因:

这段代码也是典型的“递归 DFS”(深度优先搜索)+“前序遍历”(pre‑order: 中→左→右)来 边走边拼接 根到当前节点的路径字符串,一旦到叶子节点就把这条路径加入结果集。和你之前看到的「可变列表+显式回溯」不同,这里利用了 Python 字符串不可变(immutable) 的特性:每次 path + '->'+… 都会返回一个全新的字符串,不会污染父分支的 path,所以根本不需要手动 pop() 回溯。


一、代码结构和流程

def binaryTreePaths(self, root):
    if not root:
        return []
    res = []

    # dfs(node, res, path):
    #   node: 当前节点
    #   path: 到 node 为止的整条路径字符串
    def dfs(node, res, path):
        # 1. 如果是叶子节点,直接把 path 加入 res
        if not node.left and not node.right:
            res.append(path)

        # 2. 前序“中”已经做了,就是 path 参数
        #    下面先“左”、再“右”
        if node.left:
            # 注意:path+"->"+… 产生新字符串,不影响原来的 path
            dfs(node.left, res, path + "->" + str(node.left.val))
        if node.right:
            dfs(node.right, res, path + "->" + str(node.right.val))

    # 从根开始,初始路径就是根节点值本身
    dfs(root, res, str(root.val))
    return res
  • 先序含义(中→左→右)

    • “中” 是把当前 path 当成已经到达 node 的路径;
    • 然后依次递归左子树和右子树。
  • 叶子检测

    if not node.left and not node.right:
        res.append(path)
    

    只在真正到叶子(leaf)时,才把当前完整路径写入 res

  • 字符串不可变——无需回溯
    每次调用

    dfs(child, res, path + "->" + str(child.val))
    

    path + "->"+… 都会返回一个 的字符串实例,父调用里的 path 保持不变。
    这样,不同分支互不干扰,也不用像操作列表那样在递归返回后再 pop()


二、演示一个简单例子

    1
   / \
  2   3
   \
    5
  1. dfs(1, [], "1")

    • 1 不是叶子
    • 先左:dfs(2, [], "1->2")
  2. dfs(2, [], "1->2")

    • 2 不是叶子(有右子)
    • 左子不存在,跳过
    • 右子:dfs(5, [], "1->2->5")
  3. dfs(5, [], "1->2->5")

    • 5 是叶子 → res.append("1->2->5")
    • 返回上层
  4. 回到 dfs(1, ["1->2->5"], "1"),接着处理右子:

    • dfs(3, ["1->2->5"], "1->3")
  5. dfs(3, ["1->2->5"], "1->3")

    • 3 是叶子 → res.append("1->3")

最终 res = ["1->2->5", "1->3"],正是所有从根到叶子的路径。


三、复杂度

  • 时间复杂度 O(n·h):

    • n 个节点,每个节点做一次字符串拼接,拼接花费 O(h)(h 是树高)。
  • 空间复杂度 O(h):

    • 递归栈最深 h 层;每层维护一个长度 O(h) 的 path 临时字符串。

这种写法既清晰又不易出错,适合大多数二叉树「根到叶子路径」题目。

404.左叶子之和 (优先掌握递归)

其实本题有点文字游戏,搞清楚什么是左叶子,剩下的就是二叉树的基本操作。

题目链接/文章讲解/视频讲解:https://programmercarl.com/0404.左叶子之和.html

注意:只有当前遍历的节点是父节点,才能判断其子节点是不是左叶子

img

递归 后序

# 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):
        if root is None:
            return 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):
        if root is None:
            return 0
        leftValue = 0
        if root.left is not None and root.left.left is None and root.left.right is None:
            leftValue = root.left.val
        return leftValue + self.sumOfLeftLeaves(root.left) + self.sumOfLeftLeaves(root.right)

迭代法

# 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):
        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.完全二叉树的节点个数(优先掌握递归)

需要了解,普通二叉树 怎么求,完全二叉树又怎么求

题目链接/文章讲解/视频讲解:https://programmercarl.com/0222.完全二叉树的节点个数.html

普适的递归法

# 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
        return 1 + self.countNodes(root.left) + self.countNodes(root.right) 

这种属于后序的写法,因为左右节点的信息收集完后返回上一层的父节点
我的理解:若是后序,信息需要返回父节点,那么递归的时候需要return somthing,
而从上开始往下遍历,像是前序那样子,就是到空节点以后直接return,有额外的变量,如
max_depth或result这种变量去记录和更新遍历过程中的信息

也可以用层序遍历

import collections
class Solution:
    def countNodes(self, root: TreeNode) -> int:
        queue = collections.deque()
        if root:
            queue.append(root)
        result = 0
        while queue:
            size = len(queue)
            for i in range(size):
                node = queue.popleft()
                result += 1 #记录节点数量
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        return result

img

img

img

img

利用完全二叉树的特性

写法一

class Solution:
    def countNodes(self, root: TreeNode) -> int:
        if not root:
            return 0
        left = root.left
        right = root.right
        leftDepth = 0 #这里初始为0是有目的的,为了下面求指数方便
        rightDepth = 0
        while left: #求左子树深度
            left = left.left
            leftDepth += 1
        while right: #求右子树深度
            right = right.right
            rightDepth += 1
        if leftDepth == rightDepth:
            return (2 << leftDepth) - 1 #注意(2<<1) 相当于2^2,所以leftDepth初始为0
        return self.countNodes(root.left) + self.countNodes(root.right) + 1

leftDepth+1才是深度
满二叉树节点个数计算公式: 2^树深度-1

在 Python 里,<< 是按位(bitwise)左移运算符。

  • 表达式 x << y 的含义是把整数 x 的二进制向左移动 y 位,空出来的低位补 0。
  • 从算术意义上看,这相当于 x * (2**y)

所以

2 << leftDepth

等价于

2 * (2**leftDepth)
# 也就是 2**(leftDepth + 1)

举几个例子帮助理解:

leftDepth = 0
2 << 0   # = 2 * (2**0) = 2 * 1 = 2    (也即 2**1)

leftDepth = 1
2 << 1   # = 2 * (2**1) = 2 * 2 = 4    (也即 2**2)

leftDepth = 2
2 << 2   # = 2 * (2**2) = 2 * 4 = 8    (也即 2**3)

在你的代码里写的是

(2 << leftDepth) - 1

就相当于

2**(leftDepth + 1) - 1

这正好是节点全满的完美二叉树在高度为 leftDepth+1 时的节点总数。

写法二

class Solution: # 利用完全二叉树特性
    def countNodes(self, root: TreeNode) -> int:
        if not root: return 0
        count = 1
        left = root.left; right = root.right
        while left and right:
            count+=1
            left = left.left; right = right.right
        if not left and not right: # 如果同时到底说明是满二叉树,反之则不是
            return 2**count-1
        return 1+self.countNodes(root.left)+self.countNodes(root.right)  

写法三

class Solution: # 利用完全二叉树特性
    def countNodes(self, root: TreeNode) -> int:
        if not root: return 0
        count = 0
        left = root.left; right = root.right
        while left and right:
            count+=1
            left = left.left; right = right.right
        if not left and not right: # 如果同时到底说明是满二叉树,反之则不是
            return (2<<count)-1
        return 1+self.countNodes(root.left)+self.countNodes(root.right)  

写法二三的区别:count从1还是0开始
从1开始,最后count为树深度
从0开始,count+1才是树的深度
2<<count,代表2向左移动两位,相当于2^(count+1)

posted @ 2025-07-17 22:19  ForeverEver333  阅读(22)  评论(0)    收藏  举报