hello大家好!还是标准的打招呼语哈哈哈。好久没更新博客了(国庆玩hai了),回来之后又忙着论文最后阶段的冲刺,还有竞赛的冲刺,还跑去搞了学生会的换届工作。这样一写才发现原来这段时间这么忙(bushi)。也是因为这些七七八八的事情,才把博客更新拖到现在QAQ
好了,咱们话不多说,直接进入今天正题——来种树( )
1、二叉树概要
老规矩先上点概念哈。二叉树是一种树形数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树的特点使得它在计算机科学中有着广泛的应用,包括数据存储、排序、搜索等。
以下是一些专业名词:
- 根节点(Root Node): 二叉树的最顶层节点,没有父节点。
- 子节点(Child Node): 每个节点可以有左子节点和右子节点。
- 叶子节点(Leaf Node): 没有任何子节点的节点。
- 父节点(Parent Node): 有子节点的节点。
- 深度(Depth): 从根节点到某个节点的路径长度。
- 高度(Height): 从某个节点到叶子节点的最长路径长度。
常见的二叉树大致可以分为以下几类(这里就不给图了,概念上面都有实在想象不出来就去百度图片一下吧~):
(1)满二叉树:每个节点都有两个子节点,并且所有叶子节点都在同一层。
(2)完全二叉树:除了最后一层外,每一层的节点都是满的,且最后一层的叶子节点从左到右排 列。
(3)二叉搜索树(BST, Binary Search Tree): 对于每一个节点,左子树中的所有节点值小于该节点,右子树中的所有节点值大于该节点。
二叉树的常见操作有:
(1)遍历: 常见的遍历方式有前序遍历(Pre-order)、中序遍历(In-order)、后序遍历(Post-order)和层序遍历(Level-order)。
(2)插入: 在二叉搜索树中,可以根据节点值大小插入新的节点。
(3)删除: 删除二叉树中的节点时,需考虑节点是叶子节点、只有一个子节点或有两个子节点的情况。
(4)查找: 在二叉搜索树中,可以根据节点值进行高效的查找。
#二叉树内容还蛮多的,我会多写几篇,现在讲到的这些在后面都会再详细的提及
2、二叉树的节点结构
在二叉树的数据结构中,每个节点的结构通常包含以下三个部分:
- 数据域(data): 存储节点的值或数据。
- 左子节点指针(left): 指向当前节点的左子节点。如果没有左子节点,通常为
null
或None
。 - 右子节点指针(right): 指向当前节点的右子节点。如果没有右子节点,通常为
null
或None
。
代码实现如下:
class TreeNode:
def __init__(self, data=0, left=None, right=None):
self.data = data # 节点的数据域
self.left = left # 左子节点指针
self.right = right # 右子节点指针
3、二叉树的前序、中序、后序遍历
前序遍历、中序遍历和后序遍历是二叉树遍历的三种经典方式,每种方式访问节点的顺序不同。它们分别是根据节点与其左右子节点的访问顺序来命名的。我会从递归和非递归(迭代)两种角度来分别说明他们的代码实现。(这里建议大家可以先去查一下栈这种结构,我前面还没有讲到过,后面的文章才会写)
(1) 前序遍历(Pre-order Traversal: 根-左-右)
-
访问顺序: 先访问当前节点(根节点),再访问左子树,最后访问右子树。
-
步骤:
- 访问根节点。
- 递归遍历左子树。
- 递归遍历右子树。
-
示例:
对于如下二叉树:
其遍历结果为1、2、4、5、3
递归方法
def preorder_recursive(root):
if root:
# 输出当前节点的数据(根)
print(root.data, end=' ')
# 递归遍历左子树
preorder_recursive(root.left)
# 递归遍历右子树
preorder_recursive(root.right)
#递归的代码很简便,相信大家都能看得懂,我就不详细赘述了。其实你可以把他理解为一种命令语句,你要这个代码做什么就直接打什么,不用拐着绕弯直接写
#会先输出左侧数字是因为 preorder_recursive(root.left)这一行写在上面,代码是按顺序执行的
非递归方法
def preorder_iterative(root):
if not root:
return
# 初始化栈,并将根节点入栈
stack = [root]#创建一个名为 stack 的列表,这个列表将用作栈
#(后进先出,LIFO)的数据结构。用中括号 [ ] 创建一个列表,
#并将 root 节点作为列表的第一个元素。
#将 root 节点放入栈中,表示从根节点开始遍历。
while stack:
# 弹出栈顶节点并输出其数据(根)
node = stack.pop()
print(node.data, end=' ')
# 先将右子节点入栈,因为栈是后进先出,这样能先访问左子节点
if node.right:
stack.append(node.right)
# 再将左子节点入栈
if node.left:
stack.append(node.left)
#这个方法相较于递归就比较麻烦难看懂了。在前序遍历的过程中,使用栈的目的是模拟递归调用的行为,以便在不使用递归的情况下遍历二叉树。
#通过 stack
,我们可以控制节点访问的顺序。每次从栈中弹出一个节点,访问该节点的数据,然后将它的子节点(右、左)依次压入栈中。由于栈是后进先出,先压入的右子节点会在左子节点之后被弹出,从而实现前序遍历的顺序(根-左-右)。
#stack.pop()
: 移除并返回栈顶的节点。这里的栈顶元素是上一次压入栈的节点,也就是我们接下来要访问的节点。
#在二叉树的遍历过程中,我们会逐个访问每个节点,并输出它的 data
。通过使用 end=' '
,我们可以将每个节点的值输出在同一行,以空格分隔。
#还要注意入栈顺序,栈都是先进后出,所以我们应该先压入右,再压入左
2. 中序遍历(In-order Traversal: 左-根-右)
-
访问顺序: 先访问左子树,然后访问当前节点(根节点),最后访问右子树。
-
步骤:
- 递归遍历左子树。
- 访问根节点。
- 递归遍历右子树。
-
示例:
对于如下二叉树:
其遍历结果为:4、2、5、1、3
递归方法
def inorder_recursive(root):
if root:
# 递归遍历左子树
inorder_recursive(root.left)
# 输出当前节点的数据(根)
print(root.data, end=' ')
# 递归遍历右子树
inorder_recursive(root.right)
#简单说一下逻辑,只要节点一直有左侧子节点,就会一直执行if下面的第一行,到4,没有左节点,那么执行第二行 print(root.data, end=' '),然后到第三行,也没有右侧节点,则返回root为2的时候,因为下去到4是执行第一行,此时返回则要执行第二行就是输出2,然后再到第三行代码去看右侧节点,以此类推
非递归方法
def inorder_iterative(root):
stack = [] # 用于存放节点的栈
current = root # 当前处理的节点
while current or stack:
# 一直深入到当前子树的最左节点,将沿途节点压入栈
while current:
stack.append(current)
current = current.left
# 弹出栈顶元素(最左节点)并访问
current = stack.pop()
print(current.data, end=' ')
# 转向右子树
current = current.right
#代码上要注意,root就是我们设置好的二叉树节点,他的结构我都已经写在上面了。我用AI生成了一个代码执行过程,供大家参考。
详细执行过程
-
初始状态:
stack = []
(初始化为空栈)。current = 1
(当前节点为根节点1
)。- 栈:
[]
。
-
第1次循环:
- 内层循环: 当前节点为
1
,压入栈,并继续访问其左子节点2
。- 栈:
[1]
current = 2
- 栈:
- 内层循环: 当前节点为
2
,压入栈,并继续访问其左子节点4
。- 栈:
[1, 2]
current = 4
- 栈:
- 内层循环: 当前节点为
4
,压入栈,并继续访问其左子节点(None
)。- 栈:
[1, 2, 4]
current = None
(因为4
没有左子节点)
- 栈:
- 弹出并访问: 栈顶元素
4
弹出并访问。- 输出:
4
- 栈:
[1, 2]
current = None
(因为4
没有右子节点)
- 输出:
- 内层循环: 当前节点为
-
第2次循环:
- 弹出并访问: 栈顶元素
2
弹出并访问(因为这次循环一开始current = None
)。- 输出:
4 2
- 栈:
[1]
current = 5
(转向2
的右子节点5
)
- 输出:
- 弹出并访问: 栈顶元素
-
第3次循环:
- 内层循环: 当前节点为
5
,压入栈,并继续访问其左子节点(None
)。- 栈:
[1, 5]
current = None
(因为5
没有左子节点)
- 栈:
- 弹出并访问: 栈顶元素
5
弹出并访问。- 输出:
4 2 5
- 栈:
[1]
current = None
(因为5
没有右子节点)
- 输出:
- 内层循环: 当前节点为
-
第4次循环:
- 弹出并访问: 栈顶元素
1
弹出并访问。- 输出:
4 2 5 1
- 栈:
[]
current = 3
(转向1
的右子节点3
)
- 输出:
- 弹出并访问: 栈顶元素
-
第5次循环:
- 内层循环: 当前节点为
3
,压入栈,并继续访问其左子节点(None
)。- 栈:
[3]
current = None
(因为3
没有左子节点)
- 栈:
- 弹出并访问: 栈顶元素
3
弹出并访问。- 输出:
4 2 5 1 3
- 栈:
[]
current = None
(因为3
没有右子节点)
- 输出:
- 内层循环: 当前节点为
-
结束循环: 栈为空,且
current
也为None
,遍历结束。
3. 后序遍历(Post-order Traversal: 左-右-根)
-
访问顺序: 先访问左子树,再访问右子树,最后访问当前节点(根节点)。
-
步骤:
- 递归遍历左子树。
- 递归遍历右子树。
- 访问根节点。
-
示例:
对于如下二叉树:
其遍历结果为:4、5、2、3、1
递归方法
def postorder_recursive(root):
if root:
# 递归遍历左子树
postorder_recursive(root.left)
# 递归遍历右子树
postorder_recursive(root.right)
# 输出当前节点的数据(根)
print(root.data, end=' ')
#和前面的内容都大差不差,这里就不再赘述啦~
非递归方法
def postorder_iterative(root):
if not root:
return
# 初始化栈和输出列表
stack = []
output = []
stack.append(root)
while stack:
# 弹出栈顶节点,并将其数据存入输出列表(倒序处理)
node = stack.pop()
output.append(node.data)
# 先将左子节点压入栈(因为在最终输出时它需要先处理,我们是倒序的)
if node.left:
stack.append(node.left)
# 再将右子节点压入栈
if node.right:
stack.append(node.right)
# 反转输出列表得到后序遍历顺序(因为我们在倒序存储)
print(' '.join(map(str, output[::-1])))
#这里对底下的代码print(' '.join(map(str, output[::-1])))做出解释。这是对列表 output
进行反转操作。[::-1]
是 Python 切片的用法,它表示将 output
列表从尾到头反转。map(str, output[::-1])
意思是对反转后的 output
列表中的每一个元素调用 str()
函数,把这些元素转换成字符串。join()
是一个字符串方法,它将列表中的元素连接成一个单独的字符串,并且在每两个元素之间插入指定的分隔符。在这里,' '.join(...)
表示用空格(' '
)来连接列表中的每个字符串元素。
#Python 没有专门的栈数据结构,但可以使用列表(list
)来模拟栈的行为,也就是 stack = []。
4、打印二叉树
这个问题是很多大厂面试的的时候都会询问的问题,也有很多种答案,这里我提供一种主流代码,仅供大家参考
class Node:
def __init__(self, value=0):
self.value = value
self.left = None
self.right = None
def print_tree(head, height=0, to="H", length=10):
"""
打印二叉树的函数
:head: 当前处理的节点
:height: 当前节点的层数 (根节点为 0)
:to: 表示节点类型 (H 表示根节点, v 表示父节点在左上方, ^ 表示父节点在左下方)
:length: 每个节点占用的打印宽度
"""
if head is None:
return
# 先打印右子树
print_tree(head.right, height + 1, "v", length)
# 生成当前节点的打印内容
val = to + str(head.value) + to
lenV = len(val)
lenL = (length - lenV) // 2 # 左边的空格数
lenR = length - lenV - lenL # 右边的空格数
print(' ' * (length * height) + ' ' * lenL + val + ' ' * lenR)
# 再打印左子树
print_tree(head.left, height + 1, "^", length)
# 构建测试用的二叉树
if __name__ == "__main__":
"""
1
2 3
4 5 6
7
"""
head = Node(1)
head.left = Node(2)
head.right = Node(3)
head.left.left = Node(4)
head.right.left = Node(5)
head.right.right = Node(6)
head.left.left.right = Node(7)
print_tree(head, 0, "H", 10)
#定义了一个简单的节点类 Node
,包括 value
、left
和 right
属性,用来表示节点的值和左右子节点。
#递归逻辑:
- 递归地先打印右子树,并在打印时增加高度。
- 生成当前节点的打印内容,计算左右空格,以控制节点的打印位置。
- 打印当前节点的值,并使用空格来控制缩进。
- 递归地打印左子树。
最后运行出来的图案会是这样的:
5、完成二叉树宽度优先遍历(求一颗二叉树的宽度)
这也是一个很经典的题目,要实现二叉树的宽度优先遍历(Breadth-First Traversal,通常也叫层序遍历),需要使用一个队列(Queue)来辅助存储当前层的节点。宽度优先遍历会逐层访问二叉树中的节点,从根节点开始依次访问每一层的所有节点。
宽度优先遍历的步骤
- 创建一个队列,将根节点加入队列。
- 循环执行以下步骤,直到队列为空:
- 从队列中取出最前面的节点,并访问它。
- 如果该节点有左子节点,则将左子节点加入队列。
- 如果该节点有右子节点,则将右子节点加入队列。
代码演示:
from collections import deque
class Node:
def __init__(self, value=0):
self.value = value
self.left = None
self.right = None
def breadth_first_traversal(root):
if not root:
return
queue = deque([root]) # 初始化队列并将根节点加入队列
while queue:
node = queue.popleft() # 从队列的前端取出节点
print(node.value, end=' ') # 访问节点的值
# 如果左子节点存在,则将其加入队列
if node.left:
queue.append(node.left)
# 如果右子节点存在,则将其加入队列
if node.right:
queue.append(node.right)
# 测试构建二叉树并进行遍历
if __name__ == "__main__":
root = Node(1)
root.left = Node(2)
root.right = Node(3)
root.left.left = Node(4)
root.left.right = Node(5)
root.right.right = Node(6)
root.left.right.right = Node(7)
print("Breadth-First Traversal:")
breadth_first_traversal(root)
#定义了一个节点类 Node
,包括 value
(节点的值)、left
(左子节点)和 right
(右子节点)
#依次取出队列中的节点并访问它们,直到所有节点都被访问。使用 while
循环,当队列不为空时执行以下操作:如果该节点有右子节点,则将其加入队列。如果该节点有左子节点,则将其加入队列。从队列中取出一个节点 node
,并打印它的值。使用 deque
(双端队列)初始化队列,并将根节点加入队列。检查根节点是否为空,如果是空树,直接返回。
#测试构建二叉树并调用遍历方法:构建了一棵二叉树,并调用 breadth_first_traversal
进行遍历。
我们以一个具体的二叉树为例,演示如何进行宽度优先遍历(也称为层序遍历)。假设我们有以下二叉树:
我用AI给大家做了一个步骤说明,供大家参考
宽度优先遍历步骤说明
我们按照宽度优先遍历(层序遍历)的规则,逐层遍历并访问节点。需要一个队列(FIFO,先进先出)来保持当前层次的节点顺序。
初始化:
- 队列:
[1]
(初始化,根节点1
放入队列)。
步骤1:访问根节点 1
- 从队列中取出节点
1
,并访问它。 - 访问
1
后,将它的左右子节点2
和3
加入队列。 - 输出:
1
- 队列:
[2, 3]
步骤2:访问节点 2
- 从队列中取出节点
2
,并访问它。 - 访问
2
后,将它的左右子节点4
和5
加入队列。 - 输出:
1 2
- 队列:
[3, 4, 5]
步骤3:访问节点 3
- 从队列中取出节点
3
,并访问它。 - 访问
3
后,将它的右子节点6
加入队列(3
没有左子节点)。 - 输出:
1 2 3
- 队列:
[4, 5, 6]
步骤4:访问节点 4
- 从队列中取出节点
4
,并访问它。 - 访问
4
后,它没有子节点,因此不需要将任何节点加入队列。 - 输出:
1 2 3 4
- 队列:
[5, 6]
步骤5:访问节点 5
- 从队列中取出节点
5
,并访问它。 - 访问
5
后,将它的右子节点7
加入队列(5
没有左子节点)。 - 输出:
1 2 3 4 5
- 队列:
[6, 7]
步骤6:访问节点 6
- 从队列中取出节点
6
,并访问它。 - 访问
6
后,它没有子节点,因此不需要将任何节点加入队列。 - 输出:
1 2 3 4 5 6
- 队列:
[7]
步骤7:访问节点 7
- 从队列中取出节点
7
,并访问它。 - 访问
7
后,它没有子节点,因此不需要将任何节点加入队列。 - 输出:
1 2 3 4 5 6 7
- 队列:
[]
(队列为空,遍历结束)
最后输出结果为:1 2 3 4 5 6 7
6、一些小tip
终于是写到最后了,不得不说二叉树的内容真的多,后面还有一堆QAQ
但是这些内容都还蛮重要的,大家有空还是多敲敲,可以练练手感,多去leetcode观光。明年上半年还有比赛呢
结尾也不写太多了,下次可能更新机器学习的内容,正好论文写完了,做个整理工作。
现在已经是半夜了,老实睡觉去了,明天早八QAQ
祝大家有美好的一天,晚安~~~