代码随想录算法训练营第十三天 | 二叉树part01

代码随想录算法训练营第十三天 | 二叉树part01

理论基础

二叉树的种类

在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。

#满二叉树

满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。 如图所示:

这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。

#完全二叉树

什么是完全二叉树?

完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。

大家要自己看完全二叉树的定义,很多同学对完全二叉树其实不是真正的懂了。

我来举一个典型的例子如题:

#二叉搜索树

前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树

下面这两棵树都是搜索树

#平衡二叉搜索树

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

如图:

最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。

存储方式

二叉树可以链式存储,也可以顺序存储。

那么链式存储方式就用指针 , 顺序存储的方式就是用数组

顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。

链式存储如图:

链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢?

其实就是用数组来存储二叉树,顺序存储的方式如图:

用数组来存储二叉树如何遍历的呢?

如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。

要了解,用数组依然可以表示二叉树。

遍历方式

二叉树主要有两种遍历方式:

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走。
  2. 广度优先遍历:一层一层的去遍历。

这两种遍历是图论中最基本的两种遍历方式,后面在介绍图论的时候 还会介绍到。

那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:

  • 深度优先遍历

    • 前序遍历(递归法,迭代法)
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
  • 广度优先遍历

    • 层次遍历(迭代法)

在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。

这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。

看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式

  • 前序遍历:中左右
  • 中序遍历:左中右
  • 后序遍历:左右中

大家可以对着如下图,看看自己理解的前后中序有没有问题。

最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。

之前我们讲栈与队列的时候,就说过栈其实就是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。

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

二叉树的定义
python 复制代码
class TreeNode:
    def __init__(self, val, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

递归遍历

每次写递归,都按照这三要素来写,可以保证写出正确的递归算法!

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
144 二叉树的前序遍历
python 复制代码
res = []
def dfs(node):
    if node is None:
        return
    res.append(node.val)
    dfs(node.left)
    dfs(node.right)
dfs(root)
return res   
145 二叉树的后序遍历
python 复制代码
    res = []
    def dfs(node):
        if node is None:
            return
        dfs(node.left)
        dfs(node.right)
        res.append(node.val)
    dfs(root)
    return res   
95 二叉树的中序遍历
python 复制代码
    res = []
    def dfs(node):
        if node is None:
            return
        dfs(node.left)
        res.append(node.val)
        dfs(node.right)
    dfs(root)
    return res   

迭代遍历

递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中

144 二叉树的前序遍历

我们先看一下前序遍历[中右左]。

前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。

为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。

动画如下:

python 复制代码
    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
145 二叉树的后序遍历

再来看后序遍历,先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:

python 复制代码
    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]
95 二叉树的中序遍历

分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,

因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。

那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

动画如下:

python 复制代码
    if not root:
        return []
    stack = []  # 不能提前将root结点加入stack中
    result = []
    cur = root
    while cur or stack:
        # 先迭代访问最底层的左子树结点
        if cur:     
            stack.append(cur)
            cur = cur.left      
        # 到达最左结点后处理栈顶结点    
        else:       
            cur = stack.pop()
            result.append(cur.val)
            # 取栈顶元素右结点
            cur = cur.right 
    return result        

统一迭代

无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况

那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。

动画中,result数组就是最终结果集。

可以看出我们将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集。

添加None的主要目的是为了标记一个节点,表示"我们已经访问过这个节点,现在应该将它的值添加到结果中。

144 二叉树的前序遍历
python 复制代码
    result = []
    st= []
    if root:
        st.append(root)
    while st:
        node = st.pop()
        if node != None:
            if node.right: #右
                st.append(node.right)
            if node.left: #左
                st.append(node.left)
            st.append(node) #中
            st.append(None)
        else:
            node = st.pop()
            result.append(node.val)
    return result
145 二叉树的后序遍历

当我们第一次遇到一个节点时,我们并不立即访问它,而是将其本身、None标记、右子节点和左子节点依次压入栈中。

python 复制代码
    result = []
    st = []
    if root:
        st.append(root)
    while st:
        node = st.pop()
        if node != None:
            st.append(node) #中
            st.append(None)
            if node.right: #右
                st.append(node.right)
            if node.left: #左
                st.append(node.left)
        else:
            node = st.pop()
            result.append(node.val)
    return result
95 二叉树的中序遍历

在这个中序遍历的实现中,在"中"节点后面加入None是为了标记"左子树已经处理完毕,下一步应该访问当前节点"。

python 复制代码
    result = []
    st = []
    if root:
        st.append(root)
    while st:
        node = st.pop()
        if node != None:
            if node.right: #添加右节点(空节点不入栈)
                st.append(node.right)
            st.append(node) #添加中节点
            st.append(None) #中节点访问过,但是还没有处理,加入空节点做为标记。
            if node.left: #添加左节点(空节点不入栈)
                st.append(node.left)
        else: #只有遇到空节点的时候,才将下一个节点放进结果集
            node = st.pop() #重新取出栈中元素
            result.append(node.val) #加入到结果集
    return result

层序遍历

层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。

需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。

而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。

使用队列实现二叉树广度优先遍历,动画如下:

这样就实现了层序从左到右遍历二叉树。

102 二叉树的层序遍历

第一种方法:

python 复制代码
    if not root:
        return []
    deque = collections.deque([root])
    result = []
    while deque:
        level = []
        for i in range(len(deque)):
            node = deque.popleft()
            level.append(node.val)
            if node.left:
                deque.append(node.left)
            if node.right:
                deque.append(node.right)
        result.append(level)
    return result

第二种方法:

python 复制代码
    if not root:
        return []
    levels = []
    def traverse(node,level):
        if node is None:
            return
        # 当遍历到一个新的层级(level)时,如果这一层还没有对应的列表(即 len(levels) == level),
        # 就需要创建一个新的空列表来存储这一层的节点
        if len(levels) == level:
            levels.append([])
        levels[level].append(node.val)
        traverse(node.left,level + 1)
        traverse(node.right,level + 1)
    traverse(root,0)
    return levels
107 二叉树的层次遍历II
python 复制代码
    if not root:
        return []
    levels = []
    def traverse(node,level):
        if node is None:
            return
        # 当遍历到一个新的层级(level)时,如果这一层还没有对应的列表(即 len(levels) == level),
        # 就需要创建一个新的空列表来存储这一层的节点
        if len(levels) == level:
            levels.append([])
        levels[level].append(node.val)
        traverse(node.left,level + 1)
        traverse(node.right,level + 1)
    traverse(root,0)
    return levels[::-1]
199 二叉树的右视图
python 复制代码
    if not root:
        return []
    queue = collections.deque([root])
    right_view = []
    while queue:
        # 因为一旦进入循环,len(queue)是会发生变化的,所以需要定死每次循环的次数
        level_size = len(queue)
        for i in range(level_size):
            node = queue.popleft()
            if i == level_size - 1:
                right_view.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
    return right_view
637 二叉树的层平均值
python 复制代码
    if not root:
        return []
    queue = collections.deque([root])
    averages = []
    while queue:
        size = len(queue)
        level_sum = 0
        for i in range(size):
            node = queue.popleft()
            level_sum += node.val
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        averages.append(level_sum / size)
    return averages
429 N叉树的层序遍历
python 复制代码
    if not root:
        return []
    result = []
    queue = collections.deque([root])
    while queue:
        level_size = len(queue)
        level = []
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
            for child in node.children:
                queue.append(child)
        result.append(level)
    return result
515 在每个树行中找最大值
python 复制代码
    if not root:
        return []
    deque = collections.deque([root])
    result = []
    while deque:
        size = len(deque)
        level = []
        for i in range(size):
            node = deque.popleft()
            level.append(node.val)
            if node.left:
                deque.append(node.left)
            if node.right:
                deque.append(node.right)
        result.append(max(level))
    return result
116 填充每个节点的下一个右侧节点指针
python 复制代码
    # 在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了
    if not root :
        return root
    deque = collections.deque([root])
    while deque :
        size = len(deque)
        prev = None
        for i in range(size):
            node = deque.popleft()
            if prev:
                prev.next = node
            prev = node
            if node.left:
                deque.append(node.left)
            if node.right:
                deque.append(node.right)
    return root
117 填充每个节点的下一个右侧节点指针II
python 复制代码
    # 这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道
    if not root:
        return root
    deque = collections.deque([root])
    while deque:
        size = len(deque)
        prev = None
        for i in range(size):
            node = deque.popleft()
            if prev:
                prev.next = node
            prev = node
            if node.left:
                deque.append(node.left)
            if node.right:
                deque.append(node.right)
    return root
104 二叉树的最大深度
python 复制代码
    if not root:
        return 0
    deque = collections.deque([root])
    result = []
    while deque:
        size = len(deque)
        level = []
        for i in range(size):
            node = deque.popleft()
            level.append(node.val)
            if node.left:
                deque.append(node.left)
            if node.right:
                deque.append(node.right)
        result.append(level)
    return len(result)
111 二叉树的最小深度
python 复制代码
    if not root :
        return 0
    deque = collections.deque([root])
    result = []
    while deque:
        size = len(deque)
        level = []
        for i in range(size):
            node = deque.popleft()
            level.append(node.val)
            if node.left :
                deque.append(node.left)
            if node.right:
                deque.append(node.right)
            if not node.left and not node.right:
                return len(result) + 1
        result.append(level)
相关推荐
xiaoshiguang33 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡3 小时前
【C语言】判断回文
c语言·学习·算法
别NULL3 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇3 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
ZSYP-S4 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos5 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习5 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA5 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo5 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc5 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法