

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
大家好,我是代码不加冰,今天给大家继续带来每日的刷题笔记。
摘要:
本文探讨了计算完全二叉树节点数量的高效算法。传统遍历方法时间复杂度为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计算的是整棵满二叉树的所有节点,包含节点5text
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 - 14-1=3 左子树:2,4,5 2^leftHeight4 根节点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
✅ 到这里已经确定:
1和3直接算完了,不需要进去看 3 下面。
三、进入
countNodes(2)(递归)当前树是:
text
2 / \ 4 5
- 计算左高度
从
2.left = 4
4.left = null走了 1 步
左高度 = 1
- 计算右高度
从
2.right = 5
5.left = null走了 1 步
右高度 = 1
- 比较(相等)
- 走 相等分支
java
return (1 << 左高度) + countNodes(右子树) = (1 << 1) + countNodes(5) = 2 + countNodes(5)这 2 是什么
1 << 1 = 2它就是 根节点 2 + 左子节点 4
✅ 到这里已经确定:
2和4直接算完,不需要进去看 4 下面。
四、进入
countNodes(5)(递归)当前树:
text
5
- 左高度
- 5.left = null → 0
- 右高度
- 5.right = null → 0
- 相等
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;
}
}
结语:如果对你有帮助,请**点赞,关注,收藏,**你的支持就是我最大的鼓励!

