【LeetCode刷题日记】222.极速计算完全二叉树节点数:O(log²n)算法揭秘

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:

大家好,我是代码不加冰,今天给大家继续带来每日的刷题笔记。

摘要:

本文探讨了计算完全二叉树节点数量的高效算法。传统遍历方法时间复杂度为O(n),而本文提出的优化算法利用完全二叉树特性,通过比较左右子树高度来判断是否为满二叉树,从而直接计算部分节点数。当左右高度相等时,左子树为满二叉树;否则右子树为满二叉树。满二叉树的节点数可用公式2^height-1计算。该算法通过递归处理非满子树,将时间复杂度降至O(log²n)。文中详细解析了算法执行过程,并通过示例演示了如何逐步计算节点总数,最终实现比常规遍历更高效的解决方案。

题目背景:

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

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

示例 1:

复制代码
输入: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) 的简单解决方案。你可以设计一个更快的算法吗?

题目解析:

基础知识:

首先,我们要知道什么是完全二叉树:

关键区别:并不是"每个节点都有左右子节点",而是"每一层能达到的最大节点数"。

  • 完全二叉树 :假设树的高度为 h(根节点在第1层),那么:

  • 第1层到第 h-1 层 :每一层的节点数都达到最大值(即 2^(层数-1) 个节点)

  • 第 h 层(最后一层) :节点都连续靠左排列 ,可以不满

可以看出如果整个树不是满二叉树,就递归其左右孩子,直到遇到满二叉树为止,用公式计算这个子树(满二叉树)的节点数量。

这里关键在于如何去判断一个左子树或者右子树是不是满二叉树呢

在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树。如图:

用数组存储来理解

完全二叉树可以用数组连续存储,这是堆排序的基础。

java

复制代码
// 完全二叉树 [1,2,3,4,5,6]
// 数组索引:0  1  2  3  4  5
// 节点值:  1  2  3  4  5  6

// 索引关系:
// 节点 i 的左子节点 = 2*i + 1
// 节点 i 的右子节点 = 2*i + 2

关键 :数组中没有空洞(没有 null 值),所有节点连续排列。

text

复制代码
完全二叉树:    1
              / \
             2   3
            / \ /
           4  5 6

数组:[1, 2, 3, 4, 5, 6]  ← 连续,没有空洞 ✅

非完全二叉树:  1
              / \
             2   3
            /     \
           4       5

数组:[1, 2, 3, 4, null, null, 5]  ← 有空洞 ❌

首先我们要知道数组存储的顺序是层序遍历,这样就能解释为什么最后一层靠左的时候,二叉树还是完全二叉树,靠右是不行的,例子如下:

靠右排列(不是完全二叉树)

text

复制代码
        1
       / \
      2   3
     / \   \
    4  5    6

问题:节点3没有左子节点,但有右子节点6
层序遍历应该怎么排?

按层序遍历规则:
第1层:1
第2层:2, 3
第3层:先遍历2的左子节点4,2的右子节点5,然后3的左子节点(null),3的右子节点6
结果:[1, 2, 3, 4, 5, null, 6]

关键 :层序遍历序列中出现了 null,不连续

解法1:

最简单的解法就是普通的二叉树的遍历方法,遍历整棵树统计节点,因为题目要返回的就是二叉树的节点。

这种解法有两种方式,就是递归法和层序遍历解法BFS,两种方式的时间复杂度都是O(n),

这两种方式我们在前面已经很了解了,有不清楚的可以去我的LeetCode刷题日记专栏中看二叉树相关的文章,这是普遍的方法,但我们这里处理的是一种特殊的二叉树,肯定有特殊的办法来处理。

解法2:

正如题目所说的进阶方法,有一种更快的解法

完全二叉树有一个重要性质:

如果左子树的高度等于右子树的高度,则左子树是满二叉树;

如果左子树的高度大于右子树的高度,则右子树是满二叉树。

满二叉树的节点数可以直接用公式计算:2^height - 1

利用这个性质,我们不需要遍历所有节点,只需要沿着树的左右边界计算高度,然后递归处理不满的那棵子树。

  • 左子树高度 = 2(路径:节点2 → 节点4,节点5没有被计入)

在计算节点数时 :满二叉树的公式 2^高度 - 1 计算的是整棵满二叉树的所有节点,包含节点5

text

复制代码
        2
       / \
      4   5

这是一棵高度为2的满二叉树:
- 节点:2, 4, 5
- 节点数 = 2^2 - 1 = 3 ✅(包含了5)

答案:根节点(1) + 左子树中满二叉树的部分(2,4,5)

text

复制代码
(1 << leftHeight) = 4 拆解:
┌─────────────────────────────────────────┐
│  这个4包含的节点:                         │
│                                          │
│      1  ← 根节点(1个)                   │
│     /                                    │
│    2  ← 左子树的根(1个)                 │
│   / \                                    │
│  4   5 ← 左子树的子节点(2个)            │
│                                          │
│  小计:1 + 1 + 2 = 4 ✅                   │
└─────────────────────────────────────────┘

为什么是 2^leftHeight 而不是 2^leftHeight - 1

因为 2^leftHeight - 1左子树本身的节点数(不包含根节点1)。

表达式 计算 包含的节点
2^leftHeight - 1 4-1=3 左子树:2,4,5
2^leftHeight 4 根节点1 + 左子树:1,2,4,5

所以 (1 << leftHeight) 巧妙地一次性包含了:根节点 + 满的左子树

leftHeight > rightHeight 时,说明:

  • 左子树比右子树深

  • 右子树一定是一个满二叉树(因为完全二叉树的节点都是靠左的,右子树不满的话左边也不能满)

  • 右子树的高度 = rightHeight

执行过程

第一层调用 countNodes(1)

1. 计算左高度(永远只往左走)

  • 1.left = 2

  • 2.left = 4

  • 4.left = null

  • 走了 2 步

  • 左高度 = 2

2. 计算右高度(永远只往左走)

  • 1.right = 3

  • 3.left = null

  • 走了 1 步

  • 右高度 = 1

3. 比较(不是 相等)

  • else 分支

java

复制代码
return (1 << 右高度) + countNodes(左子树)
= (1 << 1) + countNodes(2)
= 2 + countNodes(2)

这 2 是什么

  • 1 << 1 = 2

  • 它就是 根节点 1 + 右子节点 3

✅ 到这里已经确定:13 直接算完了,不需要进去看 3 下面。


三、进入 countNodes(2)(递归)

当前树是:

text

复制代码
    2
   / \
  4   5
  1. 计算左高度
  • 2.left = 4

  • 4.left = null

  • 走了 1 步

  • 左高度 = 1

  1. 计算右高度
  • 2.right = 5

  • 5.left = null

  • 走了 1 步

  • 右高度 = 1

  1. 比较(相等)
  • 相等分支

java

复制代码
return (1 << 左高度) + countNodes(右子树)
= (1 << 1) + countNodes(5)
= 2 + countNodes(5)

这 2 是什么

  • 1 << 1 = 2

  • 它就是 根节点 2 + 左子节点 4

✅ 到这里已经确定:24 直接算完,不需要进去看 4 下面。


四、进入 countNodes(5)(递归)

当前树:

text

5

  1. 左高度
  • 5.left = null → 0
  1. 右高度
  • 5.right = null → 0
  1. 相等

java

复制代码
return (1 << 0) + countNodes(null)
= 1 + 0
= 1

五、从下往上汇总

text

复制代码
countNodes(5) = 1

countNodes(2) = 2 + 1 = 3

countNodes(1) = 2 + 3 = 5

最终结果:5 ✅


设计巧妙:
复制代码
普通递归(O(n)):
count(node) = 1 + count(left) + count(right)
需要遍历所有节点

优化算法(O(log²n)):
利用完全二叉树特性,直接计算满二叉树部分

关键洞察:
2^height = 1(根节点) + (2^height - 1)(满子树)
          ↑                ↑
       多出来的1      满子树本身的节点数

题目答案:

复制代码
class Solution {
    // 通用递归解法
    public int countNodes(TreeNode root) {
        if(root == null) {
            return 0;
        }
        return countNodes(root.left) + countNodes(root.right) + 1;
    }
}

class Solution {
    // 迭代法
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int result = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size -- > 0) {
                TreeNode cur = queue.poll();
                result++;
                if (cur.left != null) queue.offer(cur.left);
                if (cur.right != null) queue.offer(cur.right);
            }
        }
        return result;
    }
}

class Solution {
    /**
     * 针对完全二叉树的解法
     *
     * 满二叉树的结点数为:2^depth - 1
     */
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        TreeNode left = root.left;
        TreeNode right = root.right;
        int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便
        while (left != null) {  // 求左子树深度
            left = left.left;
            leftDepth++;
        }
        while (right != null) { // 求右子树深度
            right = right.right;
            rightDepth++;
        }
        if (leftDepth == rightDepth) {
            return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0
        }
        return countNodes(root.left) + countNodes(root.right) + 1;
    }
}

结语:如果对你有帮助,请**点赞,关注,收藏,**你的支持就是我最大的鼓励!

相关推荐
贺国亚1 小时前
线程基础与生命周期- 并发编程
java·后端
目黑live +wacyltd1 小时前
算法备案的实操指南(含截图示例)
人工智能·算法·llm·大模型备案·算法备案
小糯米6011 小时前
C语言 指针4
c语言·数据结构·算法
洛水水1 小时前
【力扣100题】36.二叉树展开为链表
算法·leetcode·链表
小碗羊肉1 小时前
【JavaWeb | 第十篇】Spring中的事务控制
java·后端·spring
lwf0061641 小时前
PNN (Product-based Neural Network) 学习日记
算法·机器学习
SimonKing1 小时前
美团不做外卖做浏览器了,而且是AI浏览器:Tabbit
java·后端·程序员
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第48题】【JVM篇】第8题:JVM 里的有几种 ClassLoader?为什么会有多种?
java·开发语言·jvm·面试
ZPC82101 小时前
YOLO-3D + 双目相机 (RGB + 深度 + 点云) → 3D 位置 + 抓取姿态
人工智能·算法·计算机视觉·机器人