全栈开发知识库

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

题目

给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。

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

示例1:

Image

  • 输入:root = [1,2,3,4,5,6]
  • 输出:6

示例 2:

  • 输入:root = []
  • 输出:0

示例 3:

  • 输入:root = [1]
  • 输出:1

提示:

  • 树中节点的数目范围是[0, 5 * 104]
  • 0 <= Node.val <= 5 * 104
  • 题目数据保证输入的树是 完全二叉树

进阶:遍历树来统计节点是一种时间复杂度为 O(n) 的简单解决方案。你可以设计一个更快的算法吗?

思路

递归遍历每个节点,总数 = 1 + 左子树节点数 + 右子树节点数。

// 暴力 O(n),完全没利用完全二叉树性质,面试不加分
fun countNodes(root: TreeNode?): Int {
    if (root == null) return 0
    return 1 + countNodes(root.left) + countNodes(root.right)
}

进阶:

第 1 步:先想一个特殊情况——满二叉树

如果一棵树是满二叉树(每一层都填满),那节点数根本不用数,有公式:

层数 d 的满二叉树,节点数 = 2^d - 1

比如 3 层的满二叉树:2³ - 1 = 7 个节点。

怎么判断一棵树是满的? 这里有个特别巧的技巧:

  • 从根一路向左走到底,记路径上的节点数为 ld
  • 从根一路向右走到底,记路径上的节点数为 rd

如果 ld == rd,这棵树就是满的。

为什么这个判断是对的?因为在完全二叉树里,最左边那条路径一定是最深的。如果最右边那条路径也一样深,说明最后一层从最左排到了最 右,一个不缺,那就是满的。

于是思路就出来了:

▎ 递归。到每个节点先测一下 ld 和 rd: ▎ - 相等 → 这块是满树,直接套公式 2^ld - 1 返回,不再往下走 ▎ - 不相等 → 老实拆成 1 + 左子树 + 右子树,递归处理

第 3 步:为什么这样就变快了

关键在于:完全二叉树的任何一个节点,它的左右两个子树中,至少有一个是满二叉树。

想象最后一层的"断点"(最右边那个节点的位置)。这个断点只会落在左子树或右子树的其中一边,另一边必然是完整无缺的满树,会被公式 秒算掉。

所以递归实际上只沿着"断点所在的那条路"往下走,路径长度是 O(log n);而每个节点上测 ld、rd 又要 O(log n)。

总复杂度 O(log² n),比 O(n) 快得多。

  /**
   * 二叉树节点定义(LeetCode 已提供,这里为了完整性列出)
   */
  class TreeNode(var `val`: Int) {
      var left: TreeNode? = null
      var right: TreeNode? = null
  }

  class Solution {

      /**
       * 统计完全二叉树的节点个数
       * 核心思想:满二叉树可以用公式 2^d - 1 直接秒算,
       *          所以递归时先判断"当前这块是不是满的",是就套公式,不是才继续拆。
       *
       * 时间复杂度 O(log²n),空间复杂度 O(log n)(递归栈)
       */
      fun countNodes(root: TreeNode?): Int {
          // 空树,0 个节点
          if (root == null) return 0

          // ---- 第 1 步:从当前节点一路向左,量出左侧路径的节点数 ----
          var leftNode = root.left
          var leftDepth = 1              // 当前节点自己算 1 层
          while (leftNode != null) {
              leftDepth++
              leftNode = leftNode.left   // 只往左拐
          }

          // ---- 第 2 步:从当前节点一路向右,量出右侧路径的节点数 ----
          var rightNode = root.right
          var rightDepth = 1
          while (rightNode != null) {
              rightDepth++
              rightNode = rightNode.right // 只往右拐
          }

          // ---- 第 3 步:两侧一样深 → 这是一棵满二叉树,公式直接出结果 ----
          // (1 shl d) 就是 2^d,位运算比 Math.pow 更快也不会有浮点误差
          if (leftDepth == rightDepth) {
              return (1 shl leftDepth) - 1
          }

          // ---- 第 4 步:不是满树,老实递归左右子树 ----
          // 注意:这两个子树里必有一个是满的,会在下一层递归被公式秒算掉,
          //      所以真正深入递归的只有一条路,整体不会退化成 O(n)
          return 1 + countNodes(root.left) + countNodes(root.right)
      }
  }

复杂度

  • 时间复杂度: O(log² n)
  • 空间复杂度: O(log n)


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记