Binary Tree
二叉树(Binary Tree)是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。
class TreeNode:
"""二叉树节点类"""
def __init__(self, val: int):
self.val: int = val # 节点值
self.left: TreeNode | None = None # 左子节点引用
self.right: TreeNode | None = None # 右子节点引用
每个节点都有两个引用(指针),分别指向左子节点(left-child node)和右子节点(right-child node),该节点被称为这两个子节点的父节点(parent node)。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的左子树(left subtree),同理可得右子树(right subtree)。
在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树。如图 7-1 所示,如果将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
1. 常见的术语
二叉树的常用术语如图 7-2 所示。
- 根节点(root node):位于二叉树顶层的节点,没有父节点。
- 叶节点(leaf node):没有子节点的节点,其两个指针均指向
None
。 - 边(edge):连接两个节点的线段,即节点引用(指针)。
- 节点所在的层(level):从顶至底递增,根节点所在层为 1 。
- 节点的度(degree):节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
- 二叉树的高度(height):从根节点到最远叶节点所经过的边的数量。
- 节点的深度(depth):从根节点到该节点所经过的边的数量。
- 节点的高度(height):从距离该节点最远的叶节点到该节点所经过的边的数量。
注意
请注意,我们通常将“高度”和“深度”定义为“经过的边的数量”,但有些题目或教材可能会将其定义为“经过的节点的数量”。在这种情况下,高度和深度都需要加 1 。
2. 二叉树基本操作
2.1 初始化二叉树
与链表类似,首先初始化节点,然后构建引用(指针)。
# 初始化二叉树
# 初始化节点
n1 = TreeNode(val=1)
n2 = TreeNode(val=2)
n3 = TreeNode(val=3)
n4 = TreeNode(val=4)
n5 = TreeNode(val=5)
# 构建节点之间的引用(指针)
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
2.2 插入与删除节点
与链表类似,在二叉树中插入与删除节点可以通过修改指针来实现。图 7-3 给出了一个示例。
# 插入与删除节点
p = TreeNode(0)
# 在 n1 -> n2 中间插入节点 P
n1.left = p
p.left = n2
# 删除节点 P
n1.left = n2
提示
需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除通常是由一套操作配合完成的,以实现有实际意义的操作。
3. 常见二叉树类型
3.1 完美二叉树
如图 7-4 所示,完美二叉树(perfect binary tree)所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 0 ,其余所有节点的度都为 2 ;若树的高度为 h,则节点总数为 ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。
提示
请注意,在中文社区中,完美二叉树常被称为满二叉树。
3.2 完全二叉树
如图 7-5 所示,完全二叉树(complete binary tree)只有最底层的节点未被填满,且最底层节点尽量靠左填充。
3.3 完满二叉树
如图 7-6 所示,完满二叉树(full binary tree)除了叶节点之外,其余所有节点都有两个子节点。
3.4 平衡二叉树
如图 7-7 所示,平衡二叉树(balanced binary tree)中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
4. 二叉树的退化
图 7-8 展示了二叉树的理想结构与退化结构。当二叉树的每层节点都被填满时,达到“完美二叉树”;而当所有节点都偏向一侧时,二叉树退化为“链表”。
- 完美二叉树是理想情况,可以充分发挥二叉树“分治”的优势。
- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 。
如表 7-1 所示,在最佳结构和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大值或极小值。
表 7-1 二叉树的最佳结构与最差结构
完美二叉树 | 链表 | |
---|---|---|
第 层的节点数量 | ||
高度为 的树的叶节点数量 | ||
高度为 的树的节点总数 | ||
节点总数为 的树的高度 |
可视化二叉树代码
class TreeNode:
"""二叉树节点类"""
def __init__(self, val: int):
self.val: int = val # 节点值
self.left: 'TreeNode' | None = None # 左子节点引用
self.right: 'TreeNode' | None = None # 右子节点引用
def display(root):
"""打印二叉树为字符树"""
lines, *_ = _display_aux(root)
for line in lines:
print(line)
def _display_aux(node):
"""返回包含当前子树所有行的列表,以及子树宽度、高度和水平坐标"""
if node is None:
return [" "], 0, 0, 0
line = f'{node.val}'
width = len(line)
height = 1
middle = width // 2
# 只有左子树
if node.left and not node.right:
l_lines, l_width, l_height, l_middle = _display_aux(node.left)
first_line = ' ' * (l_middle + 1) + '_' * (l_width - l_middle - 1) + line
second_line = ' ' * l_middle + '/' + ' ' * (l_width - l_middle - 1 + width)
shifted_lines = [line + ' ' * width for line in l_lines]
return [first_line, second_line] + shifted_lines, l_width + width, l_height + 2, l_width + width // 2
# 只有右子树
if not node.left and node.right:
r_lines, r_width, r_height, r_middle = _display_aux(node.right)
first_line = line + '_' * r_middle + ' ' * (r_width - r_middle)
second_line = ' ' * (width + r_middle) + '\\' + ' ' * (r_width - r_middle - 1)
shifted_lines = [' ' * width + line for line in r_lines]
return [first_line, second_line] + shifted_lines, width + r_width, r_height + 2, width // 2
# 同时有左右子树
if node.left and node.right:
l_lines, l_width, l_height, l_middle = _display_aux(node.left)
r_lines, r_width, r_height, r_middle = _display_aux(node.right)
first_line = ' ' * (l_middle + 1) + '_' * (l_width - l_middle - 1) + line + '_' * r_middle + ' ' * (r_width - r_middle)
second_line = ' ' * l_middle + '/' + ' ' * (l_width - l_middle - 1 + width + r_middle) + '\\' + ' ' * (r_width - r_middle - 1)
# 对齐高度
if l_height < r_height:
l_lines += [' ' * l_width] * (r_height - l_height)
elif r_height < l_height:
r_lines += [' ' * r_width] * (l_height - r_height)
merged_lines = [a + ' ' * width + b for a, b in zip(l_lines, r_lines)]
return [first_line, second_line] + merged_lines, l_width + width + r_width, max(l_height, r_height) + 2, l_width + width // 2
# 叶子节点
return [line], width, height, middle
# 示例:创建二叉树并打印
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
root.right.right = TreeNode(6)
display(root)
# --------
n1 = TreeNode(1)
n2 = TreeNode(2)
n3 = TreeNode(3)
n4 = TreeNode(4)
n5 = TreeNode(5)
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
p = TreeNode(0)
n1.left = p
p.left = n2
display(n1)
class TreeNode:
"""二叉树节点类"""
def __init__(self, val: int):
self.val: int = val # 节点的值
self.left: 'TreeNode' | None = None # 左子节点,初始为 None
self.right: 'TreeNode' | None = None # 右子节点,初始为 None
def display(root):
"""打印二叉树为字符树"""
lines, *_ = _display_aux(root) # 调用辅助函数,获取打印所需的行列表
for line in lines:
print(line) # 逐行打印
def _display_aux(node):
"""
辅助递归函数,返回表示二叉树的字符串行列表,以及子树的宽度、高度和根节点在宽度中的偏移量
"""
if node is None:
# 如果节点为空,返回一个空格,占位
return [" "], 0, 0, 0
line = f'{node.val}' # 将节点的值转换为字符串
width = len(line) # 当前行的宽度,即节点值的字符数
height = 1 # 当前子树的高度,初始为 1
middle = width // 2 # 根节点在当前行中的中间位置
# 情况 1:只有左子树
if node.left and not node.right:
l_lines, l_width, l_height, l_middle = _display_aux(node.left) # 递归左子树
# 第一行:在左子树上方添加连接线,连接到当前节点
first_line = ' ' * (l_middle + 1) + '_' * (l_width - l_middle - 1) + line
# 第二行:在左子树和当前节点之间添加斜杠,表示连接
second_line = ' ' * l_middle + '/' + ' ' * (l_width - l_middle - 1 + width)
# 调整左子树的行,使其右移,和当前节点对齐
shifted_lines = [line + ' ' * width for line in l_lines]
# 返回合并后的行列表,更新宽度和高度信息
return [first_line, second_line] + shifted_lines, l_width + width, l_height + 2, l_width + width // 2
# 情况 2:只有右子树
if not node.left and node.right:
r_lines, r_width, r_height, r_middle = _display_aux(node.right) # 递归右子树
# 第一行:在当前节点后添加连接线,连接到右子树
first_line = line + '_' * r_middle + ' ' * (r_width - r_middle)
# 第二行:在当前节点和右子树之间添加反斜杠,表示连接
second_line = ' ' * (width + r_middle) + '\\' + ' ' * (r_width - r_middle - 1)
# 调整右子树的行,使其左移,和当前节点对齐
shifted_lines = [' ' * width + line for line in r_lines]
# 返回合并后的行列表,更新宽度和高度信息
return [first_line, second_line] + shifted_lines, width + r_width, r_height + 2, width // 2
# 情况 3:同时有左、右子树
if node.left and node.right:
l_lines, l_width, l_height, l_middle = _display_aux(node.left) # 递归左子树
r_lines, r_width, r_height, r_middle = _display_aux(node.right) # 递归右子树
# 第一行:连接左子树、当前节点和右子树的连接线
first_line = ' ' * (l_middle + 1) + '_' * (l_width - l_middle - 1) + line + '_' * r_middle + ' ' * (r_width - r_middle)
# 第二行:在左子树和当前节点之间添加斜杠,在当前节点和右子树之间添加反斜杠
second_line = ' ' * l_middle + '/' + ' ' * (l_width - l_middle - 1 + width + r_middle) + '\\' + ' ' * (r_width - r_middle - 1)
# 如果左子树高度小于右子树,高度对齐,左子树补充空行
if l_height < r_height:
l_lines += [' ' * l_width] * (r_height - l_height)
# 如果右子树高度小于左子树,高度对齐,右子树补充空行
elif r_height < l_height:
r_lines += [' ' * r_width] * (l_height - r_height)
# 合并左、右子树的行列表,每行之间加上当前节点的间隔
merged_lines = [a + ' ' * width + b for a, b in zip(l_lines, r_lines)]
# 返回完整的行列表,更新宽度和高度信息
return [first_line, second_line] + merged_lines, l_width + width + r_width, max(l_height, r_height) + 2, l_width + width // 2
# 情况 4:叶子节点(没有子节点)
return [line], width, height, middle # 返回节点值行,及其宽度和高度信息
# 示例:创建二叉树并打印
root = TreeNode(1) # 根节点 1
root.left = TreeNode(2) # 左子节点 2
root.right = TreeNode(3) # 右子节点 3
root.left.left = TreeNode(4) # 节点 2 的左子节点 4
root.left.right = TreeNode(5) # 节点 2 的右子节点 5
root.right.right = TreeNode(6) # 节点 3 的右子节点 6
display(root) # 打印二叉树
公众号:AI悦创【二维码】
AI悦创·编程一对一
AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发、Web、Linux」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh
C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh
方法一:QQ
方法二:微信:Jiabcdefh
- 0
- 0
- 0
- 0
- 0
- 0