思路:
我们之前讲过了关于二叉树的深度优先遍历(前、中、后序遍历)。
接下来我们再来介绍二叉树的另一种遍历方式:层序遍历(广度优先遍历)。
二叉树的层序遍历使用的是迭代法
递归:递归是通过函数调用自身的过程来解决问题的。在递归函数中,通常会包含一个终止条件,以确保递归在达到某个特定状态时停止。
迭代:迭代是通过循环结构(如for循环、while循环)来重复执行一段代码,直到满足某个终止条件。迭代通常使用一个明确的循环变量来控制循环的进程。
层序遍历一个二叉树。就是一层一层从左到右的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。需要借用一个辅助数据结构即队列来实现,队列先进先出,符合层序遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
使用队列实现二叉树广度优先遍历,动画如下:
代码:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root: # 根结点为空则返回空列表
return []
queue = collections.deque([root]) # 创建了一个双端队列,并将根节点root作为初始元素放入队列中
result = [] # 初始化一个空列表result,用于存储每一层的节点值
while queue: # 当队列queue不为空
level = [] # 在每一层遍历开始前,初始化一个空列表level,用于存储当前层的节点值
for i in range(len(queue)): # 循环次数为当前队列的长度(即当前层的节点数),队列queue的长度在整个循环过程中这个长度值是不会改变的。这意味着,即使我们在循环体内从队列中弹出元素并可能向队列中添加新元素,循环的次数仍然是基于循环开始时队列的长度。
cur = queue.popleft() # 从队列左侧(前端)删除并返回一个节点
level.append(cur.val) # 将该节点的值加入当前层的列表
if cur.left: # 如果该节点有左子节点,将左子节点加入队列
queue.append(cur.left)
if cur.right: # 如果该节点有右子节点,将右子节点加入队列
queue.append(cur.right)
result.append(level) # 将当前层的节点值列表level加入结果列表result
return result
时间复杂度 O(N) : N为二叉树的节点数量,每个点进队出队各一次,故渐进时间复杂度为 O(n)。
空间复杂度 O(N) : 最差情况下,即当树为平衡二叉树时,最多有 N/2个树节点同时在 queue 中,使用 O(N)大小的额外空间。另外,我们还需要一个列表result来存储每一层的节点值,这个列表的大小也是O(n)。因此,总的空间复杂度是O(n)。
思路:
可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。
关键在于遍历顺序,前中后序应该选哪一种遍历顺序。
这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次,某些节点的左右孩子节点不翻转!
遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。
那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!
总结:
针对二叉树的问题,解题之前一定要想清楚究竟是使用前中后序遍历,还是层序遍历。
二叉树解题的大忌就是自己稀里糊涂的过了(因为这道题相对简单),但是也不知道自己是怎么遍历的。
代码:
深度优先遍历(前序遍历)(递归法)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root: # 如果传入的根节点root为None(即二叉树为空),则直接返回None
return None
root.left, root.right = root.right, root.left # 将当前节点的左子节点和右子节点进行交换
self.invertTree(root.left) # 递归地反转左子树和右子树。这是通过再次调用invertTree方法,并将当前节点的左子节点和右子节点作为参数传入来实现的。
self.invertTree(root.right)
return root # 返回反转后的二叉树的根节点
时间复杂度:O(N),其中 N为二叉树节点的数目。我们会遍历二叉树中的每一个节点,对每个节点而言,我们在常数时间内交换其两棵子树。
空间复杂度:O(N),使用的空间由递归栈的深度决定,它等于当前节点在二叉树中的高度。在平均情况下,二叉树的高度与节点个数为对数关系,即 O(logN)。而在最坏情况下,树形成链状,空间复杂度为 O(N)。
深度优先遍历(后序遍历)(递归法)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:
if not root:
return None
self.invertTree(root.left)
self.invertTree(root.right)
root.left, root.right = root.right, root.left
return root
广度优先遍历(层序遍历)(迭代法)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root: # 如果传入的根节点root为None(即二叉树为空),则直接返回None
return None
queue = collections.deque([root]) # 初始化一个双端队列queue,并将根节点root加入队列
while queue:
size=len(queue) # 获取当前层的节点数
for i in range(size): # 遍历当前层的所有节点
node = queue.popleft() # 从队列左侧弹出一个节点
node.left, node.right = node.right, node.left # 交换当前节点的左右子节点
if node.left: # 如果当前节点有左子节点或右子节点,则将它们加入队列
queue.append(node.left)
if node.right:
queue.append(node.right)
return root # 最后,返回反转后的二叉树的根节点。这个根节点其实就是原来的根节点,但是它的所有节点的左右子节点都已经被交换了
时间复杂度O(n):同样每个节点都需要入队列/出队列一次,所以是 O(n)
空间复杂度O(n):在最坏的情况下,即当二叉树的最后一层节点全部被加入队列时,队列的大小将达到最大,即存储了二叉树中几乎所有的节点。因此,队列的空间需求为 O(n),其中 n 是节点总数。
思路:
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
但都可以理解算是后序遍历,尽管已经不是严格上在一个树上进行遍历的后序遍历了。
1.递归法:
递归三部曲:确定递归函数的参数和返回值、确定终止条件、确定单层递归的逻辑
2.迭代法:
这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。
这里我们可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(注意这不是层序遍历)
通过队列来判断根节点的左子树和右子树的内侧和外侧是否相等,如动画所示:
代码:
递归法(这种递归比较的过程实际上是一种自定义的遍历方式,它结合了递归调用和对称性的检查。它没有按照传统的层序遍历或深度优先遍历的顺序来访问节点,而是根据对称性的需求来比较和访问相应的节点。)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
if not root: # 如果根节点为None(即二叉树为空),则它自然是对称的,所以返回True
return True
return self.compare(root.left, root.right) # 调用compare方法,传入根节点的左子节点和右子节点作为参数,用于比较它们是否对称
def compare(self, left, right):
if left == None and right != None: # 检查是否只有一个子节点为None,如果是,则它们不是对称的
return False
elif left != None and right == None: # 检查是否只有一个子节点为None,如果是,则它们不是对称的
return False
elif left == None and right == None: # 检查两个子节点是否同时为None(即两个子树都为空),如果是,则它们是对称的
return True
elif left.val != right.val: # 最后,它比较两个非空子节点的值是否相等,如果不相等,则它们不是对称的
return False
#此时就是:左右节点都不为空,且数值相同的情况
#此时才做递归,做下一层的判断
outside = self.compare(left.left, right.right) # 递归地比较左子树的左子节点和右子树的右子节点是否对称
inside = self.compare(left.right, right.left) # 递归地比较左子树的右子节点和右子树的左子节点是否对称
isSame = outside and inside # 当这两个比较都为True时,才认为这两棵子树是对称的。
return isSame
时间复杂度:O(n),由于需要对每个节点进行访问和比较,并且每次比较都会递归地访问子节点,所以时间复杂度是O(n),其中n是二叉树中的节点数。
空间复杂度:O(n) ,在最坏的情况下,当二叉树完全不平衡时(例如,所有节点都只有左子节点或只有右子节点,变成一个链表结构),递归调用栈的深度可能达到n。因此,空间复杂度在最坏情况下是O(n)。然而,在平均情况下,空间复杂度通常会更低,特别是当二叉树相对平衡时。
当我们说二叉树退化成链表时,我们指的是二叉树中的节点大多数或全部只有一个子节点,导致树的形状类似于链表。
迭代法
import collections
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
if not root:
return True
queue = collections.deque()
queue.append(root.left) #将左子树头结点加入队列
queue.append(root.right) #将右子树头结点加入队列
while queue: #接下来就要判断这这两个树是否相互翻转
leftNode = queue.popleft()
rightNode = queue.popleft()
if not leftNode and not rightNode: #左节点为空、右节点为空,此时说明是对称的
continue
#左右一个节点不为空,或者都不为空但数值不相同,返回false
if not leftNode or not rightNode or leftNode.val != rightNode.val:
return False
queue.append(leftNode.left) #加入左节点左孩子
queue.append(rightNode.right) #加入右节点右孩子
queue.append(leftNode.right) #加入左节点右孩子
queue.append(rightNode.left) #加入右节点左孩子
return True
时间复杂度:O(n)
空间复杂度:O(n),这里需要用一个队列来维护节点,每个节点最多进队一次,出队一次,队列中最多不会超过 n个点,故渐进空间复杂度为 O(n)