代码随想录训练营 Day13打卡 二叉树 part01
一、 二叉树理论基础
二叉树是一种重要的数据结构,用于表示具有层次关系的数据。二叉树的每个节点最多有两个子节点,通常称为左子节点和右子节点。
种类
- 普通二叉树: 节点最多有两个子节点,没有其他特定规则。
- 满二叉树: 所有非叶子节点都拥有两个子节点,并且所有叶子节点都在同一层上。
- 完全二叉树: 除最后一层外,每一层都是满的,并且最后一层的节点都尽可能地集中在左侧。
- 二叉搜索树(BST): 每个节点的左子树只包含键小于节点键的节点,右子树只包含键大于节点键的节点。
- 平衡二叉树(AVL树): 任何节点的两个子树的高度差不超过1。
- 红黑树: 一种自平衡的二叉搜索树,它有额外的属性和特定的规则,以确保树的高度大致平衡。
存储方式
二叉树可以用多种方式存储,最常见的包括:
- 链式存储: 每个节点包含数据和两个指向子节点的引用(左和右)。
- 顺序存储: 使用数组存储二叉树的节点。对于数组中位置为 i 的节点,其左子节点位于 2i+1,右子节点位于 2i+2,父节点位于
(i-1)/2。
遍历方式
二叉树的遍历分为三种主要类型:
- 前序遍历(Preorder): 先访问根节点,然后递归地做前序遍历左子树,再递归地做前序遍历右子树。
- 中序遍历(Inorder): 先递归地做中序遍历左子树,访问根节点,然后递归地做中序遍历右子树。
- 后序遍历(Postorder): 先递归地做后序遍历左子树,然后递归地做后序遍历右子树,最后访问根节点。
- 层次遍历(Level-order): 按照树的层次从上到下、从左到右遍历,通常使用队列来实现。
Python中二叉树的实现
class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = right
二、 递归遍历
递归算法的三个要素
- 参数与返回值: 定义递归函数的参数及其返回值。
- 终止条件: 定义递归应停止的条件。
- 单层递归逻辑: 定义每层递归需要执行的操作及如何递归调用自身。
遵循这三个步骤可以帮助你编写出清晰且正确的递归算法。
前序遍历(Preorder Traversal)
前序遍历的顺序是根节点 → 左子树 → 右子树。
class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def preorderTraversal(self, root: TreeNode) -> List[int]:res = []def dfs(node):# 终止条件:节点为空if not node:return# 单层递归逻辑:访问根,然后左子树,然后右子树res.append(node.val) # 访问根节点dfs(node.left) # 递归访问左子树dfs(node.right) # 递归访问右子树dfs(root)return res
中序遍历(Inorder Traversal)
中序遍历的顺序是左子树 → 根节点 → 右子树。
class Solution:def inorderTraversal(self, root: TreeNode) -> List[int]:res = []def dfs(node):# 终止条件:节点为空if not node:return# 单层递归逻辑:先左子树,再访问根,最后右子树dfs(node.left) # 递归访问左子树res.append(node.val) # 访问根节点dfs(node.right) # 递归访问右子树dfs(root)return res
后序遍历(Postorder Traversal)
后序遍历的顺序是左子树 → 右子树 → 根节点。
class Solution:def postorderTraversal(self, root: TreeNode) -> List[int]:res = []def dfs(node):# 终止条件:节点为空if not node:return# 单层递归逻辑:先左子树,再右子树,最后访问根dfs(node.left) # 递归访问左子树dfs(node.right) # 递归访问右子树res.append(node.val) # 访问根节点dfs(root)return res
文章讲解
视频讲解
三、 迭代遍历
为什么可以使用迭代方式实现二叉树遍历?
递归本质上是通过函数 调用栈 来保存暂停点的数据,然后在返回时继续执行。
每个递归调用保存当前函数的局部变量和必要的状态信息,使得函数能够在执行完成后返回到正确的位置继续执行。
通过使用栈结构,我们可以显式地模拟这个过程,自己管理栈中元素的入栈和出栈,从而控制程序的执行流程。
前序遍历(迭代法)
前序遍历的顺序是:中 - 左 - 右。要使用迭代法实现前序遍历,你可以遵循以下步骤:
-
创建一个空栈,首先将根节点压入栈中。
-
循环直到栈为空:
· 弹出栈顶元素,访问该节点。
· 将该节点的右孩子压入栈中(如果有)。
· 将该节点的左孩子压入栈中(如果有)。
这样的处理顺序确保了每次从栈中弹出的是下一个要访问的节点,因为栈是后进先出(LIFO)的数据结构,所以要先处理的节点后入栈。
动画如下:
# 前序遍历-迭代-LC144_二叉树的前序遍历
class Solution:def preorderTraversal(self, root: TreeNode) -> List[int]:# 根结点为空则返回空列表if not root:return []stack = [root]result = []while stack:node = stack.pop()# 中结点先处理result.append(node.val)# 右孩子先入栈if node.right:stack.append(node.right)# 左孩子后入栈if node.left:stack.append(node.left)return result
后序遍历(迭代法)
再来看后序遍历,先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:
# 后序遍历-迭代-LC145_二叉树的后序遍历
class Solution:def postorderTraversal(self, root: TreeNode) -> List[int]:if not root:return []stack = [root]result = []while stack:node = stack.pop()# 中结点先处理result.append(node.val)# 左孩子先入栈if node.left:stack.append(node.left)# 右孩子后入栈if node.right:stack.append(node.right)# 将最终的数组翻转return result[::-1]
中序遍历(迭代法)
中序遍历的迭代实现需要精心处理访问和处理元素的顺序,因为中序遍历的顺序是左中右,这导致了处理节点的顺序(将节点值加入结果数组)与节点的访问顺序(遍历到该节点)不一致。
那么在 使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
动画如下:
实现思路
-
栈的使用:
栈用来暂存还未处理的节点,这主要是因为在深入左子树过程中,我们还没处理父节点及其右子树。
-
访问与处理分离:
使用一个指针 cur(当前节点)来进行树的遍历,遵循中序遍历的规则(左-中-右)。
当 cur 不为空,表示还有左子树需要进一步深入访问,所以将 cur 入栈并转向左子节点。
当 cur 为空,表示左边已到底,此时从栈中取出节点进行处理(输出节点值),然后转向右子树。 -
迭代循环的条件:
当 cur 不为空或栈不为空时,循环继续。这是因为:
cur 不为空表示当前节点或其左子树还未完全处理。
栈不为空表示还有一些节点的右子树需要处理。
class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def inorderTraversal(self, root: TreeNode) -> List[int]:if not root:return []stack = [] # 使用栈来追踪未处理的节点result = [] # 结果数组,用来存放按照中序遍历的节点值cur = root # 使用 cur 指针来遍历树while cur or stack:if cur:# 深入遍历到左子树最深处,左子节点一直入栈stack.append(cur)cur = cur.leftelse:# 到达最左侧节点后,开始处理节点cur = stack.pop() # 弹出待处理的节点result.append(cur.val) # 将节点值加入结果数组cur = cur.right # 转向处理右子树return result
通过上述迭代法,我们能够不使用递归(系统调用栈)而使用显式栈来控制中序遍历的流程,这样可以避免递归可能引起的栈溢出问题,并且对遍历过程有更明确的控制。
文章讲解
视频讲解1
视频讲解2
四、 层序遍历
层序遍历二叉树(也称为宽度优先遍历或广度优先遍历)是一种按照层次顺序遍历二叉树节点的方法。在这一遍历过程中,首先访问根节点,然后依次访问其子节点,再依次访问下一层的所有节点,依此类推,直到遍历完所有层。
为了实现层序遍历,我们通常使用队列这种数据结构,因为队列遵循先进先出(FIFO)的原则,这与层序遍历的逻辑相吻合。
使用队列实现二叉树广度优先遍历,动画如下:
下面是一个使用队列实现层序遍历的步骤说明:
-
初始化队列: 创建一个空队列。
-
入队根节点: 将根节点入队。
-
循环处理队列:
循环条件:只要队列不为空,就一直处理。
出队节点:从队列头部取出一个节点。
访问节点:处理或访问该节点。
入队子节点:将该节点的左右子节点(如果有)入队。 -
重复步骤3: 直到队列为空。
版本一 迭代法
层次遍历(或广度优先搜索)通常使用队列来实现。这种方法依靠队列的先进先出(FIFO)特性来保持节点的访问顺序。
from collections import dequeclass TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:if not root:return []queue = deque([root]) # 初始化队列,首先加入根节点result = [] # 用于存储每一层的节点值while queue:level = [] # 存储当前层的所有节点值level_length = len(queue) # 当前层的节点数for _ in range(level_length):cur = queue.popleft() # 弹出队列前端的节点level.append(cur.val) # 将当前节点的值加入到本层列表中# 如果当前节点有左子节点,将其加入队列if cur.left:queue.append(cur.left)# 如果当前节点有右子节点,将其加入队列if cur.right:queue.append(cur.right)result.append(level) # 将当前层的结果加入到最终结果列表中return result
版本二 递归法
递归方法通过定义一个辅助函数来遍历树,这个辅助函数接受当前节点和节点的层级作为参数,层级用来确定当前节点值应该被添加到结果列表的哪个部分。
class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:if not root:return []levels = [] # 用于存储所有层的节点值def traverse(node, level):# 如果当前层还没有创建列表,则新增一个空列表if len(levels) == level:levels.append([])# 将当前节点的值添加到对应层的列表中levels[level].append(node.val)# 递归处理左子节点,层级加一if node.left:traverse(node.left, level + 1)# 递归处理右子节点,层级加一if node.right:traverse(node.right, level + 1)traverse(root, 0) # 从根节点的第0层开始遍历return levels
- 迭代法 通过显式使用队列来按层次顺序遍历树的节点,适合于需要严格按照层序的场景。
- 递归法 通过调用栈来实现层次遍历,更直观,但在深度很大的树中可能会导致栈溢出。
这两种方法各有优势,通常迭代法在实现树的层次遍历时更为常见和直接,递归法则更加简洁。根据具体情况选择合适的方法。
文章讲解
视频讲解