第五章 树

94 阅读22分钟

第五章 树

参考文章:www.hello-algo.com/

代码地址:github.com/Liucc-123/d…

1、二叉树

1.1、二叉树的基本概念

  • 定义:二叉树是一种非线性数据结构,每个节点最多有两个子节点,分别称为左子节点和右子节点。
  • 节点结构:每个节点包含:
    • 一个值(val
    • 一个指向左子节点的引用(left
    • 一个指向右子节点的引用(right
    /* 二叉树节点类 */
    class TreeNode {
        int val;         // 节点值
        TreeNode left;   // 左子节点引用
        TreeNode right;  // 右子节点引用
        TreeNode(int x) { val = x; }
    }
    

在树结构中,每个节点都有两个指针,分别指向左子节点和右子节点,该节点被称为这两个字节点的父结点。其左子节点及其以下所有节点所形成的树称为该节点的左子树。同理,可得右子树。

**在二叉树中,除了叶子节点,其他所有节点都包括子节点和非空子树。**如下图,如果将节点 2 视为父结点,那么节点 4 是其左子节点,节点 5 是其右子节点,节点 4 及其底下所有节点形成的树是节点 2 的左子树,节点 5 及其底下所有节点形成的树称为节点 2 的右子树。

image.png

1.2、二叉树的常见术语

  • 根节点:位于顶层,没有父节点。
  • 叶节点:没有子节点的节点,两个指针均指向 null。
  • :连接两个节点的线段。
  • 节点所在的层:从根节点开始,逐层向下递增,根节点所在的层是 1。
  • 节点的度:节点的子节点的数量。在二叉树中,节点的度的取值范围是0、1 或 2。
  • 节点的深度:从根节点到该节点所经过边的数量。
  • 节点的高度: 从该节点到最远叶节点所经过边的数量。
  • 二叉树的高度:从根节点到最远叶节点的边的数量。

image.png

请注意,我们通常将“高度”和“深度”定义为“经过的边的数量”,但有些题目或教材可能会将其定义为“经过的节点的数量”。在这种情况下,高度和深度都需要加 1 。

1.3、 二叉树的基本操作

  • 初始化:创建节点并构建引用关系。

    示例代码(Java):

    // 初始化节点
    TreeNode n1 = new TreeNode(1);
    TreeNode n2 = new TreeNode(2);
    TreeNode n3 = new TreeNode(3);
    TreeNode n4 = new TreeNode(4);
    TreeNode n5 = new TreeNode(5);
    // 构建节点之间的引用(指针)
    n1.left = n2;
    n1.right = n3;
    n2.left = n4;
    n2.right = n5;
    
  • 插入与删除节点

    • 插入:通过修改指针将新节点插入到指定位置。
    • 删除:修改指针以移除节点,可能需要调整子树。

image.png

- 示例代码(Java):
    
    ```python
    TreeNode P = new TreeNode(0);
    // 在 n1 -> n2 中间插入节点 P
    n1.left = P;
    P.left = n2;
    // 删除节点 P
    n1.left = n2;
    ```
    

1.4、常见二叉树类型

  • 完美二叉树

    • 每层节点都被完全填满。
    • 叶节点的度为 0,其余节点的度为 2。
    • 节点总数为 (2^{h+1} - 1),其中 (h) 是树的高度。
    💡

    请注意,完美二叉树也常被称为满二叉树。

image.png

  • 完全二叉树
    • 只有最底层的节点未被填满,但尽量靠左填充。

    • 完美二叉树是完全二叉树的特例。

image.png

  • 完满二叉树
    • 除了叶节点外,所有节点都有两个子节点。

image.png

  • 平衡二叉树
    • 任意节点的左子树和右子树的高度差不超过 1。

image.png

1.5、二叉树的退化

  • 理想情况:完美二叉树,高度为 (O(\log n)),操作效率高。

  • 最差情况:退化为链表,高度为 (O(n)),操作效率低。

image.png

  • 对比

    特性完美二叉树链表
    第 (i) 层节点数(2^{i-1})1
    高度为 (h) 的叶节点数(2^h)1
    高度为 (h) 的节点总数(2^{h+1} - 1)(h + 1)
    节点总数为 (n) 的树的高度(\log_2(n + 1) - 1)(n - 1)

2、二叉树遍历

1. 二叉树遍历概述

  • 定义:二叉树遍历是指按照某种顺序访问二叉树中的所有节点,确保每个节点恰好被访问一次。
  • 重要性:遍历是二叉树操作的基础,广泛应用于搜索、排序、统计等场景。

2. 遍历方式分类

二叉树的遍历方式主要分为两大类:

  1. 层序遍历(Level-order Traversal):按层次顺序从上到下、从左到右访问节点,也是广度优先搜索的一种。
  2. 深度优先遍历(Depth-First Traversal):包括前序遍历、中序遍历和后序遍历,基于递归或栈实现。

3. 层序遍历

3.1 算法原理

  • 核心思想:借助队列实现广度优先搜索(BFS),逐层访问节点。
  • 步骤
    1. 初始化队列,将根节点入队,初始化一个动态数组,保存遍历二叉树的序列。
    2. 当队列非空时,执行以下操作:
      • 弹出队首节点,访问该节点。
      • 将该节点的左子节点(若存在)入队。
      • 将该节点的右子节点(若存在)入队。
    3. 重复上述过程,直到队列为空。

image.png

3.2 代码实现(Java)

package com.liucc.chapter_tree;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import com.liucc.utils.PrintUtil;
import com.liucc.utils.TreeNode;

/**
 * 层序遍历(广度优先搜索) 遍历树
 */
public class binary_tree_bfs {

    public static List<Integer> levelOrder(TreeNode root){
        // 初始化队列
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        // 初始化一个列表,保存遍历序列
        List<Integer> res = new ArrayList<>();
        // 遍历队列
        while (!queue.isEmpty()) {
            TreeNode poll = queue.poll(); // 出队
            res.add(poll.val); // 访问
            if (poll.left!= null) {
                queue.offer(poll.left); // 左子节点入队
            }else if (poll.right != null) {
                queue.offer(poll.right); // 右子节点入队
            }
        }
        return res;
    }

    public static void main(String[] args) {
        /* 初始化二叉树 */
        // 这里借助了一个从数组直接生成二叉树的函数
        TreeNode root = TreeNode.listToTree(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
        System.out.println("\n初始化二叉树\n");
        PrintUtil.printTree(root);

        /* 层序遍历 */
        List<Integer> list = levelOrder(root);
        System.out.println("\n层序遍历的节点打印序列 = " + list);
    }
}

3.3 复杂度分析

  • 时间复杂度:O(n),每个节点被访问一次。
  • 空间复杂度:O(n),在最坏情况下(满二叉树),队列中可能存储约 n/2 个节点。

4. 深度优先遍历

image.png 深度优先遍历基于递归或栈实现,按照递归的顺序访问节点。根据访问根节点的时机,分为三种遍历方式:

4.1 前序遍历(Pre-order Traversal)

  • 访问顺序:根节点 -> 左子树 -> 右子树。
  • 代码实现(Java)
void preOrder(TreeNode root) {
    if (root == null)
        return;
    // 访问优先级:根节点 -> 左子树 -> 右子树
    list.add(root.val);
    preOrder(root.left);
    preOrder(root.right);
}

4.2 中序遍历(In-order Traversal)

  • 访问顺序:左子树 -> 根节点 -> 右子树。
  • 代码实现(Java)
void inOrder(TreeNode root) {
    if (root == null)
        return;
    // 访问优先级:左子树 -> 根节点 -> 右子树
    inOrder(root.left);
    list.add(root.val);
    inOrder(root.right);
}

4.3 后序遍历(Post-order Traversal)

  • 访问顺序:左子树 -> 右子树 -> 根节点。
  • 代码实现(Java)
void postOrder(TreeNode root) {
    if (root == null)
        return;
    // 访问优先级:左子树 -> 右子树 -> 根节点
    postOrder(root.left);
    postOrder(root.right);
    list.add(root.val);
}

4.4 复杂度分析

  • 时间复杂度:O(n),每个节点被访问一次。
  • 空间复杂度:O(n),递归调用栈的深度在最坏情况下(树退化为链表)为 n。

4.5 完整代码

package com.liucc.chapter_tree;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.liucc.utils.PrintUtil;
import com.liucc.utils.TreeNode;

/**
 * 深度优先搜索 遍历树
 */
public class binary_tree_dfs {
    static List<Integer> list = new ArrayList<>();
    //前序遍历 根->左->右
    static void preOrder(TreeNode root){
        if (root == null) {
            return;
        }
        list.add(root.val);
        preOrder(root.left);
        preOrder(root.right);
    }

    // 中序遍历 左->根->右
    static void inOrder(TreeNode root){
        if (root == null) {
            return;
        }
        inOrder(root.left);
        list.add(root.val);
        inOrder(root.right);
    }

    // 后序遍历 左->右->根
    static void postOrder(TreeNode root){
        if (root == null) {
            return;
        }
        postOrder(root.left);
        postOrder(root.right);
        list.add(root.val);
    }

    public static void main(String[] args) {
        /* 初始化二叉树 */
        // 这里借助了一个从数组直接生成二叉树的函数
        TreeNode root = TreeNode.listToTree(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
        System.out.println("\n初始化二叉树\n");
        PrintUtil.printTree(root);

        // 前序遍历
        preOrder(root);
        System.out.println("\n前序遍历序列打印:" + list.toString());
        // 中序遍历
        list.clear();
        inOrder(root);
        System.out.println("\n中序遍历序列打印:" + list.toString());
        // 后序遍历
        list.clear();
        postOrder(root);
        System.out.println("\n后序遍历序列打印:" + list.toString());
    }
}

5. 遍历的应用

  • 层序遍历:适用于按层次处理节点,例如计算树的宽度、逐层打印节点。
  • 前序遍历:常用于复制树结构、序列化二叉树。
  • 中序遍历:在二叉搜索树中,中序遍历的结果是有序的,可用于排序和查找。
  • 后序遍历:常用于删除树节点、计算树的后序表达式。

3、二叉树数组表示

在之前的章节中,我们二叉树是通过链表实现的,二叉树的基本单位是TreeNode ,节点之间通过指针相互连接。那么通过数组是否可以实现二叉树这种数据结构呢?答案肯定也是可以的。

3.1、完美二叉树的数组表示

  • 定义:完美二叉树(完全二叉树)是一种特殊的二叉树,其中每一层(除最后一层外)的节点都完全填满,并且所有叶子节点都位于最后一层。
  • 数组表示方法:将完美二叉树的节点按照层序遍历的顺序存储到数组中。每个节点的索引与其子节点和父节点的索引存在固定关系:
    • 父节点索引:(i - 1) / 2
    • 左子节点索引:2 * i + 1
    • 右子节点索引:2 * i + 2
  • 优点:这种表示方法简单且高效,适用于完美二叉树。

image.png

3.2、任意二叉树的数组表示

  • 问题:对于非完美二叉树,直接按层序遍历存储会导致数组中存在大量空位(None),无法唯一表示二叉树结构。

image.png

  • 解决方案:在数组中显式地存储所有空节点(None),使数组能够唯一表示任意二叉树。

  • 示例代码

    tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
    

image.png

3.3、代码实现

以下代码实现了一棵基于数组表示的二叉树,包括以下几种操作。

  • 给定某节点,获取它的值、左(右)子节点、父节点。
  • 获取前序遍历、中序遍历、后序遍历、层序遍历序列。
package com.liucc.chapter_tree;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.liucc.utils.PrintUtil;
import com.liucc.utils.TreeNode;

/**
 * 基于数组实现的二叉树
 */
class ArrayBinaryTree{
    private List<Integer> tree;
    public ArrayBinaryTree(List<Integer> arr){
        this.tree = arr;
    }
    // 列表容量
    public int size(){
        return tree.size();
    }
    // 获取索引为 i节点的值
    public Integer val(int i){
        // 索引越界,返回 null
        if (i < 0 || i >= size()) {
            return null;
            
        }
        return tree.get(i);
    }
    // 获取索引为 i 左子节点的索引
    public Integer left(int i){
        return 2 * i + 1;
    }
    // 获取索引为 i 右子节点的索引
    public Integer right(int i){
        return 2 * i + 2;
    }
    // 获取索引为 i 父节点的索引
    public Integer parent(int i){
        return (i - 1) / 2;
    }
    // 层序遍历
    public List<Integer> levelOrder(){
        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < size(); i++) {
            if (val(i) != null) {
                res.add(val(i));
            }
        }
        return res;
    }

    // 深度优先遍历
    /**
     * 
     * @param i 当前节点索引
     * @param order 遍历方式 pre:前序、in:中序、post:后序
     * @param res 遍历结果
     */
    public void dfs(Integer i, String order, List<Integer> res){
        // 如果为空位,直接返回
        if (val(i) == null) {
            return;
        }
        if ("pre".equals(order)) {
            res.add(val(i));
        }
        dfs(left(i), order, res);
        if ("in".equals(order)) {
            res.add(val(i));
        }
        dfs(right(i), order, res);
        if ("post".equals(order)) {
            res.add(val(i));
        }
    }
    // 前序遍历
    public List<Integer> preOrder(){
        List<Integer> res = new ArrayList<>();
        dfs(0, "pre", res);
        return res;
    }
    // 中序遍历
    public List<Integer> inOrder(){
        List<Integer> res = new ArrayList<>();
        dfs(0, "in", res);
        return res;
    }
    // 后序遍历
    public List<Integer> postOrder(){
        List<Integer> res = new ArrayList<>();
        dfs(0, "post", res);
        return res;
    }
}

public class array_binary_tree {

    public static void main(String[] args) {
        // 初始化二叉树
        // 这里借助了一个从数组直接生成二叉树的函数
        List<Integer> arr = Arrays.asList(1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15);

        TreeNode root = TreeNode.listToTree(arr);
        System.out.println("\n初始化二叉树\n");
        System.out.println("二叉树的数组表示:");
        System.out.println(arr);
        System.out.println("二叉树的链表表示:");
        PrintUtil.printTree(root);

        // 数组表示下的二叉树类
        ArrayBinaryTree abt = new ArrayBinaryTree(arr);

        // 访问节点
        int i = 1;
        Integer l = abt.left(i);
        Integer r = abt.right(i);
        Integer p = abt.parent(i);
        System.out.println("\n当前节点的索引为 " + i + " ,值为 " + abt.val(i));
        System.out.println("其左子节点的索引为 " + l + " ,值为 " + (l == null ? "null" : abt.val(l)));
        System.out.println("其右子节点的索引为 " + r + " ,值为 " + (r == null ? "null" : abt.val(r)));
        System.out.println("其父节点的索引为 " + p + " ,值为 " + (p == null ? "null" : abt.val(p)));

        // 遍历树
        List<Integer> res = abt.levelOrder();
        System.out.println("\n层序遍历为:" + res);
        res = abt.preOrder();
        System.out.println("前序遍历为:" + res);
        res = abt.inOrder();
        System.out.println("中序遍历为:" + res);
        res = abt.postOrder();
        System.out.println("后序遍历为:" + res);
    }
}

3.4、优点与局限性

  • 优点
    • 空间效率:不需要存储指针,节省空间。
    • 访问效率:数组存储在连续内存中,对缓存友好,访问速度快。
    • 随机访问:可以直接通过索引访问任意节点。
  • 局限性
    • 内存限制:数组需要连续内存空间,不适合存储数据量过大的树。
    • 动态操作:增删节点需要通过数组插入或删除操作实现,效率较低。
    • 空间浪费:当二叉树中存在大量空节点时,数组中实际存储的节点数据比重较低,空间利用率低。

4、二叉搜索树

定义

二叉搜索树(BST)是一种特殊的二叉树,满足以下条件:

  1. 对于根节点,左子树中所有节点的值 < 根节点的值 < 右子树中所有节点的值。
  2. 任意节点的左、右子树也是二叉搜索树。

image.png

二叉搜索树的操作

1. 查找节点

  • 目标:在BST(二叉搜索树)中查找一个目标值num

  • 过程

    • 从根节点开始,比较目标值num与当前节点值cur.val
    • 如果cur.val < num,则向右子树移动;如果cur.val > num,则向左子树移动。
    • 如果cur.val == num,则找到目标节点。
  • 时间复杂度:平均情况下为O(log n),最坏情况下为O(n)(树退化为链表)。

  • 代码实现

    /* 查找节点 */
    TreeNode search(int num) {
        TreeNode cur = root;
        // 循环查找,越过叶节点后跳出
        while (cur != null) {
            // 目标节点在 cur 的右子树中
            if (cur.val < num)
                cur = cur.right;
            // 目标节点在 cur 的左子树中
            else if (cur.val > num)
                cur = cur.left;
            // 找到目标节点,跳出循环
            else
                break;
        }
        // 返回目标节点
        return cur;
    }
    

2. 插入节点

image.png

  • 目标:在BST中插入一个新值num

  • 过程

    • 如果树为空,初始化根节点为TreeNode(num)
    • 否则,从根节点开始,根据num与当前节点值的大小关系,找到插入位置。
    • 如果cur.val < num,则向右子树移动;如果cur.val > num,则向左子树移动。
    • 如果找到重复值,则直接返回,不插入。
    • 插入新节点后,需要继续保持BST的性质。
  • 时间复杂度:平均情况下为O(log n),最坏情况下为O(n)(树退化为链表)。

  • 代码实现

    /* 插入节点 */
    void insert(int num) {
        // 若树为空,则初始化根节点
        if (root == null) {
            root = new TreeNode(num);
            return;
        }
        TreeNode cur = root, pre = null;
        // 循环查找,越过叶节点后跳出
        while (cur != null) {
            // 找到重复节点,直接返回
            if (cur.val == num)
                return;
            pre = cur;
            // 插入位置在 cur 的右子树中
            if (cur.val < num)
                cur = cur.right;
            // 插入位置在 cur 的左子树中
            else
                cur = cur.left;
        }
        // 插入节点
        TreeNode node = new TreeNode(num);
        if (pre.val < num)
            pre.right = node;
        else
            pre.left = node;
    }
    

3. 删除节点

  • 目标:从BST中删除一个值为num的节点。
  • 过程
    • 首先找到目标节点cur
    • 根据目标节点的子节点数量,分为三种情况:
      1. 子节点数量为0:直接删除该节点。

image.png

      2. **子节点数量为1**:将目标节点替换为其子节点。

image.png

      3. **子节点数量为2**:找到右子树的最小节点(或左子树的最大节点),用其值覆盖目标节点,然后删除该最小节点。

image.png

image.png

  • 时间复杂度:平均情况下为O(log n),最坏情况下为O(n)(树退化为链表)。

  • 代码实现

    /* 删除节点 */
    void remove(int num) {
        // 若树为空,直接提前返回
        if (root == null)
            return;
        TreeNode cur = root, pre = null;
        // 循环查找,越过叶节点后跳出
        while (cur != null) {
            // 找到待删除节点,跳出循环
            if (cur.val == num)
                break;
            pre = cur;
            // 待删除节点在 cur 的右子树中
            if (cur.val < num)
                cur = cur.right;
            // 待删除节点在 cur 的左子树中
            else
                cur = cur.left;
        }
        // 未找到待删除节点,则直接返回
        if (cur == null)
            return;
        // 子节点数量 = 0 or 1
        if (cur.left == null || cur.right == null) {
            // 当子节点数量 = 0 / 1 时, child = null / 该子节点
            TreeNode child = cur.left != null ? cur.left : cur.right;
            // 删除节点 cur
            if (cur != root) {
                if (pre.left == cur)
                    pre.left = child;
                else
                    pre.right = child;
            } else {
                // 若删除节点为根节点,则重新指定根节点
                root = child;
            }
        }
        // 子节点数量 = 2
        else {
            // 获取中序遍历中 cur 的下一个节点
            TreeNode tmp = cur.right;
            while (tmp.left != null) {
                tmp = tmp.left;
            }
            // 递归删除节点 tmp
            remove(tmp.val);
            // 用 tmp 覆盖 cur
            cur.val = tmp.val;
        }
    }
    

完整代码

package com.liucc.chapter_tree;

import com.liucc.utils.PrintUtil;
import com.liucc.utils.TreeNode;

/**
 * 二叉搜索树
 * 特点:左子树值 < 根节点值 < 右子树值
 */
class BinarySearchTree{
    private TreeNode root;
    public BinarySearchTree(){
        this.root = null;  // 初始化空树
    }
    // 获取根节点
    public TreeNode getRoot(){
        return root;
    }
    // 查找节点
    public TreeNode search(int num){
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val == num) { // 找到目标值
                return cur;
            }else if (cur.val > num) { // 目标值在左子树
                cur = cur.left;
            }else{ // 目标值在右子树
                cur = cur.right;
            }
        }
        return null;
    }
    /**
     * 插入节点
     * 注意事项:1、插入节点的值不能重复;2、定义节点 prev 保存父节点,方便插入操作
     */
    public void insert(int num){
        // 空树情况
        if (root == null) {
            root = new TreeNode(num);
            return;
        }
        // 循环查找,定位待插入元素位置
        TreeNode cur = root, prev = null;
        while (cur != null) {
            // 元素重复,直接结束
            if (cur.val == num) {
                System.out.println("树中已存在值为" + num + "的节点,禁止重复插入");
                return;
            }
            prev = cur;
            if (cur.val > num) { // 左子树
                cur = cur.left;
            }else{ // 右子树
                cur = cur.right;
            }
        }
        // 插入新节点
        TreeNode newNode = new TreeNode(num);
        if (prev.val > num) {
            prev.left = newNode;
        }else{
            prev.right = newNode;
        }
    }  
    
    /**
     * 删除节点
     * 注意事项:节点删除后,依然要保持二叉搜索书的特点
     * @param num
     */
    public void remove(int num){
        // 如果树为空,提前返回
        if (root == null) {
            return;
        }
        TreeNode cur = root, prev = null;
        // 1、循环查找,定位待删除节点位置
        while (cur != null) {
            // 找到目标节点,退出循环
            if (cur.val == num) {
                break;
            }
            prev = cur;
            if (cur.val > num) { // 左子树找
                cur = cur.left;
            }else{ // 右子树找
                cur = cur.right;
            }
        }
        // 2、删除节点
        // 未找到待删除节点
        if (cur == null) {
            System.out.println("未找到待删除节点");
            return;
        }
        // 2.1、待删除节点的度为 0 或 1
        if (cur.left == null || cur.right == null) {
            TreeNode child = cur.left !=null ? cur.left : cur.right;
            if (prev.val > cur.val) { // 挂在父结点的左子树
                prev.left = child;
            }else{  // 挂在右子树
                prev.right = child;
            }
        }
        // 2.2、度为 2,不可直接删除,找到右子树中的最小节点,来替换删除节点
        else{
            // 中序遍历查找
            TreeNode temp = cur.right;
            while (temp.left != null) {
                temp = temp.left;
            }
            // 递归删除
            remove(temp.val);
            // 替换待删除节点
            cur.val = temp.val;
        }
    }
}

public class binary_search_tree {
    public static void main(String[] args) {
        /* 初始化二叉搜索树 */
        BinarySearchTree bst = new BinarySearchTree();
        // 请注意,不同的插入顺序会生成不同的二叉树,该序列可以生成一个完美二叉树
        int[] nums = { 8, 4, 12, 2, 6, 10, 14, 1, 3, 5, 7, 9, 11, 13, 15 };
        for (int num : nums) {
            bst.insert(num);
        }
        System.out.println("\n初始化的二叉树为\n");
        PrintUtil.printTree(bst.getRoot());

        /* 查找节点 */
        TreeNode node = bst.search(7);
        System.out.println("\n查找到的节点对象为 " + node + ",节点值 = " + node.val);

        /* 插入节点 */
        bst.insert(16);
        System.out.println("\n插入节点 16 后,二叉树为\n");
        PrintUtil.printTree(bst.getRoot());

        /* 删除节点 */
        bst.remove(1);
        System.out.println("\n删除节点 1 后,二叉树为\n");
        PrintUtil.printTree(bst.getRoot());
        bst.remove(2);
        System.out.println("\n删除节点 2 后,二叉树为\n");
        PrintUtil.printTree(bst.getRoot());
        bst.remove(4);
        System.out.println("\n删除节点 4 后,二叉树为\n");
        PrintUtil.printTree(bst.getRoot());
    }
}

4. 中序遍历有序

  • 目标:通过中序遍历BST,得到一个有序的序列。
  • 过程:中序遍历(左-根-右)会按照从小到大的顺序访问BST中的所有节点。
  • 时间复杂度O(n)

二叉搜索树的效率

  • 查找、插入、删除操作:平均情况下为O(log n),最坏情况(树退化为链表)下为O(n)
  • 中序遍历O(n)
  • 平衡二叉搜索树(如AVL树):通过自平衡操作,保证所有操作的时间复杂度为O(log n)

二叉搜索树的常见应用

  • 数据存储与检索:快速查找、插入和删除操作。
  • 排序:通过中序遍历得到有序序列。
  • 动态数据结构:支持动态插入和删除操作的数据结构。
  • 区间查询:通过BST的有序性,快速定位区间内的数据。

5、AVL树*

1. AVL树的背景与意义

在二叉搜索树(BST)中,频繁的插入和删除操作可能导致树的退化(如退化为链表),从而使查找、插入和删除操作的时间复杂度退化为O(n)。

image.png

1962年,G. M. Adelson-Velsky和E. M. Landis提出了AVL树,通过旋转操作确保树在插入和删除操作后保持平衡,从而将操作时间复杂度维持在O(log n)。

2. AVL树的基本概念

  • 定义:AVL树是一种平衡二叉搜索树,满足以下两个条件:

    1. 是一棵二叉搜索树(BST)。
    2. 每个节点的左右子树高度差(平衡因子)的绝对值不超过1。
    /* AVL 树节点类 */
    class TreeNode {
        public int val;        // 节点值
        public int height;     // 节点高度
        public TreeNode left;  // 左子节点
        public TreeNode right; // 右子节点
        public TreeNode(int x) { val = x; }
    }
    
  • 节点高度:节点的高度是从该节点到最远叶节点的边的数量。叶节点的高度为0,空节点的高度为-1。

    /* 获取节点高度 */
    int height(TreeNode node) {
        // 空节点高度为 -1 ,叶节点高度为 0
        return node == null ? -1 : node.height;
    }
    
    /* 更新节点高度 */
    void updateHeight(TreeNode node) {
        // 节点高度等于最高子树高度 + 1
        node.height = Math.max(height(node.left), height(node.right)) + 1;
    }
    
  • 平衡因子:节点的平衡因子定义为左子树高度减去右子树高度。对于任意节点,平衡因子的范围是[-1, 1]。

    /* 获取平衡因子 */
    int balanceFactor(TreeNode node) {
        // 空节点平衡因子为 0
        if (node == null)
            return 0;
        // 节点平衡因子 = 左子树高度 - 右子树高度
        return height(node.left) - height(node.right);
    }
    

3. AVL树的旋转操作

  • 旋转的目的:通过旋转操作调整树的结构,使失衡节点重新恢复平衡,同时保持二叉搜索树的性质。

  • 旋转类型

    (1)右旋:用于处理左偏树(平衡因子 > 1)的情况。

    操作步骤

    1. 将失衡节点的左子节点(child)提升为新的根节点。

    2. 将失衡节点(node)变为child的右子节点。

    3. 如果child有右子节点(grand_child),将其作为node的左子节点。

    4. 更新相关节点的高度。

image.png

image.png

image.png

image.png

如果当节点 `child` 有右子节点(记为 `grand_child` )时,需要在右旋中添加一步:将 `grand_child` 作为 `node` 的左子节点。

image.png

代码实现

/* 右旋操作 */
TreeNode rightRotate(TreeNode node) {
    TreeNode child = node.left;
    TreeNode grandChild = child.right;
    // 以 child 为原点,将 node 向右旋转
    child.right = node;
    node.left = grandChild;
    // 更新节点高度
    updateHeight(node);
    updateHeight(child);
    // 返回旋转后子树的根节点
    return child;
}

(2)左旋:用于处理右偏树(平衡因子 < -1)的情况。

操作步骤

  1. 将失衡节点的右子节点(child)提升为新的根节点。
  2. 将失衡节点(node)变为child的左子节点。
  3. 如果child有左子节点(grand_child),将其作为node的右子节点。
  4. 更新相关节点的高度。

image.png

同理,当节点 child 有左子节点(记为 grand_child )时,需要在左旋中添加一步:将 grand_child 作为 node 的右子节点。

image.png

代码实现

/* 左旋操作 */
TreeNode leftRotate(TreeNode node) {
    TreeNode child = node.right;
    TreeNode grandChild = child.left;
    // 以 child 为原点,将 node 向左旋转
    child.left = node;
    node.right = grandChild;
    // 更新节点高度
    updateHeight(node);
    updateHeight(child);
    // 返回旋转后子树的根节点
    return child;
}

(3)先左旋后右旋:用于处理左偏树中,左子节点的平衡因子 < 0 的情况。

操作步骤

  1. 对失衡节点的左子节点执行左旋。
  2. 对失衡节点执行右旋。

image.png

(4)先右旋后左旋:用于处理右偏树中,右子节点的平衡因子 > 0 的情况。

操作步骤

  1. 对失衡节点的右子节点执行右旋。
  2. 对失衡节点执行左旋。

image.png

通过判断失衡节点的平衡因子及其子节点的平衡因子的正负号,我们可以判断失衡节点属于这四类情况中的哪一种。

失衡节点的平衡因子子节点的平衡因子应采用的旋转方法
>1 (左偏树)≥0右旋
>1 (左偏树)<0先左旋后右旋
<−1 (右偏树)≤0左旋
<−1 (右偏树)>0先右旋后左旋

代码实现

/* 执行旋转操作,使该子树重新恢复平衡 */
TreeNode rotate(TreeNode node) {
    // 获取节点 node 的平衡因子
    int balanceFactor = balanceFactor(node);
    // 左偏树
    if (balanceFactor > 1) {
        if (balanceFactor(node.left) >= 0) {
            // 右旋
            return rightRotate(node);
        } else {
            // 先左旋后右旋
            node.left = leftRotate(node.left);
            return rightRotate(node);
        }
    }
    // 右偏树
    if (balanceFactor < -1) {
        if (balanceFactor(node.right) <= 0) {
            // 左旋
            return leftRotate(node);
        } else {
            // 先右旋后左旋
            node.right = rightRotate(node.right);
            return leftRotate(node);
        }
    }
    // 平衡树,无须旋转,直接返回
    return node;
}

4. AVL树的常见操作

  • 插入节点

    1. 按照BST(二叉搜索树)的插入规则插入新节点。
    2. 从插入点向上回溯,检查每个祖先节点的平衡因子。
    3. 如果发现失衡节点,根据平衡因子和子节点的平衡因子选择适当的旋转操作。

    代码实现

    /* 插入节点 */
    void insert(int val) {
        root = insertHelper(root, val);
    }
    
    /* 递归插入节点(辅助方法) */
    TreeNode insertHelper(TreeNode node, int val) {
        if (node == null)
            return new TreeNode(val);
        /* 1. 查找插入位置并插入节点 */
        if (val < node.val)
            node.left = insertHelper(node.left, val);
        else if (val > node.val)
            node.right = insertHelper(node.right, val);
        else
            return node; // 重复节点不插入,直接返回
        updateHeight(node); // 更新节点高度
        /* 2. 执行旋转操作,使该子树重新恢复平衡 */
        node = rotate(node);
        // 返回子树的根节点
        return node;
    }
    
  • 删除节点

    1. 按照BST的删除规则删除节点。
    2. 从删除点向上回溯,检查每个祖先节点的平衡因子。
    3. 如果发现失衡节点,根据平衡因子和子节点的平衡因子选择适当的旋转操作。

    代码实现

    /* 删除节点 */
    void remove(int val) {
        root = removeHelper(root, val);
    }
    
    /* 递归删除节点(辅助方法) */
    TreeNode removeHelper(TreeNode node, int val) {
        if (node == null)
            return null;
        /* 1. 查找节点并删除 */
        if (val < node.val)
            node.left = removeHelper(node.left, val);
        else if (val > node.val)
            node.right = removeHelper(node.right, val);
        else {
            if (node.left == null || node.right == null) {
                TreeNode child = node.left != null ? node.left : node.right;
                // 子节点数量 = 0 ,直接删除 node 并返回
                if (child == null)
                    return null;
                // 子节点数量 = 1 ,直接删除 node
                else
                    node = child;
            } else {
                // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点
                TreeNode temp = node.right;
                while (temp.left != null) {
                    temp = temp.left;
                }
                node.right = removeHelper(node.right, temp.val);
                node.val = temp.val;
            }
        }
        updateHeight(node); // 更新节点高度
        /* 2. 执行旋转操作,使该子树重新恢复平衡 */
        node = rotate(node);
        // 返回子树的根节点
        return node;
    }
    
  • 查找节点

    • AVL树的查找操作与普通BST相同,时间复杂度为O(log n)。

    代码实现

    public TreeNode search(int val){
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val < val) {
                cur = cur.right;
            }else if (cur.val > val) {
                cur = cur.left;
            }else
                break;
        }
        return cur;
    }
    

5. AVL树的应用场景

  • AVL树适用于需要频繁进行插入、删除和查找操作的场景,如数据库索引、符号表等。
  • 由于AVL树始终保持平衡,因此在这些场景中,AVL树能够提供高效的性能。

6. AVL树的优缺点

  • 优点
    • 操作时间复杂度稳定为O(log n)。
    • 适合频繁的增删查改操作。
  • 缺点
    • 插入和删除操作需要进行旋转,增加了操作的复杂性。
    • 旋转操作可能导致较高的维护成本。