代码随想录算法训练营第十四天 | 二叉树Part 1: 二叉树理论基础 、二叉树递归遍历、二叉树迭代遍历

代码随想录算法训练营第十四天 | 二叉树Part 1: 二叉树理论基础 、二叉树递归遍历、二叉树迭代遍历

今日学习的文章链接和视频链接

参考代码随想录

自己看到题目的第一想法

终于进入二叉树专题,很兴奋也很害怕

自己实现过程中遇到哪些困难

  • 二叉树基础知识之前学过,用链表实现二叉树不难,复习了用数组实现二叉树
  • 二叉树递归一开始不好理解

今日收获,记录一下自己的学习时长

  • 应打卡7月11日,7月17日补打卡,学习时长共6hr
  • 复习了二叉树两种遍历深度优先(bfs)和广度优先(dfs)中的bfs的三种方式
  • 二叉树bfs递归遍历和迭代遍历都整理出来并理解了,很有成就感
  • 二叉树*3,94.二叉树中序遍历、144.二叉树前序遍历、145.二叉树后续遍历
  • 继续学习二叉树dfs层序遍历

二叉树 Binary Tree

1. 树基础

树(Tree):由 n > 0 个节点与节点组合组成的有限集合。当 n = 0 时,树被称为空树;当 n > 0 时,树被称为非空树

1.1 节点分类

  • 节点的度:节点所含有子树的个数
  • 叶子节点:也叫做 终端节点,度为0
  • 分支节点:也叫做 非终端节点,度不为0

1.2 节点间关系

  • 子节点:一个节点含有的子树的根节点称为该节点的子节点。
  • 父节点:如果一个节点含有子节点,则这个节点称为子节点的父节点。
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点。

2. 二叉树基础

二叉树(Binary Tree):树中各个节点度不大于2的有序树称为二叉树。

  • 二叉树最多有两个子树,称为「左子树」和「右子树」。
  • 二叉树左右子树不可以互换。

2.1 二叉树种类

2.1.1 满二叉树 Full Binary Tree

满二叉树(Full Binary Tree):如果所有分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,则称该二叉树为满二叉树。

  • 即满二叉树只有度为0或2的节点,并且度为0的节点在同一层上。
2.1.2 完全二叉树 Complete Binary Tree

完全二叉树(Complete Binary Tree):如果叶子节点只能出现在最下面两层,并且最下层的叶子节点都依次排列在该层最左边的位置上,具有这种特点的二叉树称为完全二叉树。

完全二叉树要满足:

  • 叶子节点只能出现在下面两层
  • 除了最底层可能没有填满之外,其余每层节点数量达到最大值
  • 最下层的叶子节点集中在该层左边
  • 若最底层为h层(h1开始),则最底层节点个数为1 ~ 2^(h-1)

注意:满二叉树是完全二叉树的一种特殊情况。

2.1.3 二叉搜索树 Binary Search Tree

二叉搜索树是一个有序树,并满足一下性质:

  • 对于一个节点来说,如果它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值
  • 对于一个节点来说,如果它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值
  • 任意节点的左、右子树也是二叉搜索树
2.1.4 平衡二叉搜索树 Balanced Binary Search Tree

平衡二叉搜索树(Balanced Binary Tree):又称为AVL Tree。是一种结构平衡的二叉搜索树。即叶节点高度差的绝对值不超过1,并且左、右两个子树都是一棵平衡二叉搜索树。

AVL树满足以下性质:

  • 空二叉树是一个AVL树
  • 如果T是一个AVL树,它的左、右子树也是AVL树
  • AVL树的高度为O(logn)

2.2 二叉树的存储方式

二叉树的存储结构分为「顺序存储」和「链式存储」。

2.2.1 二叉树顺序存储
  • 二叉树顺序存储使用一个一维数组来存储二叉树的节点。
  • 节点位置采用完全二叉树的节点层次编号,从上到下,每一层从左到右依次存放二叉树元素。
  • 如果对应位置的二叉树元素不存在,则存放「空节点」。

数组和二叉树对应关系:

  • 如果某个二叉树节点编号为i,且该节点不是叶子节点,其左子节点下标为2 * i + 1右子节点下标为2 * i + 2
  • 如果某个二叉树节点编号为i,且该节点是叶子节点,其根节点下标为(i - 1) // 2

顺序存储特点:

  • 对于完全二叉树,特别是满二叉树,使用顺序存储能充分利用空间,所以使用顺序存储很合理
  • 对于一般二叉树来说,顺序存储要设置很多空节点,会浪费空间
  • 二叉树顺序存储时插入、删除操作效率低
2.2.2 二叉树链式存储
  • 二叉树链式存储使用链表来存储二叉树的节点
  • 每个链节点node包含一个数据域val存储节点信息,包含两个指针域leftright,分别指向左子节点和右子节点。

二叉树链式存储的定义:
Python

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

Java

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    // constructor
    TreeNode() {}
    TreeNode(int val) {this.val = val;}
    TreeNode(int val, TreeNode left, TreeNode left) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

二叉树遍历

二叉树遍历主要有两种方式:「深度优先遍历」和「广度优先遍历」

1 深度优先遍历 Depth-first Search

从二叉树的根节点root出发,先遍历完根节点的左子树所有节点,然后回到根节点继续遍历根节点的右子树所有节点。整个过程反复进行直到所有节点都被访问完。

深度优先遍历(DFS)可以分为「前序遍历」(Preorder)、「中序遍历」(Inorder)和「后序遍历」(Postorder)。

  • 前序遍历:「」 -> ->
  • 中序遍历: -> 「」 ->
  • 后序遍历: -> -> 「

例如:

5
/ \
4  6
/ \  / \
1  2  7  8

前序遍历:5 4 1 2 6 7 8
中序遍历:1 4 2 5 7 6 8
后序遍历:1 2 4 7 8 6 5

1.1 前序遍历 Preorder Traversal

前续遍历规则:

  • 如果树为空,则返回
  • 如果树不为空,则:
    • 访问根节点
    • 按照前续遍历方式遍历根节点的左子树
    • 按照前续遍历方式遍历根节点的右子树

所以前序遍历顺序为: -> ->

1.1.1 前序遍历的递归实现

递归写法的三要素:

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

前序遍历的递归实现:

  • 确定递归的 参数和返回值: 需要传入的参数为当前节点,因为要打印出前序遍历节点的值,所以参数需要一个数组来存放前序遍历节点的值,不需要返回值
# LC144-二叉树的前序遍历 
    def preorder(cur: Optional[TreeNode], res: List[int]) -> None:
  • 确定递归的 终止条件:遍历过程中,如果当前节点为空,说明本层遍历结束,所以终止条件为当前节点为空就返回
        if cur == None: return
  • 确定 单层递归的逻辑:前序遍历顺序为中左右,所以单层递归时,要先取中节点的值,然后左,然后右
        res.append(cur.val) # 中
        preorderTraversal(cur.left, res) # 左
        preorderTraversal(cur.right, res) # 右

完整代码:

1.1.1.1 Python - 前序遍历的递归实现
# 前序遍历-递归-LC144_二叉树的前序遍历
# 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: TreeNode) -> List[int]:
        # 前序遍历的递归实现
        res = list()

        def preorder(cur: TreeNode, vec: List[int]) -> None:
            # 终止条件
            if not cur: return

            vec.append(cur.val) # 中
            preorder(cur.left, vec) # 左
            preorder(cur.right, vec) # 右

        preorder(root, res)
        return res
1.1.1.2 Java - 前序遍历的递归实现
// 前序遍历-递归-LC144_二叉树的前序遍历
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        // 前序遍历的递归实现
        List<Integer> result = new LinkedList<>();
        preorder(root, result);
        return result;
    }

    public void preorder(TreeNode cur, List<Integer> temp) {
        // 终止条件
        if (cur == null) {
            return;
        }

        temp.add(cur.val); // 中  
        preorder(cur.left, temp); // 左
        preorder(cur.right, temp); // 右
    }
}
1.1.2 前序遍历的迭代实现

迭代实现的基本思路:

  • 使用递归就是每一次递归调用都把函数的局部变量、参数、返回值压入栈中,然后等递归返回的时候,从栈顶弹出上一层的各项参数,返回上一层。
  • 所以迭代实现递归算法时需要用

前序遍历的迭代实现:
每次遍历时,处理完中间节点后,先把 右节点 压入栈中,然后再把 左节点 压入栈中,这样出栈的时候是 中左右 的顺序。

注意:中节点入栈

具体算法:

  • 判断二叉树是否为空,如果为空返回
  • 初始化返回数组res
  • 初始化空栈stack,把根节点root压入栈中
  • 当栈stack不为空时:
    • 栈弹出当前栈顶元素node,并处理该元素,即res记录该节点的值
    • 如果node右节点不为空,访问右节点并入栈
    • 然后(注意顺序),如果node左节点不为空,访问左节点并入栈
1.1.2.1 Python - 前序遍历的迭代实现
# 前序遍历-递归-LC144_二叉树的前序遍历
# 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: TreeNode) -> List[int]:
        # 前序遍历的迭代实现
        
        # 返回数组 
        res = []

        # 空树
        if not root: 
            return res
        
        # list as stack
        # root只在第一次入栈,之后迭代过程中,root不入栈
        stack = [root]
        
        # 终止条件为stack为空,说明遍历完树
        while stack: # 相当于 len(stack) != 0
            node = stack.pop()          # 弹出根节点
            res.append(node.val)        # 访问根节点

            if node.right:
                stack.append(node.right) # 右子树先入栈
            if node.left:
                stack.append(node.left)  # 左子树后入栈

        return res 
1.1.2.2 Java - 前序遍历的迭代实现
// 前序遍历-递归-LC144_二叉树的前序遍历
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        // 前序遍历的迭代实现
        List<Integer> result = new LinkedList<>();

        // 空树 
        if (root == null) {
            return result;
        }

        Deque<TreeNode> stack = new ArrayDeque<>();
        stack.push(root);

        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            result.add(node.val);

            // 先右入栈
            if (node.right != null) {
                stack.push(node.right);
            }

            // 后左入栈
            if (node.left != null) {
                stack.push(node.left);
            }
        }

        return result;
    }
}

1.2 中序遍历 Inorder Traversal

中续遍历规则:

  • 如果树为空,则返回
  • 如果树不为空,则:
    • 按照中续遍历方式遍历根节点的左子树
    • 访问根节点
    • 按照中续遍历方式遍历根节点的右子树

所以中序遍历顺序为: -> ->

1.2.1 中序遍历的递归实现

中序遍历递归的基本思路与前序遍历递归一致。

中序遍历的递归实现:

  • 确定递归的 参数和返回值: 需要传入的参数为当前节点,因为要打印出中序遍历节点的值,所以参数需要一个数组来存放前序遍历节点的值,不需要返回值
# LC094-二叉树的中序遍历 
    def inorder(cur: Optional[TreeNode], res: List[int]) -> None:
  • 确定递归的 终止条件:遍历过程中,如果当前节点为空,说明本层遍历结束,所以终止条件为当前节点为空就返回
        if cur == None: return
  • 确定 单层递归的逻辑:中序遍历顺序为左中右,所以单层递归时,要先取根节点的左子树的值,然后根节点,最后右子树
        inorderTraversal(cur.left, res) # 左
        res.append(cur.val) # 中
        inorderTraversal(cur.right, res) # 右

完整代码:

1.2.1.1 Python - 中序遍历的递归实现
# 中序遍历-递归-LC094_二叉树的中序遍历
# 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: TreeNode) -> List[int]:
        # 前序遍历的递归实现
        res = list()

        def inorder(cur: TreeNode, vec: List[int]) -> None:
            # 终止条件
            if not cur: return

            inorder(cur.left, vec) # 左
            vec.append(cur.val) # 中
            inorder(cur.right, vec) # 右

        inorder(root, res)
        return res
1.2.1.2 Java - 中序遍历的递归实现
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        inorder(root, res);
        return res;
    }

    public void inorder(TreeNode cur, List<Integer> temp) {
        // 终止条件
        if (cur == null) {
            return;
        }

        inorder(cur.left, temp);    // 左
        temp.add(cur.val);          // 中 
        inorder(cur.right, temp);   // 右
    }
}
1.2.2 中序遍历的迭代实现
  • 中序遍历的迭代实现思路与前序遍历不同
  • 前序遍历迭代中一共有两个步骤:
    • 访问:从根节点root开始,不断向下遍历,将遍历到的元素放入栈stack
    • 处理:栈stack中弹出栈顶节点,记录栈顶节点的值
  • 前序遍历的顺序是中左右,访问的节点和处理的节点是同一个节点
  • 中序遍历的顺序是左中右,从根节点root开始,一层层往下遍历直到树的最底部,然后才开始处理节点;所以在中序遍历中,需要保证在左子树访问完之前,当前的元素不能出栈

具体算法:

  • 判断二叉树是否为空,如果为空返回
  • 初始化空栈stack,初始化返回数组res
  • 当前节点或栈不为空时:
    • 如果当前节点不为空,循环遍历当前节点的左子树,并将当前子树的根节点加入栈中
    • 如果当前节点为空,说明已经遍历到子树的左边最底部,栈stack弹出栈顶元素node,并记录该元素的值;然后将访问当前栈顶元素的右子树
  • 注意循环条件为当前节点cur不为空或者栈stack不为空,即while cur or stack
    • 如果当前节点cur不为空,但是栈stack为空,说明当前节点cur是一个当前子树的根节点,还需要继续向下访问(继续向下遍历)
    • 如果当前节点cur为空,但是栈stack不为空,说明已经遍历到当前子树的最底层,需要开始处理节点(即栈弹出栈顶元素并记录)
    • 只有当前节点cur为空和栈stack也为空时,说明整棵树都遍历到,遍历结束
1.2.2.1 Python - 中序遍历的迭代实现
# 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: TreeNode) -> List[int]:
        res = list()

        # 空树
        if not root:
            return res

        # list as stack
        stack = list()

        cur = root
        while cur or stack:
            if cur:
                # 从根节点依次向下访问
                stack.append(cur)
                cur = cur.left
            else:
                # not cur: 说明遍历到子树的最左节点
                # 处理当前节点并访问当前节点的右子树
                cur = stack.pop()
                res.append(cur.val)
                cur = cur.right
        
        return res
1.2.2.2 Java - 中序遍历的迭代实现
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();

        // 空树
        if (root == null) {
            return res;
        }

        // deque as stack
        Deque<TreeNode> stack = new ArrayDeque<>();

        TreeNode cur = root;
        
        // 当前节点不为空或栈不为空 
        while (cur != null || !stack.isEmpty()) {
            if (cur != null) {
                // 继续向下访问
                stack.push(cur);
                cur = cur.left;
            } else {
                // 处理栈顶节点,并访问栈顶节点右子树
                cur = stack.pop();
                res.add(cur.val);
                cur = cur.right;
            }
        }

        return res;
    }
}

1.3 后序遍历 Postorder Traversal

后序遍历规则:

  • 如果树为空,则返回
  • 如果树不为空,则:
    • 按照后序遍历方式遍历根节点的左子树
    • 按照后序遍历方式遍历根节点的右子树
    • 访问根节点

所以后序遍历顺序为: -> ->

1.3.1 后序遍历的递归实现

后序遍历递归的基本思路与前序遍历递归、中序遍历递归一致。

后序遍历的递归实现:

  • 确定递归的 参数和返回值: 需要传入的参数为当前节点,因为要打印出后序遍历节点的值,所以参数需要一个数组来存放前序遍历节点的值,不需要返回值
# LC145-二叉树的后序遍历 
    def postorder(cur: Optional[TreeNode], res: List[int]) -> None:
  • 确定递归的 终止条件:遍历过程中,如果当前节点为空,说明本层遍历结束,所以终止条件为当前节点为空就返回
        if cur == None: return
  • 确定 单层递归的逻辑:后序遍历顺序为左右中,所以单层递归时,要先取根节点的左子树的值,然后右子树的值,最后根节点
        postorderTraversal(cur.left, res)   # 左
        postorderTraversal(cur.right, res)  # 右
        res.append(cur.val)                 # 中

完整代码:

1.3.1.1 Python - 后序遍历的递归实现
# 后序遍历-递归-LC145_二叉树的后序遍历
# 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: TreeNode) -> List[int]:
        # 后序遍历的递归实现
        res = list()

        def postorder(cur: TreeNode, vec: List[int]) -> None:
            # 终止条件
            if not cur: return

            postorder(cur.left, vec)    # 左
            postorder(cur.right, vec)   # 右
            vec.append(cur.val)         # 中

        postorder(root, res)
        return res
1.3.1.2 Java - 后序遍历的递归实现
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        postorder(root, res);
        return res;
    }

    public void postorder(TreeNode cur, List<Integer> temp) {
        // 终止条件
        if (cur == null) {
            return;
        }

        postorder(cur.left, temp);    // 左
        postorder(cur.right, temp);   // 右
        temp.add(cur.val);            // 中 
    }
}
1.3.2 后序遍历的迭代实现
  • 后续遍历的迭代实现基本思路与前序遍历一致
  • 后续遍历的顺序是左右中,前序遍历的顺序是中左右,具体更改如下:
    • 在原本前序遍历的基础上,先修改访问和处理节点的顺序,更改为左子树入栈,再右子树入栈,此时访问的顺序(即出栈的顺序)变为 中右左
    • 最后返回数组res时,反转数组,顺序变为 左右中

具体算法:

  • 判断二叉树是否为空,如果为空返回
  • 初始化返回数组res
  • 初始化空栈stack,把根节点root压入栈中
  • 当栈stack不为空时:
    • 处理栈顶元素,栈弹出当前栈顶元素noderes记录该节点的值
    • 如果node左节点不为空,访问左节点并入栈
    • 然后(注意顺序),如果node右节点不为空,访问右节点并入栈
  • 此时返回数组res的顺序为中右左,最后反转返回数组res,顺序为左右中
1.3.2.1 Python - 后序遍历的迭代实现
# 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]:
        res = list()

        # 空树
        if not root:
            return res
        
        # list as stack
        stack = [root]

        while stack: # 出栈顺序:中右左
            # 中
            node = stack.pop()
            res.append(node.val)

            # 左
            if node.left:
                stack.append(node.left)

            # 右
            if node.right:
                stack.append(node.right)
        
        # 反转res:左右中
        return res[::-1]
1.3.2.2 Java - 后序遍历的迭代实现
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();

        // 空树
        if (root == null) {
            return res;
        }

        // deque as stack
        Deque<TreeNode> stack = new ArrayDeque<>();
        stack.push(root);

        // 出栈顺序:中右左
        while (!stack.isEmpty()) {
            // 中
            TreeNode node = stack.pop();
            res.add(node.val);

            // 左
            if (node.left != null) {
                stack.push(node.left);
            } 
            
            // 右
            if (node.right != null) {
                stack.push(node.right);
            }
        }

        // 反转res:左右中
        Collections.reverse(res);
        return res;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值