完全二叉树是数据结构中一个既基础又重要的概念,在堆结构、优先队列等场景中有着广泛应用。判断一棵二叉树是否为完全二叉树,是面试和算法竞赛中的常见问题。本文将深入探讨两种高效的检验方法,并分享实际编码中的关键细节。
完全二叉树的严格定义是:除最后一层外,所有层都被完全填满,并且所有节点都尽可能地向左集中。这意味着:
这种结构特性使得完全二叉树特别适合用数组实现,因为它的节点可以按照层序遍历顺序连续存储在数组中,不需要额外的指针空间。
提示:完全二叉树与堆结构密切相关,理解完全二叉树的特性有助于掌握优先队列的实现原理。
检验完全二叉树的核心在于验证其结构特性。我们有两种主要思路:
这两种方法各有优劣,层序遍历法更直观,而编号法则能揭示完全二叉树与数组存储的深层联系。
层序遍历法的核心在于利用队列进行广度优先搜索,同时记录是否遇到了第一个空节点。具体步骤如下:
foundNull初始为falsefoundNull为truefoundNull是否为true,如果是则立即返回falsejava复制public boolean isCompleteTree(TreeNode root) {
if (root == null) return true;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean foundNull = false;
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node == null) {
foundNull = true;
} else {
if (foundNull) return false;
queue.offer(node.left);
queue.offer(node.right);
}
}
return true;
}
foundNull标记一旦设置,后续遇到任何非空节点都意味着结构违规注意:在处理树结构问题时,明确区分"空指针"和"值为null的节点"很重要。这里的队列中存储的是可能为null的节点引用。
节点编号法利用了完全二叉树的一个重要特性:如果按照层序遍历顺序为节点编号(根节点为1,左子节点为2i,右子节点为2i+1),那么对于n个节点的完全二叉树,最大编号应该恰好等于n。
实现步骤:
java复制public boolean isCompleteTree(TreeNode root) {
if (root == null) return true;
Queue<TreeNode> nodeQueue = new LinkedList<>();
Queue<Integer> idxQueue = new LinkedList<>();
nodeQueue.offer(root);
idxQueue.offer(1);
int count = 0;
int lastIdx = 0;
while (!nodeQueue.isEmpty()) {
TreeNode node = nodeQueue.poll();
int idx = idxQueue.poll();
count++;
lastIdx = idx;
if (node.left != null) {
nodeQueue.offer(node.left);
idxQueue.offer(idx * 2);
}
if (node.right != null) {
nodeQueue.offer(node.right);
idxQueue.offer(idx * 2 + 1);
}
}
return lastIdx == count;
}
节点编号法的优势在于:
这种方法特别适合需要同时处理节点位置信息的场景,比如在堆结构实现中验证堆的性质。
| 特性 | 层序遍历法 | 节点编号法 |
|---|---|---|
| 直观性 | 高 | 中 |
| 实现复杂度 | 中 | 中 |
| 空间使用 | 需要存储空节点 | 需要额外存储编号 |
| 扩展性 | 一般 | 强 |
| 适用场景 | 简单验证 | 需要位置信息的场景 |
java复制// 测试工具方法:通过数组构建二叉树
private static TreeNode buildTree(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) return null;
TreeNode root = new TreeNode(arr[0]);
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int i = 1;
while (!queue.isEmpty() && i < arr.length) {
TreeNode node = queue.poll();
if (i < arr.length && arr[i] != null) {
node.left = new TreeNode(arr[i]);
queue.offer(node.left);
}
i++;
if (i < arr.length && arr[i] != null) {
node.right = new TreeNode(arr[i]);
queue.offer(node.right);
}
i++;
}
return root;
}
// 测试用例
public void testCompleteTree() {
Solution solution = new Solution();
// 完全二叉树
assertTrue(solution.isCompleteTree(buildTree(new Integer[]{1,2,3,4,5,6})));
// 非完全二叉树(空洞)
assertFalse(solution.isCompleteTree(buildTree(new Integer[]{1,2,3,4,5,null,7})));
// 单节点
assertTrue(solution.isCompleteTree(buildTree(new Integer[]{1})));
// 只有左子树的不完全二叉树
assertFalse(solution.isCompleteTree(buildTree(new Integer[]{1,2,null,4,5})));
// 满二叉树
assertTrue(solution.isCompleteTree(buildTree(new Integer[]{1,2,3,4,5,6,7})));
// 空树
assertTrue(solution.isCompleteTree(buildTree(new Integer[]{})));
// 只有右子树的树
assertFalse(solution.isCompleteTree(buildTree(new Integer[]{1,null,3,null,7})));
}
Q1:为什么层序遍历法需要将null节点入队?
A:这是为了准确检测"空洞"现象。如果只将非空节点入队,就无法知道空节点出现的位置,从而无法判断后续是否还有非空节点。
Q2:完全二叉树和满二叉树的区别是什么?
A:满二叉树要求所有层都完全填满,节点数达到最大值。完全二叉树只要求除最后一层外都填满,且最后一层从左到右连续排列。所有满二叉树都是完全二叉树,但反之不成立。
Q3:节点编号法为什么能验证完全二叉树?
A:完全二叉树按层序遍历编号时,节点编号应该是连续的1到n。如果编号出现跳跃(即lastIdx > count),说明树结构有空洞,不是完全二叉树。
foundNull标志的变化,帮助理解算法流程在实际编码面试中,我经常发现候选人能够实现基本算法,但往往忽略了边界条件的处理。比如在完全二叉树检验中,空树和单节点树是容易遗漏的测试用例。建议在编写算法时,先明确所有可能的边界情况,再着手实现核心逻辑。