代码随想录算法训练营第13天 |二叉树的学习

目录

二叉树

理论基础

二叉树的分类

1. 满二叉树 (Full Binary Tree)

2. 完全二叉树 (Complete Binary Tree)

3. 平衡二叉树 (Balanced Binary Tree)

5. 二叉搜索树 (Binary Search Tree, BST)

二叉树的存储

1. 链式存储 (Linked Representation)

2. 顺序存储 (Sequential Representation)

遍历方式

深度优先搜索

DFS 的工作原理

基本步骤:

广度优先搜索

BFS 的工作原理

基本步骤:

二叉树的遍历

递归遍历

前序遍历

中序遍历

后序遍历

非递归遍历

前序遍历

中序遍历

后续遍历

层序遍历


二叉树

理论基础

二叉树的分类

1. 满二叉树 (Full Binary Tree)
  • 定义:满二叉树是一种二叉树,除了叶子节点外,每个节点都有两个子节点,深度为h的完美二叉树有2^h - 1个节点
  • 特点:所有非叶子节点都有两个子节点,且所有叶子节点都在同一层。
2. 完全二叉树 (Complete Binary Tree)
  • 定义:完全二叉树是一种二叉树,所有层都被完全填满,除了可能是最后一层,且最后一层的所有节点尽可能靠左。
  • 特点:没有缺失的节点,除非是在最后一层的最右边。
3. 平衡二叉树 (Balanced Binary Tree)
  • 定义:平衡二叉树是一种二叉树,其中每个节点的左子树和右子树的高度差不超过1,空树。
  • 特点:平衡二叉树保证了二叉树的高度尽可能低,从而优化了查找、插入和删除操作的效率。
5. 二叉搜索树 (Binary Search Tree, BST)
  • 定义:二叉搜索树是一种有序二叉树,其中每个节点的值大于其左子树中所有节点的值,小于其右子树中所有节点的值。
  • 特点:在理想情况下(即树是平衡的),查找、插入、删除操作的时间复杂度为O(log n)。

二叉树的存储

二叉树的存储方式有多种,常见的主要有以下两种方法:链式存储顺序存储。每种方法都有其适用场景和优缺点。

1. 链式存储 (Linked Representation)

链式存储是二叉树最常用的存储方式,特别适合用于动态变化的树结构。

特点:

  • 每个节点用一个对象或结构体来表示,包含节点的值、左子节点指针、右子节点指针,和(有时)指向父节点的指针。
  • 链式存储易于动态地添加或删除节点,不需要预先分配大量的连续内存。

节点结构

在典型的链式存储中,每个节点包含三个主要部分:

  • 值/数据域:存储节点的数据。
  • 左指针域:指向左子节点。
  • 右指针域:指向右子节点。
class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;

    TreeNode(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}
2. 顺序存储 (Sequential Representation)

顺序存储是将二叉树节点按某种规则顺序存储在数组中的方式,通常适用于完全二叉树或满二叉树。

特点

  • 二叉树的节点按照层次从上到下、从左到右存储在数组中。
  • 数组的第一个位置(通常从索引0或1开始)存储根节点,其他节点按照其在二叉树中的位置依次存储。

节点位置关系

假设父节点在数组中的索引为i,则:

  • 左子节点的索引2*i + 1 (或 2*i,如果数组从1开始)
  • 右子节点的索引2*i + 2 (或 2*i + 1,如果数组从1开始)
  • 父节点的索引(i-1)/2 (对于索引从0开始的情况)

遍历方式

与图论中的深度优先搜索广度优先搜索是一致的。

深度优先搜索

深度优先搜索(Depth-First Search,简称DFS)是一种遍历或搜索树或图数据结构的算法。它优先深入到树或图的一个分支的尽可能深的节点,然后再回溯并探索其他分支。这种策略使得DFS能够深入到数据结构的最深处,然后逐层返回,直到遍历所有节点。

前序遍历:中左右

中序遍历:左中右

后序遍历:左右中

DFS 的工作原理

DFS 使用栈结构来实现遍历的过程。栈可以通过显式使用栈数据结构来实现,也可以通过递归调用来隐式实现

基本步骤
  1. 访问当前节点
  2. 标记该节点为已访问,以避免重复访问。
  3. 递归地访问该节点的未被访问的相邻节点,对于树结构来说,这意味着先访问左子树,然后访问右子树。
  4. 当节点的所有相邻节点都已被访问或没有未被访问的相邻节点时,回溯到前一个节点,然后继续访问其他未被访问的节点。
  5. 重复以上步骤,直到所有节点都被访问

递归实现:

class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;

    TreeNode(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

public class DFS {
    public void depthFirstSearch(TreeNode node) {
        if (node == null) {
            return;
        }

        // 访问当前节点
        System.out.print(node.value + " ");

        // 递归地访问左子节点
        depthFirstSearch(node.left);

        // 递归地访问右子节点
        depthFirstSearch(node.right);
    }

    public static void main(String[] args) {
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);
        root.left.right = new TreeNode(5);

        DFS dfs = new DFS();
        dfs.depthFirstSearch(root);  // 输出: 1 2 4 5 3
    }
}

栈实现:

class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;

    TreeNode(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

public class DFS {
    public void depthFirstSearch(TreeNode node) {
        if (node == null) {
            return;
        }

        // 访问当前节点
        System.out.print(node.value + " ");

        // 递归地访问左子节点
        depthFirstSearch(node.left);

        // 递归地访问右子节点
        depthFirstSearch(node.right);
    }

    public static void main(String[] args) {
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);
        root.left.right = new TreeNode(5);

        DFS dfs = new DFS();
        dfs.depthFirstSearch(root);  // 输出: 1 2 4 5 3
    }
}

广度优先搜索

广度优先搜索(Breadth-First Search,简称 BFS)是一种遍历或搜索树或图数据结构的算法。与深度优先搜索(DFS)不同,BFS 从起始节点开始,首先访问其所有邻居节点,然后再逐层向下深入到下一级的节点。这种逐层访问的方式确保了在无权重图中,从起始节点到任意其他节点的最短路径可以通过 BFS 得到。

BFS 的工作原理

BFS 使用队列(Queue)数据结构来实现逐层遍历的过程。队列遵循先进先出(FIFO)的原则,确保先进入队列的节点先被处理。

基本步骤
  1. 将起始节点加入队列,并标记为已访问。
  2. 从队列中取出一个节点,访问该节点,并将其所有未被访问的相邻节点加入队列。
  3. 标记这些相邻节点为已访问,以防止重复访问。
  4. 重复步骤 2 和 3,直到队列为空,表示所有节点都已被访问。

使用队列实现:

import java.util.LinkedList;
import java.util.Queue;

class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;

    TreeNode(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

public class BFS {
    public void breadthFirstSearch(TreeNode root) {
        if (root == null) {
            return;
        }

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            System.out.print(node.value + " ");

            // 将左子节点加入队列
            if (node.left != null) {
                queue.offer(node.left);
            }

            // 将右子节点加入队列
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }

    public static void main(String[] args) {
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);
        root.left.right = new TreeNode(5);

        BFS bfs = new BFS();
        bfs.breadthFirstSearch(root);  // 输出: 1 2 3 4 5
    }
}

二叉树的遍历

递归遍历

1.确定递归函数的参数和返回值

2.确定终止条件

3.确定单层递归的逻辑

前序遍历
 public List<Integer> preorderTraversal(TreeNode root) {

        List<Integer> result = new ArrayList<>();

        preorder(root, result);

        return result;
    }

    public void preorder(TreeNode root, List<Integer> result) {
        if (root == null) {
            return;
        }
        result.add(root.val);
        preorder(root.left, result);
        preorder(root.right, result);
    }
中序遍历
public List<Integer> inorderTraversal(TreeNode root) {

        List<Integer> result = new ArrayList<>();
        inorder(root, result);
        return result;
    }

    public void inorder(TreeNode root, List<Integer> result) {
        if (root == null) {
            return;
        }
        inorder(root.left, result);
        result.add(root.val);
        inorder(root.right, result);
    }
后序遍历
 public List<Integer> postorderTraversal(TreeNode root) {

        List<Integer> result = new ArrayList<>();
        postorder(root, result);

        return result;
    }

    public void postorder(TreeNode root, List<Integer> result) {
        if (root == null) {
            return;
        }
        postorder(root.left, result);
        postorder(root.right, result);
        result.add(root.val);
    }

非递归遍历

null在谁前就先访问谁,null后如果还要push就先访问push进去的,再访问null

前序遍历

前序遍历,访问的元素和要处理的元素一致

每次循环操纵栈中第一个节点

 public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        Stack<TreeNode> st = new Stack<>();
        //放入第一个元素,放进栈中
        if (root != null) {
            st.push(root);
        }
        while (!st.empty()) {
            //储存栈中的第一个节点
            TreeNode node = st.peek();
            if (node != null) {
                //因为在后面还要压入一次中节点
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
                if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)
                st.push(node);                          // 添加中节点
                st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。

            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.peek();    // 重新取出栈中元素
                st.pop();
                result.add(node.val); // 加入到结果集
            }
        }
        return result;
    }

中序遍历

先看节点的左孩子为空则弹出中间,右孩子也为空则继续弹出前一个节点

左孩子不为空,则继续用栈收集

左孩子为空,弹出中间,右孩子不为空,收集右孩子,再看右孩子的左孩子是否为空

 public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        Stack<TreeNode> st = new Stack<>();
        if (root != null) st.push(root);
        while (!st.empty()) {
            TreeNode node = st.peek();
            if (node != null) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
                st.push(node);                          // 添加中节点
                st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。

                if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.peek();    // 重新取出栈中元素
                st.pop();
                result.add(node.val); // 加入到结果集
            }
        }
        return result;
    }
后续遍历

将左右交换,然后再将数组颠倒

  public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        Stack<TreeNode> st = new Stack<>();
        if (root != null) st.push(root);
        while (!st.empty()) {
            TreeNode node = st.peek();
            if (node != null) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                st.push(node);                          // 添加中节点
                st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
                if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
                if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)         
                               
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.peek();    // 重新取出栈中元素
                st.pop();
                result.add(node.val); // 加入到结果集
            }
        }
        return result;
   }

层序遍历

每次循环,先得到一个队列的大小为size,然后得到左右孩子,每次弹出一个,直到弹出size个,

下次循环再得到size

// 102.二叉树的层序遍历
class Solution {
    public List<List<Integer>> resList = new ArrayList<List<Integer>>();

    public List<List<Integer>> levelOrder(TreeNode root) {
        //checkFun01(root,0);
        checkFun02(root);

        return resList;
    }

    //BFS--递归方式
    public void checkFun01(TreeNode node, Integer deep) {
        if (node == null) return;
        deep++;

        if (resList.size() < deep) {
            //当层级增加时,list的Item也增加,利用list的索引值进行层级界定
            List<Integer> item = new ArrayList<Integer>();
            resList.add(item);
        }
        resList.get(deep - 1).add(node.val);

        checkFun01(node.left, deep);
        checkFun01(node.right, deep);
    }

    //BFS--迭代方式--借助队列
    public void checkFun02(TreeNode node) {
        if (node == null) return;
        Queue<TreeNode> que = new LinkedList<TreeNode>();
        que.offer(node);

        while (!que.isEmpty()) {
            List<Integer> itemList = new ArrayList<Integer>();
            int len = que.size();

            while (len > 0) {
                TreeNode tmpNode = que.poll();
                itemList.add(tmpNode.val);

                if (tmpNode.left != null) que.offer(tmpNode.left);
                if (tmpNode.right != null) que.offer(tmpNode.right);
                len--;
            }

            resList.add(itemList);
        }

    }
}

  • 16
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值