今天我们来解决LeetCode第222题——完全二叉树的节点个数。这道题看似简单,但其中蕴含着对二叉树结构的深刻理解。作为一名经历过多次算法面试的老手,我发现很多候选人在这个问题上容易陷入思维定式。让我们从基础开始,逐步深入探讨几种不同的解法。
完全二叉树是一种特殊的二叉树结构,其中除了最后一层外,所有层都被完全填充,并且最后一层的节点都尽可能集中在左侧。这种结构在实际应用中非常常见,比如堆的实现就依赖于完全二叉树的性质。
递归解法是最直观的二叉树节点计数方法。它的核心思想是:一棵树的节点总数等于左子树节点数加上右子树节点数再加1(当前节点)。这种分而治之的策略是解决树形结构问题的经典范式。
java复制public int countNodes(TreeNode root) {
return dfs(root);
}
private int dfs(TreeNode node) {
if(node == null) return 0;
return dfs(node.left) + dfs(node.right) + 1;
}
这个解法的时间复杂度是O(N),其中N是树的节点总数。因为我们需要访问树中的每一个节点一次。空间复杂度取决于树的深度,最坏情况下(树退化为链表)是O(N),平均情况下是O(logN)。
提示:虽然递归解法简洁易懂,但在处理极大树时可能会遇到栈溢出问题。在实际工程中,对于深度很大的树,迭代解法可能更安全。
优势:
局限:
层序遍历使用队列数据结构,按层次遍历树的节点。这种方法同样可以用于计算节点总数。
java复制public int countNodes(TreeNode root) {
if(root == null) return 0;
int cnt = 0;
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while(!q.isEmpty()) {
TreeNode t = q.poll();
cnt++;
if(t.left != null) q.add(t.left);
if(t.right != null) q.add(t.right);
}
return cnt;
}
相比递归解法,层序遍历在实际运行中通常会更慢(6ms vs 0ms),这是因为:
层序遍历的优势在于:
对于高度为h的完全二叉树:
基于这个性质,我们可以设计更高效的算法:
java复制public int countNodes(TreeNode root) {
if(root == null) return 0;
int leftHeight = getHeight(root.left);
int rightHeight = getHeight(root.right);
if(leftHeight == rightHeight) {
// 左子树是满的
return (1 << leftHeight) + countNodes(root.right);
} else {
// 右子树是满的,但少一层
return (1 << rightHeight) + countNodes(root.left);
}
}
private int getHeight(TreeNode node) {
int height = 0;
while(node != null) {
height++;
node = node.left;
}
return height;
}
这个算法的时间复杂度是O(logN * logN),因为:
| 解法 | 时间复杂度 | 空间复杂度 | LeetCode运行时间 | 击败百分比 |
|---|---|---|---|---|
| 递归 | O(N) | O(logN) | 0ms | 100% |
| 层序 | O(N) | O(N) | 6ms | 5.92% |
| 优化 | O(log²N) | O(logN) | 0ms | 100% |
从实测数据可以看出,递归解法和优化解法在性能上表现优异,而层序遍历由于额外的队列操作,效率明显较低。
在处理树结构时,空指针是最常见的错误。建议:
完善的测试用例应该包括:
对于大规模树结构:
在实际工程项目中,除了正确性外,我们还需要考虑:
例如,我们可以将计数器抽象为一个独立的组件:
java复制interface TreeCounter {
int countNodes(TreeNode root);
}
class RecursiveCounter implements TreeCounter {
// 实现递归计数
}
class LevelOrderCounter implements TreeCounter {
// 实现层序计数
}
这种设计模式使得算法实现可以灵活替换,便于测试和比较不同方法的性能。
根据不同的应用场景,我建议:
对于LeetCode练习,建议先实现递归解法确保正确性,再尝试优化解法提升性能,最后用层序遍历巩固广度优先搜索的理解。
完全二叉树的节点计数问题可以引申出许多有趣的变种:
理解这些变种问题有助于深化对树形结构的掌握。例如,统计叶子节点可以修改递归终止条件:
java复制if(node.left == null && node.right == null) return 1;
在实际面试中,我曾多次遇到这个问题及其变种。总结几点经验:
有一次面试,面试官要求我在白板上实现O(log²N)的解法。因为没有准备,我首先实现了递归版本,然后通过分析完全二叉树的性质,逐步推导出了优化解法。这种展示思考过程的方式反而获得了加分。
对于完全二叉树的处理,关键在于利用其结构特性避免不必要的计算。在实际工程中,这种优化思维同样重要——了解数据结构的特性,才能写出最高效的代码。