六、二叉树(1)

六、二叉树(1)

  • 理论基础
    • 种类
    • 存储方式
    • 遍历方式
  • 定义
  • 144.二叉树的前序遍历递归法,后面见迭代
  • 145.二叉树的后序遍历,递归
  • 94.二叉树的中序遍历,递归
      • 定义
      • 特点和区别
      • 适用场景
  • 迭代遍历
    • 前序迭代
    • 中序迭代
    • 后序迭代
      • 中序遍历(Inorder Traversal)
      • 后序遍历(Postorder Traversal)
      • 思路上的主要区别
  • 统一迭代 (标记法)
  • 层序遍历

理论基础

种类

满二叉树:节点都是满的,节点个数2^k - 1
完全二叉树:除最后一层外都是满的,底部可以空,但是有值的地方必须从左到右连续,
平衡二叉搜索树:(搜索树有序)平衡☞左子树和右子树的深度差不能> 1

存储方式

链式
.left 指向左儿子 .right指向右儿子(指针啦~)
线式
数组1 2 3 4 5 6顺序着标,对于某个节点i,i * 2+1是他的左孩子 i * 2+2是右孩子

一般是用链式也就是链表去构造,自己封装好,把头节点传入待处理的函数

遍历方式

深度优先搜索
前中后、迭代、递归(能递归的一定能迭代)
广度优先搜索 层序

前序遍历: 中左右
中序:左中右
后序:左右中(发现是先左后右,前中后是相对于中而言)

栈其实就是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。

而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。

定义

面试传入二叉树需要自己定义数据结构 初始值赋值None

# 
class TreeNode:
    def __init__(self, val, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

递归遍历三部曲:

  1. 确定递归函数的参数和返回值
  2. 确定终止条件
  3. 确定单层递归的逻辑

144.二叉树的前序遍历递归法,后面见迭代

# 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 preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []  # 全局变量
        # 递归参数
        def dfs(node):
            # 终止条件
            if node == None:
                return
            res.append(node.val)
            dfs(node.left)
            dfs(node.right)

        # 入口
        dfs(root)
        return res
        

145.二叉树的后序遍历,递归

# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        def dfs(node):

            if node == None:
                return
            dfs(node.left)
            dfs(node.right)
            ans.append(node.val)

        dfs(root)
        return ans

94.二叉树的中序遍历,递归

左中右

# 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 inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        def dfs(node):

            if node == None:
                return
            dfs(node.left)
            ans.append(node.val)
            dfs(node.right)
            

        dfs(root)
        return ans

递归和迭代的主要区别:

定义

  • 递归:一个函数直接或间接地调用自身来解决问题的方式。递归通常将问题分解为一个或多个较小的子问题,直到这些子问题足够简单以直接解决。

    示例:

    def factorial(n):
        if n == 0:
            return 1
        else:
            return n * factorial(n - 1)
    
  • 迭代:使用循环结构(如 for 循环或 while 循环)反复执行一段代码直到满足某个条件来解决问题。

    示例:

    def factorial(n):
        result = 1
        for i in range(1, n + 1):
            result += i
        return result
    

特点和区别

  1. 实现方式

    • 递归:通过函数调用自身实现。
    • 迭代:通过循环结构(如 forwhile 循环)实现。
  2. 终止条件

    • 递归:必须有一个明确的终止条件(基准情况)来避免无限递归。
    • 迭代:通过循环条件控制循环的终止。
  3. 空间复杂度

    • 递归:每次递归调用都会在调用栈上分配新的栈帧,可能导致较高的空间开销。
    • 迭代:通常只需要固定的额外空间(循环变量),因此空间开销较小。
  4. 可读性和简洁性

    • 递归:有时递归的解决方案更直观和简洁,特别是对于树形或分治问题。
    • 迭代:对于某些问题,迭代可能更直接和高效。
  5. 性能

    • 递归:由于函数调用的开销和可能的栈溢出,递归在某些情况下可能不如迭代高效。
    • 迭代:通常比递归更高效,特别是在需要大量递归调用的情况下。

适用场景

  • 递归:适用于自然分解为更小子问题的问题,如树遍历、图遍历、分治算法(如归并排序、快速排序)、计算斐波那契数列等。

    示例:计算斐波那契数列

    def fibonacci(n):
        if n <= 1:
            return n
        else:
            return fibonacci(n - 1) + fibonacci(n - 2)
    
  • 迭代:适用于需要重复执行某段代码直到满足特定条件的问题,如遍历数组、链表等线性数据结构、简单的数学计算等。

    示例:计算斐波那契数列

    def fibonacci(n):
        a, b = 0, 1
        for _ in range(n):
            a, b = b, a + b
        return a
    

迭代遍历

用栈模拟+while 循环
前序是中左右,所以每一个节点弹出保存之前,先入栈右节点,再入栈左节点,右节点先保存,在左节点的子树 没有全部弹出之前,会一直在,所以很好的实现了前序

前序迭代

def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        if root == None:
            return ans
        stack = [root]
        while stack:
            temp = stack.pop()
            ans.append(temp.val)  # 进去之前要考虑为None
            if temp.right:
                stack.append(temp.right)
            if temp.left:
                stack.append(temp.left)

        return ans

中序迭代

左中右,有一个node,先放入右节点,它,左节点,啥时候弹出?得先找到最左边那个节点。所以我们得按拿的顺序去放,先放入节点5,再放他的左节点4,如果还有左节点,一直放左节点,直到最左边1,弹出左1,弹出中4,如果有右2,放入右,再弹出右2,在弹出上一层的中5
六、二叉树(1)_第1张图片
注意:while cur or stack: # 循环条件# 怎么更新cur
# 假设这个最左节点是一个中节点,它还有一个右节点
cur = cur.right
# 去考虑右节点为根的小树

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        if  not root:
            return ans
        # 不能提前将root放入,
        stack = []
        cur = root
        while cur or stack:  # 循环条件
            
            # if node.left: # 这样会反复添加
            #     stack.append(node.left) 所以用一个指针来找
            if cur:
                stack.append(cur)
                cur = cur.left
            # 当左线找到最左点or 右节点为none
            else:
                cur = stack.pop()  #为空,
                ans.append(cur.val)
                # 怎么更新cur
                # 假设这个最左节点是一个中节点,它还有一个右节点
                cur = cur.right
                # 去考虑右节点为根的小树
        return ans

而且当到中的时候,stack是为空的巧的是cur指向右子树,指针表示了查询的顺序

后序迭代

左右中,可以通过先序遍历中左右改成 中右左,在ans[::-1]结果反转一下

# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        if not root:
            return ans

        cur = root
        stack = []
        last_visited = None
        while cur or stack:
            if cur:  # 左
                stack.append(cur)
                cur = cur.left
            # 当cur为空
            else:
                cur = stack[-1]
                # 会重复访问
                if cur.right and last_visited != cur.right:  # 右节点重复访问
                    
                    cur = cur.right
                else:
                    ans.append(cur.val) # 中
                    last_visited = stack.pop()
                    cur = None # 右还没遍历 当 cur 为 None 时,查看栈顶节点的右子树
                    
        return ans

                    

发现是用cur记录是否遍历过,cur有值就入栈,中序的顺序是左 中 右
后序遍历和中序遍历在实现上有一些相似之处,但它们在访问节点的顺序和处理方式上有显著的区别。以下是对这两种遍历方法的详细总结,以及它们的不同之处:

中序遍历(Inorder Traversal)

中序遍历的顺序是:左子树 -> 根节点 -> 右子树。其实现方式如下:
重点是我要用一个cur去找通过中 左 右(新头) 左的顺序

为什么?因为我的访问顺序和处理顺序是不一样的,前序中左右,一层一层都是先中,但中序是先左,所以得找到最左边。访问过的放到stack里面,没访问的用cur去更新,访问顺序也是中左右的顺序,符合我们寻找的逻辑
此时重复的是找最左点,cur空时(左点到头 or右点空)时弹出,右点又为新的数的头一直找左

# 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 inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        # 左中右 找左 压入右,中, 中作为新的左
        ans = []
        stack = []
        if not root:
            return []
        cur = root
        
            
        while cur or stack:
            # 去找头和左,要一直找到最左点,才能看他对应的右,这个右又是新的头
            while cur: 
                stack.append(cur)
                cur = cur.left
            # if not cur: 执行到下面应该就是cur为空
            node = stack.pop()
            ans.append(node.val)
            # if node.right: 如果存在,cur没毛病,如果不存在,反正cur之前也是空
            cur = node.right
                
        return ans

后序遍历(Postorder Traversal)

后序遍历的顺序是:左子树 -> 右子树 -> 根节点。其实现方式如下:

# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        if not root:
            return ans

        cur = root
        stack = []
        last_visited = None
        
        while cur or stack:
            if cur:  # 遍历左子树
                stack.append(cur)
                cur = cur.left
            else:
                cur = stack[-1]  # 查看栈顶节点

                # 如果右子树存在且未被访问过,遍历右子树
                if cur.right and last_visited != cur.right:
                    cur = cur.right
                else:
                    # 访问根节点
                    ans.append(cur.val)
                    last_visited = stack.pop()  # 弹出栈顶节点并更新 last_visited
                    cur = None  # 重置 cur 为 None

        return ans

把后续左右中看做中右左的逆,一层一层的,类似前序迭代

# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        if not root:
            return []
        stack = [root]
        # 左右中  中右左
        # 为什么能直接就出来,要出现中右左 可以是栈中,左右中,也可以是先中 弹出,再左右
        # 先中,弹出有什么好,类似一层一层的考虑,下一层就是他的左子和右子
        while stack:
            node = stack.pop()
            ans.append(node.val)
            if node.left:
                stack.append(node.left)
            if node.right:
                stack.append(node.right)
        return ans[::-1]
        




迭代到底在重复什么?

看前序迭代,从根节点开始,每次弹出一个,是中,再弹出左,左作为中节点,还有他的左和右,就这样一层一层的找左右,通过栈,把右压在底下,左先弹出来。我们从逻辑上看前序也是中,左(作为中还有左),左,左,一直到头,看到头的左对应的右,也就是刚刚被压在底下的右。右可能也是作为中去找左和右,所以有有了一场迭代,去找左和右
曾经的左作为新的中,曾经的右作为新的中,先进右后进左以便先出左

思路上的主要区别

  1. 访问顺序

    • 中序遍历:按照左子树 -> 根节点 -> 右子树的顺序。需要确保在访问根节点之前已经遍历了左子树,在访问右子树之前已经访问了根节点。
    • 后序遍历:按照左子树 -> 右子树 -> 根节点的顺序。需要确保在访问根节点之前已经遍历了左右子树。
  2. 辅助变量

    • 中序遍历:不需要辅助变量,只需通过当前节点 cur 和栈来遍历树。
    • 后序遍历:需要一个辅助变量 last_visited 来跟踪上一次访问的节点,以避免重复访问右子树。
  3. 栈的处理

    • 中序遍历:在找到最左节点后,弹出栈顶节点并访问它,然后遍历右子树。
    • 后序遍历:在找到最左节点后,查看栈顶节点。如果右子树存在且未被访问过,则遍历右子树;否则,访问根节点并更新 last_visited 变量。
  4. 终止条件

    • 中序遍历:循环的终止条件是当前节点 curNone 且栈为空。
    • 后序遍历:循环的终止条件相同,但需要额外检查右子树的访问状态。

前面用迭代来实现前中后序,后面通过栈用递归来实现。
针对三种遍历方式,使用迭代法是可以写出统一风格的代码!

统一迭代 (标记法)

访问放入栈,要处理的也放入,但是做一个标记。
标记什么?标记已经展开过left right的点,所以不会重复添加
六、二叉树(1)_第2张图片

对于前序来说,中左右。遇到中,我们放进去,但是他先弹出,所以我们放在最外面,拿出来5,放入他的右6和左4,再放入中5,由于我们要弹出的数后面做个记号放一个none,如果弹出一个数是none,那他前一个数就是要处理的,弹出放入ans。
那怎么处理这个逻辑呢,普通的要处理的数没有左右,后面依旧添加一个None表示他要弹出

# 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 preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        st = []
        if not root:
            return []
        st = [root]
        while st:
            node = st.pop()
            if node:
                if node.right:
                    st.append(node.right)
                if node.left:
                    st.append(node.left)
                # 拿出来看了一下,再放回
                st.append(node)
                st.append(None)  # 标记要处理,
                # 不管是中节点(有儿子的),还是最后要弹出的节点(没儿子的),轮到他后面都有一个标记
            else:
                # 待处理
                ans.append(st.pop().val)
        return ans

对于中序,左中右。右边先进,中后进,左最后,左作为新的中,none加在哪里?其实我们发现,对于一个中节点去访问他的左右节点这个操作,是不断重复的,也就是每个节点,都会有机会被当做中去看有没有左右。什么中这个节点访问过,所以在他后面标记一个None,每个节点都有机会被当做中,所以都会被标记,也就都会输出。至于输出的顺序,受进栈的顺序影响,只要我一直是右中左的顺序,就不会出错

# 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 inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        ans = []
        st = [root] # 存放访问和标记 标记法
        while st:
            node = st.pop()
            if node:
                # 访问,展开左右
                # 左中右, 右中左
                if node.right:
                    st.append(node.right)
                st.append(node)
                st.append(None)
                if node.left:
                    st.append(node.left)
                # 如果是要弹出的叶子节点,等到访问他们的时候自然会加上NONE

            else:
                ans.append(st.pop().val)
        return ans

后序,左右中。同样标记访问展开过的点

# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        ans = []
        st = [root] # 存放访问和标记 标记法
        while st:
            node = st.pop()
            if node:
                st.append(node)
                st.append(None)
                if node.right:
                    st.append(node.right)
                if node.left:
                    st.append(node.left)        
            else:
                ans.append(st.pop().val)
        return ans

标记法太好用了!标记展开的点,迭代重复展开,遇到叶子节点或者展开过的点就回有none,直接输出即可。不管是递归还是迭代还是标记法都是深度优先下面考虑广度优先的层序遍历。

层序遍历

深度优先是用栈来实现,(一路一路)。广度优先是用队列,(一层一层)
需要哪些变量。ans存结果是一个二维的列表 queue放拿出来的值 node是拿出来之后要left right展开的点 level是一层的一维的列表
102.二叉树的层序遍历

注意deque初始化应该是一个iterable的queue = collections.deque([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 levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
       if not root:
           return []
       ans = []
       queue = collections.deque([root])
       while queue:
           # 后面queue会随着node展开增加,怎么才能知道我的node是在这一层的呢?
           # 用queue的长度 每次queue进入循环都只存了一层的信息
           level = [] # 应该在每层初始化
           for _ in range(len(queue)):              
               node = queue.popleft()
               if node.left:
                   queue.append(node.left)
               if node.right:
                   queue.append(node.right)
               level.append(node.val)
           ans.append(level)
       return ans

递归

# 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 []
       ans = []

       def dfs(node, level):
           if  not node:
               return
           # ans虽然是[[]]但是如果level 1 就超出范围了
           # 所以应该每层开始都要初始化一下
           if len(ans) == level:
               ans.append([])
           ans[level].append(node.val)
           dfs(node.left, level+1)
           dfs(node.right, level+1)

       dfs(root, 0)
       return ans

107.二叉树的层次遍历II

199.二叉树的右视图
637.二叉树的层平均值
429.N叉树的层序遍历
515.在每个树行中找最大值
116.填充每个节点的下一个右侧节点指针
117.填充每个节点的下一个右侧节点指针II
104.二叉树的最大深度
111.二叉树的最小深度

你可能感兴趣的:(算法基础,python)