完全二叉树
较难0完全二叉树:一种整齐有规律的树形结构
想象一下,你正在班级里排座位,老师要求大家按顺序坐好:第一排坐满4个同学,第二排坐满4个,第三排坐满4个……到了最后一排,人数不够也没关系,但必须从最左边的座位开始坐,中间不能有空位。这种“先上后下、先左后右,绝不留空”的坐法,就和完全二叉树一模一样。
完全二叉树是除了最后一层外,上面每一层的节点数都达到最大值(即该层所有位置都填满),并且最后一层的节点全部靠左连续排列的二叉树。这种结构在计算机科学中有很多好处,比如可以用数组来高效存储,实现堆排序、优先队列等经典算法。
一、完全二叉树的两条核心规则
-
上一层必须全满
假设树有h层,那么第 1 层到第h-1层都必须是满的(每个节点都有左右孩子)。例如一个高度为 3 的完全二叉树,前两层(第1层1个节点,第2层2个节点)必须全部存在。 -
最后一层必须靠左连续
最后一层的节点必须从最左边开始依次排列,中间不能有空缺。也就是说,从左到右数节点时,不会出现“右边有节点、左边却空着”的情况。
为什么叫“完全”?
因为它“几近完整”,除了最后一层可能有缺失,其他部分都完整。它比一般二叉树更规整,比满二叉树(每一层都满)更灵活。
二、生活中的完全二叉树
-
学校运动会淘汰赛
把8支球队的赛程画成一棵二叉树,第1轮是4场比赛,第2轮是2场,第3轮是1场。如果只有6支球队,那么最后两场比赛的队伍需要“轮空”,但如果第1轮先安排6队中的4队比赛,另外2队轮空,这种安排就不符合完全二叉树——因为轮空的队伍在树中相当于某些节点缺失且不连续。而真正完全二叉树的淘汰赛会先让所有球队依次配对,实在不够的才轮空,并且轮空位置也靠左。 -
电影院座位编号
假设电影院有若干排,每排座位数相同,但今天观众人数少,大家从每排最左边开始坐,不允许隔空坐人。这种“靠左连续”的坐法就是完全二叉树的思想。
三、完全二叉树的性质(数学规律)
-
节点数与高度的关系
设树的高度为h(根节点高度为1),则最少有2^(h-1)个节点(最后一层只有1个),最多有2^h - 1个节点(满二叉树)。 -
用数组存储时的索引规律
这是完全二叉树最大优势:- 从数组索引
1开始存储根节点(索引0留空或放总数)。 - 对于任意节点在数组中的位置
i:- 它的左孩子位置为
2 * i - 它的右孩子位置为
2 * i + 1 - 它的父节点位置为
i // 2
- 它的左孩子位置为
- 数组的最后一个非叶子节点下标为
总节点数 // 2。
这个规律省去了用指针(或
left/right属性)连接节点的麻烦,只需简单的下标运算就能找到子节点或父节点。 - 从数组索引
-
为什么可以用数组?
因为完全二叉树是“连续”的,从左到右、从上到下编号正好与数组的下标一一对应。如果树中间有空洞,这种对应关系就会被打乱,必须用指针结构。
四、Python 实现:用列表模拟完全二叉树
1. 从层序遍历序列构建完全二叉树
假设我们已经按层序遍历得到了节点的值序列(例如 [1,2,3,4,5,6]),由于完全二叉树连续无空洞,我们可以直接把它存入数组,并在索引0处放一个占位符(比如 None),让真正的数据从索引1开始。
def build_complete_tree(values):
# values: 按层序顺序给出的节点值列表,完全二叉树连续,无None
# 返回一个列表,索引0占位None,索引1起存储节点值
return [None] + values # 前面加一个占位符
2. 辅助函数:拿到孩子和父节点的索引
def left_child(index):
# 返回左孩子的位置(下标)
return index * 2
def right_child(index):
# 返回右孩子的位置(下标)
return index * 2 + 1
def parent(index):
# 返回父节点的位置
return index // 2
3. 遍历完全二叉树
因为数组已经天然记录了节点的顺序,我们可以用递归或栈来遍历。下面给出前序、中序、后序和层序遍历的代码。
def preorder_tree(tree, index, result):
# 前序遍历:根 -> 左 -> 右
if index < len(tree) and tree[index] is not None:
result.append(tree[index]) # 访问根
preorder_tree(tree, left_child(index), result) # 递归左子树
preorder_tree(tree, right_child(index), result) # 递归右子树
def inorder_tree(tree, index, result):
# 中序遍历:左 -> 根 -> 右
if index < len(tree) and tree[index] is not None:
inorder_tree(tree, left_child(index), result)
result.append(tree[index])
inorder_tree(tree, right_child(index), result)
def postorder_tree(tree, index, result):
# 后序遍历:左 -> 右 -> 根
if index < len(tree) and tree[index] is not None:
postorder_tree(tree, left_child(index), result)
postorder_tree(tree, right_child(index), result)
result.append(tree[index])
def level_order_tree(tree):
# 层序遍历:直接按数组顺序输出(跳过索引0)
# 注意:完全二叉树数组本身已经是层序顺序,但需去掉None(本例中不会出现None)
return [tree[i] for i in range(1, len(tree)) if tree[i] is not None]
4. 完整示例
现在我们来创建一个有6个节点的完全二叉树,并打印所有遍历结果。
# 创建一个完全二叉树,按层序输入:1为根,2左,3右,4、5、6为下一层(2的孩子)
tree = build_complete_tree([1, 2, 3, 4, 5, 6])
res_pre = []
preorder_tree(tree, 1, res_pre)
print("前序遍历结果:", res_pre) # 输出: [1, 2, 4, 5, 3, 6]
res_in = []
inorder_tree(tree, 1, res_in)
print("中序遍历结果:", res_in) # 输出: [4, 2, 5, 1, 3, 6]
res_post = []
postorder_tree(tree, 1, res_post)
print("后序遍历结果:", res_post) # 输出: [4, 5, 2, 6, 3, 1]
print("层序遍历结果:", level_order_tree(tree)) # 输出: [1, 2, 3, 4, 5, 6]
注意:中序和后序遍历结果与普通二叉树一样,完全二叉树并没有特殊的遍历规则,只是存储方式不同。
五、新手最容易犯的错
-
数组索引从 0 还是 1 开始?
- 如果索引从 0 开始,那么左孩子是
2*i+1,右孩子是2*i+2,父节点是(i-1)//2。 - 如果索引从 1 开始,公式更简洁。建议统一用一种,并在写代码时明确注释。
- 常见错误:混用两种公式,或者忘记留出索引 0 的占位符,导致越界。
- 如果索引从 0 开始,那么左孩子是
-
判断孩子是否存在的条件
用数组存储时,节点不一定都存在(虽然完全二叉树是连续的,但用 None 占位时也可能有空洞)。一定要检查下标是否超出数组长度,以及该位置的值是否为None。否则递归时会访问到空值或越界。# 错误写法:直接访问可能会下标越界 res.append(tree[left_child(index)]) # 如果 left_child(index) 超出 len(tree)-1,会报错 # 正确写法: if left_child(index) < len(tree): ... -
认为完全二叉树一定是满二叉树
- 满二叉树:所有层都是满的。
- 完全二叉树:除了最后一层,上面全满,最后一层靠左连续。
完全二叉树不一定是满二叉树,但满二叉树一定是完全二叉树。
-
层序遍历结果不等价于数组顺序
对于完全二叉树,数组的顺序(从索引1开始)就是层序遍历的顺序,但遇到None占位的情况(比如不是完全二叉树时),层序遍历需要跳过None。不过在我们讨论的“连续完全二叉树”中不会出现None。
六、相关知识点指引
- 堆(Heap):堆是一种完全二叉树,通常用数组实现,分为最大堆和最小堆。堆排序、优先队列的底层都是堆。
- 优先队列(Priority Queue):用堆实现,支持快速取出最大/最小元素。
- 线段树(Segment Tree):虽然不一定是完全二叉树,但也常用数组存储,其父子索引关系与完全二叉树一样(2i, 2i+1)。
- 二叉堆的插入与删除:完全二叉树的性质保证了堆的平衡,插入时在数组末尾添加节点,然后“上浮”;删除根时,把最后一个节点移到根,然后“下沉”。
- 树的遍历:前序、中序、后序、层序遍历,是二叉树的基本操作,掌握它们后可以应对很多树的问题。
如果你已经掌握了完全二叉树的数组存储,接下来可以尝试用 Python 实现一个最小堆,或者用堆解决“合并K个有序链表”这样有趣的问题。
例题精讲
一棵完全二叉树共有100个节点,则其叶子节点个数为多少?
一棵完全二叉树一定是满二叉树。
以下函数用于判断一棵二叉树是否为完全二叉树。请补充代码。
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def is_complete(root):
if not root:
return True
from collections import deque
queue = deque([root])
flag = False # 标记是否遇到空节点
while queue:
node = queue.popleft()
if not node:
flag = True
else:
if flag:
return False
queue.append(node.left)
queue.append(___)
return True将一棵完全二叉树按层序编号(根节点编号为1),则编号为10的节点的父节点编号是?
一棵完全二叉树有2^k个节点,则其深度为k+1。