数据结构与算法(五):树

news2024/11/26 4:45:30

参考引用

  • Hello 算法
  • Github:hello-algo

1. 二叉树

  • 二叉树(binary tree)是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑

    • 与链表类似,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用
    /* 二叉树节点结构体 */
    struct TreeNode {
        int val;          // 节点值
        TreeNode *left;   // 左子节点指针
        TreeNode *right;  // 右子节点指针
        TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    };
    
  • 每个节点都有两个指针,分别指向左子节点和右子节点,该节点被称为这两个子节点的父节点。当给定一个二叉树的节点时,将该节点的左子节点及其以下节点形成的树称为该节点的左子树,同理可得右子树

  • 在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树

    • 如果将 “节点 2” 视为父节点,则其左子节点和右子节点分别是 “节点 4” 和 “节点 5”,左子树是 “节点 4 及其以下节点形成的树”,右子树是 “节点 5 及其以下节点形成的树”

在这里插入图片描述

1.1 二叉树常见术语

  • 根节点 root node
    • 位于二叉树顶层的节点,没有父节点
  • 叶节点 leaf node
    • 没有子节点的节点,其两个指针均指向 None
  • 边 edge
    • 连接两个节点的线段,即节点指针
  • 节点所在的层 level
    • 从顶至底递增,根节点所在层为 1
  • 节点的度 degree
    • 节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2
  • 二叉树的高度 height
    • 从根节点到最远叶节点所经过的边的数量
  • 节点的深度 depth
    • 从根节点到该节点所经过的边的数量
  • 节点的高度 height
    • 从最远叶节点到该节点所经过的边的数量

在这里插入图片描述

1.2 二叉树基本操作

/* 1、初始化二叉树 */
// 与链表类似,首先初始化节点,然后构建指针
// 初始化节点
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;

/* 2、插入与删除节点 */
// 与链表类似,在二叉树中插入与删除节点可以通过修改指针来实现
TreeNode* P = new TreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1->left = P;
P->left = n2;
// 删除节点 P
n1->left = n2;

在这里插入图片描述

1.3 常见二叉树类型

1.3.1 完美二叉树(满二叉树)
  • 完美二叉树(perfect binary tree)所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 0,其余所有节点的度都为 2;若树高度为 h,则节点总数为 2 h + 1 − 1 2^{h+1}-1 2h+11,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象

在这里插入图片描述

1.3.2 完全二叉树
  • 完全二叉树(complete binary tree)只有最底层的节点未被填满,且最底层节点尽量靠左填充

在这里插入图片描述

1.3.3 完满二叉树
  • 完满二叉树(full binary tree)除了叶节点之外,其余所有节点都有两个子节点

在这里插入图片描述

1.3.4 平衡二叉树
  • 平衡二叉树(balanced binary tree)中任意节点的左子树和右子树的高度之差的绝对值不超过 1

在这里插入图片描述

1.4 二叉树的退化

  • 当二叉树的每层节点都被填满时,达到 “完美二叉树”;而当所有节点都偏向一侧时,二叉树退化为 “链表”
    • 完美二叉树是理想情况,可以充分发挥二叉树 “分治” 的优势
    • 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 O(n)

在这里插入图片描述

在这里插入图片描述

2. 二叉树遍历

从物理结构的角度来看,树是一种基于链表的数据结构,因此其遍历方式是通过指针逐个访问节点。然而,树是一种非线性数据结构,这使得遍历树比遍历链表更加复杂,需要借助搜索算法来实现

  • 二叉树常见的遍历方式包括层序遍历、前序遍历、中序遍历和后序遍历

2.1 层序遍历

  • 层序遍历(level-order traversal)从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点
    • 层序遍历本质上属于广度优先遍历(breadth-first traversal),它体现了 “一圈一圈向外扩展” 的逐层遍历思想

在这里插入图片描述

  • 广度优先遍历通常借助 “队列” 来实现。队列遵循 “先进先出” 的规则,而广度优先遍历则遵循 “逐层推进” 的规则,两者背后的思想是一致的
    /* 层序遍历 */
    // 时间复杂度:所有节点被访问一次,使用 O(n) 时间,其中 n 为节点数量
    // 空间复杂度:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 (n+1)/2 个节点,占用 O(n) 空间
    vector<int> levelOrder(TreeNode *root) {
        // 初始化队列,加入根节点
        queue<TreeNode *> queue;
        queue.push(root);
        // 初始化一个列表,用于保存遍历序列
        vector<int> vec;
        while (!queue.empty()) {
            TreeNode *node = queue.front();
            queue.pop();                 // 队列出队
            vec.push_back(node->val);    // 保存节点值
            if (node->left != nullptr)
                queue.push(node->left);  // 左子节点入队
            if (node->right != nullptr)
                queue.push(node->right); // 右子节点入队
        }
        return vec;
    }
    

2.2 前序、中序、后序遍历

  • 前序、中序和后序遍历都属于深度优先遍历(depth-first traversal),体现 “先走到尽头,再回溯继续” 的思想
  • 下图展示了对二叉树进行深度优先遍历的工作原理。深度优先遍历就像是绕着整个二叉树的外围 “走” 一圈,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历

在这里插入图片描述

  • 深度优先搜索通常基于递归实现
    // 时间复杂度:所有节点被访问一次,使用 O(n) 时间
    // 空间复杂度:在最差情况下,即树退化为链表时,递归深度达到 n,系统占用 O(n) 栈帧空间
    /* 前序遍历 */
    void preOrder(TreeNode *root) {
        if (root == nullptr)
            return;
        // 访问优先级:根节点 -> 左子树 -> 右子树
        vec.push_back(root->val);
        preOrder(root->left);
        preOrder(root->right);
    }
    
    /* 中序遍历 */
    void inOrder(TreeNode *root) {
        if (root == nullptr)
            return;
        // 访问优先级:左子树 -> 根节点 -> 右子树
        inOrder(root->left);
        vec.push_back(root->val);
        inOrder(root->right);
    }
    
    /* 后序遍历 */
    void postOrder(TreeNode *root) {
        if (root == nullptr)
            return;
        // 访问优先级:左子树 -> 右子树 -> 根节点
        postOrder(root->left);
        postOrder(root->right);
        vec.push_back(root->val);
    }
    

前序遍历二叉树的递归过程可分为 “递” 和 “归” 两个逆向的部分

  • “递” 表示开启新方法,程序在此过程中访问下一个节点
  • “归” 表示函数返回,代表当前节点已经访问完毕

2.3 二叉树数组表示

2.3.1 表示完美二叉树
  • 给定一个完美二叉树,将所有节点按照层序遍历的顺序存储在一个数组中,则每个节点都对应唯一的数组索引。根据层序遍历的特性,可以推导出父节点索引与子节点索引之间的 “映射公式”
    • 若节点的索引为 i,则该节点的左子节点索引为 2i+1,右子节点索引为 2i+2

在这里插入图片描述

映射公式的角色相当于链表中的指针。给定数组中的任意一个节点,都可通过映射公式来访问它的左(右)子节点

2.3.2 表示任意二叉树
  • 完美二叉树是一个特例,在二叉树的中间层通常存在许多 None。由于层序遍历序列并不包含这些 None,因此无法仅凭该序列来推测 None 的数量和分布位置。这意味着存在多种二叉树结构都符合该层序遍历序列
    • 在层序遍历序列中显式地写出所有 None,下图所示,这样层序遍历序列就可以唯一表示二叉树
    // 使用 int 最大值 INT_MAX 标记空位
    vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
    

在这里插入图片描述

  • 完全二叉树非常适合使用数组来表示。完全二叉树的 None 只出现在最底层且靠右的位置,因此所有 None 一定出现在层序遍历序列的末尾。这意味着使用数组表示完全二叉树时,可以省略存储所有 None

在这里插入图片描述

  • 实现一个基于数组表示的二叉树,包括以下几种操作

    • 给定某节点,获取它的值、左(右)子节点、父节点
    • 获取前序遍历、中序遍历、后序遍历、层序遍历序列
    /* 数组表示下的二叉树类 */
    class ArrayBinaryTree {
    public:
        /* 构造方法 */
        ArrayBinaryTree(vector<int> arr) {
            tree = arr;
        }
    
        /* 节点数量 */
        int size() {
            return tree.size();
        }
    
        /* 获取索引为 i 节点的值 */
        int val(int i) {
            // 若索引越界,则返回 INT_MAX ,代表空位
            if (i < 0 || i >= size())
                return INT_MAX;
            return tree[i];
        }
    
        /* 获取索引为 i 节点的左子节点的索引 */
        int left(int i) {
            return 2 * i + 1;
        }
    
        /* 获取索引为 i 节点的右子节点的索引 */
        int right(int i) {
            return 2 * i + 2;
        }
    
        /* 获取索引为 i 节点的父节点的索引 */
        int parent(int i) {
            return (i - 1) / 2;
        }
    
        /* 层序遍历 */
        vector<int> levelOrder() {
            vector<int> res;
            // 直接遍历数组
            for (int i = 0; i < size(); i++) {
                if (val(i) != INT_MAX)
                    res.push_back(val(i));
            }
            return res;
        }
    
        /* 前序遍历 */
        vector<int> preOrder() {
            vector<int> res;
            dfs(0, "pre", res);
            return res;
        }
    
        /* 中序遍历 */
        vector<int> inOrder() {
            vector<int> res;
            dfs(0, "in", res);
            return res;
        }
    
        /* 后序遍历 */
        vector<int> postOrder() {
            vector<int> res;
            dfs(0, "post", res);
            return res;
        }
    
    private:
        vector<int> tree;
    
        /* 深度优先遍历 */
        void dfs(int i, string order, vector<int> &res) {
            // 若为空位,则返回
            if (val(i) == INT_MAX)
                return;
            // 前序遍历
            if (order == "pre")
                res.push_back(val(i));
            dfs(left(i), order, res);
            // 中序遍历
            if (order == "in")
                res.push_back(val(i));
            dfs(right(i), order, res);
            // 后序遍历
            if (order == "post")
                res.push_back(val(i));
        }
    };
    
2.3.3 优势与局限性
  • 二叉树的数组表示主要有以下优点

    • 数组存储在连续的内存空间中,对缓存友好,访问与遍历速度较快
    • 不需要存储指针,比较节省空间
    • 允许随机访问节点
  • 二叉树的数组表示主要有以下局限性

    • 数组存储需要连续内存空间,因此不适合存储数据量过大的树
    • 增删节点需要通过数组插入与删除操作实现,效率较低
    • 当二叉树中存在大量 None 时,数组中包含的节点数据比重较低,空间利用率较低

3. 二叉搜索树

  • 如下图所示,二叉搜索树(binary search tree)满足以下条件
    • 对于根节点,左子树中所有节点的值 < 根节点的值 < 右子树中所有节点的值
    • 任意节点的左、右子树也是二叉搜索树,即同样满足上述条件

在这里插入图片描述

3.1 二叉搜索树的操作

3.1.1 查找节点
  • 给定目标节点值 num,可以根据二叉搜索树的性质来查找。声明一个节点 cur,从二叉树的根节点 root 出发,循环比较节点值 cur.val 和 num 之间的大小关系

    • 若 cur.val < num ,说明目标节点在 cur 的右子树中,因此执行 cur = cur.right
    • 若 cur.val > num ,说明目标节点在 cur 的左子树中,因此执行 cur = cur.left
    • 若 cur.val = num ,说明找到目标节点,跳出循环并返回该节点
  • 二叉搜索树的查找操作与二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 O(log n) 时间

    /* 查找节点 */
    TreeNode *search(int num) {
        TreeNode *cur = root;
        // 循环查找,越过叶节点后跳出
        while (cur != nullptr) {
            // 目标节点在 cur 的右子树中
            if (cur->val < num)
                cur = cur->right;
            // 目标节点在 cur 的左子树中
            else if (cur->val > num)
                cur = cur->left;
            // 找到目标节点,跳出循环
            else
                break;
        }
        // 返回目标节点
        return cur;
    }
    
3.1.2 插入节点
  • 给定一个待插入元素 num,为保持二叉搜索树 “左子树 < 根节点 < 右子树” 性质,插入操作流程如下图所示
    • 查找节点插入位置
      • 与查找操作相似,从根节点出发,根据当前节点值和 num 的大小关系循环向下搜索,直到越过叶节点(遍历至 None)时跳出循环
    • 在该位置插入节点
      • 初始化节点 num,将该节点置于 None 的位置

在这里插入图片描述

  • 在代码实现中,需要注意以下两点
    • 二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插入,直接返回
    • 为实现插入节点,需要借助节点 pre 保存上一轮循环的节点。这样在遍历至 None 时,可以获取到其父节点,从而完成节点插入操作
    // 时间复杂度:O(log n)
    void insert(int num) {
        // 若树为空,则初始化根节点
        if (root == nullptr) {
            root = new TreeNode(num);
            return;
        }
        TreeNode *cur = root, *pre = nullptr;
        // 循环查找,越过叶节点后跳出
        while (cur != nullptr) {
            // 找到重复节点,直接返回
            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.1.3 删除节点
  • 先在二叉树中查找到目标节点,再将其从二叉树中删除。与插入节点类似,需要保证在删除操作完成后,二叉搜索树的 “左子树 < 根节点 < 右子树” 的性质仍然满足。需要根据目标节点的子节点数量,共分为 0、1 和 2 这三种情况,执行对应的删除节点操作
1. 当待删除节点的度为 0 时,表示该节点是叶节点,可以直接删除

在这里插入图片描述

2. 当待删除节点的度为 1 时,将待删除节点替换为其子节点即可

在这里插入图片描述

3. 当待删除节点的度为 2 时,无法直接删除它,而需要使用一个节点替换该节点
  • 由于要保持二叉搜索树 “左 < 根 < 右” 的性质,因此这个节点可以是右子树的最小节点或左子树的最大节点
  • 假设选择右子树的最小节点(即中序遍历的下一个节点),则删除操作流程如下
    • 查找 cur 在中序遍历的后继节点 nex
    • 在二叉树中递归删除节点 nex
    • 将节点 nex 值赋给节点 cur

在这里插入图片描述

// 时间复杂度:O(log n)
// 其中查找待删除节点需要 O(log n) 时间,获取中序遍历后继节点需要 O(log n) 时间
void remove(int num) {
    // 若树为空,直接提前返回
    if (root == nullptr)
        return;
    TreeNode *cur = root, *pre = nullptr;
    // 循环查找,越过叶节点后跳出
    while (cur != nullptr) {
        // 找到待删除节点,跳出循环
        if (cur->val == num)
            break;
        pre = cur;
        // 待删除节点在 cur 的右子树中
        if (cur->val < num)
            cur = cur->right;
        // 待删除节点在 cur 的左子树中
        else
            cur = cur->left;
    }
    // 若无待删除节点,则直接返回
    if (cur == nullptr)
        return;

    // 1、子节点数量 = 0 or 1
    if (cur->left == nullptr || cur->right == nullptr) {
        // 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点
        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;
        // 删除节点 cur
        if (cur != root) {
            if (pre->left == cur)
                pre->left = child;
            else
                pre->right = child;
        } else {
            // 若删除节点为根节点,则重新指定根节点
            root = child;
        }
        // 释放内存
        delete cur;
    }
    // 2、子节点数量 = 2
    else {
        // 获取中序遍历中 cur 的下一个节点
        TreeNode *tmp = cur->right;
        while (tmp->left != nullptr) {
            tmp = tmp->left;
        }
        int tmpVal = tmp->val;
        // 递归删除节点 tmp
        remove(tmp->val);
        // 用 tmp 覆盖 cur
        cur->val = tmpVal;
    }
}
3.1.4 中序遍历有序
  • 二叉树的中序遍历遵循 “左 < 根 < 右” 的遍历顺序,而二叉搜索树满足 “左子节点 < 根节点 < 右子节点” 的大小关系。这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:二叉搜索树的中序遍历序列是升序的
  • 利用中序遍历升序的性质,在二叉搜索树中获取有序数据仅需 O(n) 时间,无须进行额外的排序操作,非常高效

在这里插入图片描述

3.2 二叉搜索树的效率

  • 二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能表现。只有在高频添加、低频查找删除的数据适用场景下,数组比二叉搜索树的效率更高

在这里插入图片描述

  • 在理想情况下,二叉搜索树是 “平衡” 的,这样就可以在 log n 轮循环内查找任意节点。然而,如果在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为下图所示的链表,这时各种操作的时间复杂度也会退化为 O(n)

在这里插入图片描述

  • 在完美二叉树中插入两个节点后,树将严重向左倾斜,查找操作的时间复杂度也随之恶化

在这里插入图片描述

3.3 二叉搜索树应用

  • 用作系统中的多级索引,实现高效的查找、插入、删除操作
  • 作为某些搜索算法的底层数据结构
  • 用于存储数据流,以保持其有序状态

4. AVL 树

4.1 AVL 树常见术语

  • AVL 树既是二叉搜索树也是平衡二叉树,同时满足这两类二叉树的所有性质,因此也被称为平衡二叉搜索树(balanced binary search tree)
4.1.1 节点高度
  • 由于 AVL 树的相关操作需要获取节点高度,因此需要为节点类添加 height 变量
    /* AVL 树节点类 */
    struct TreeNode {
        int val{};          // 节点值
        int height = 0;     // 节点高度
        TreeNode *left{};   // 左子节点
        TreeNode *right{};  // 右子节点
        TreeNode() = default;
        explicit TreeNode(int x) : val(x){}
    };
    
  • “节点高度” 是指从该节点到最远叶节点的距离,即所经过的 “边” 的数量
    • 需要特别注意的是,叶节点的高度为 0,而空节点的高度为 -1
    /* 获取节点高度 */
    int height(TreeNode *node) {
        // 空节点高度为 -1 ,叶节点高度为 0
        return node == nullptr ? -1 : node->height;
    }
    
    /* 更新节点高度 */
    void updateHeight(TreeNode *node) {
        // 节点高度等于最高子树高度 + 1
        node->height = max(height(node->left), height(node->right)) + 1;
    }
    
4.1.2 节点平衡因子
  • 节点的平衡因子(balance factor)定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0
    /* 获取平衡因子 */
    int balanceFactor(TreeNode *node) {
        // 空节点平衡因子为 0
        if (node == nullptr)
            return 0;
        // 节点平衡因子 = 左子树高度 - 右子树高度
        return height(node->left) - height(node->right);
    }
    

设平衡因子为 f,则一棵 AVL 树的任意节点的平衡因子皆满足 -1 < f < 1

4.2 AVL 树旋转

  • AVL 树的特点在于 “旋转” 操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡
    • 换句话说,旋转操作既能保持 “二叉搜索树” 的性质,也能使树重新变为 “平衡二叉树”
  • 将平衡因子绝对值 > 1 的节点称为 “失衡节点”
    • 根据节点失衡情况的不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋
4.2.1 右旋
  • 如下图所示(节点下方为平衡因子)
    • 从底至顶看,二叉树中首个失衡节点是 “节点 3”
    • 关注以该失衡节点为根节点的子树,将该节点记为 node,其左子节点记为 child
    • 执行 “右旋” 操作:以 child 为原点,将 node 向右旋转
    • 右旋完成后,用 child 替代以前 node 的位置,子树已恢复平衡,并仍保持二叉搜索树的特性

在这里插入图片描述

  • 如下图所示,当节点 child 有右子节点(记为 grandChild)时,需要在右旋中添加一步:将 grandChild 作为 node 的左子节点

在这里插入图片描述

/* 右旋操作 */
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;
}
4.2.2 左旋

在这里插入图片描述

在这里插入图片描述

/* 左旋操作 */
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;
}
4.2.3 先左旋后右旋
  • 对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡
    • 此时需要先对 child 执行 “左旋”,再对 node 执行 “右旋”

在这里插入图片描述

4.2.4 先右旋后左旋

在这里插入图片描述

4.2.5 旋转的选择

在这里插入图片描述

  • 如下表,通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于上图哪种情况

在这里插入图片描述

  • 为便于使用,将旋转操作封装成一个函数。通过这个函数就能对各种失衡情况进行旋转,使失衡节点重新恢复平衡
    /* 执行旋转操作,使该子树重新恢复平衡 */
    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.3 AVL 树常用操作

4.3.1 插入节点
  • AVL 树的节点插入操作与二叉搜索树在主体上类似。唯一的区别在于,在 AVL 树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点
    • 因此,需要从这个节点开始,自底向上执行旋转操作,使所有失衡节点恢复平衡
    /* 插入节点 */
    void insert(int val) {
        root = insertHelper(root, val);
    }
    
    /* 递归插入节点(辅助方法) */
    TreeNode *insertHelper(TreeNode *node, int val) {
        if (node == nullptr)
            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;
    }
    
4.3.2 删除节点
  • 类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡
    /* 删除节点 */
    void remove(int val) {
        root = removeHelper(root, val);
    }
    
    /* 递归删除节点(辅助方法) */
    TreeNode *removeHelper(TreeNode *node, int val) {
        if (node == nullptr)
            return nullptr;
        /* 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 == nullptr || node->right == nullptr) {
                TreeNode *child = node->left != nullptr ? node->left : node->right;
                // 子节点数量 = 0 ,直接删除 node 并返回
                if (child == nullptr) {
                    delete node;
                    return nullptr;
                }
                // 子节点数量 = 1 ,直接删除 node
                else {
                    delete node;
                    node = child;
                }
            } else {
                // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点
                TreeNode *temp = node->right;
                while (temp->left != nullptr) {
                    temp = temp->left;
                }
                int tempVal = temp->val;
                node->right = removeHelper(node->right, temp->val);
                node->val = tempVal;
            }
        }
        updateHeight(node); // 更新节点高度
        /* 2. 执行旋转操作,使该子树重新恢复平衡 */
        node = rotate(node);
        // 返回子树的根节点
        return node;
    }
    
4.3.3 查找节点
  • AVL 树的节点查找操作与二叉搜索树一致

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1070809.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Qt】顶层窗口和普通窗口区别以及用法

区别 在Qt项目开发中&#xff0c;经常会用到窗体控件用于显示及数据操作和其他交互等。 但&#xff0c;窗体分为顶层窗口&#xff08;Top-level Window&#xff09;和普通窗口&#xff08;Regular Window&#xff09;。 他们之间是有区别的&#xff0c;包括在项目实际中的用法…

【Vue面试题十一】、Vue组件之间的通信方式都有哪些?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;Vue组件之间的通信方式都…

学习网络编程No.7【应用层之序列化和反序列化】

引言&#xff1a; 北京时间&#xff1a;2023/9/14/19:13&#xff0c;下午刚刚更完文章&#xff0c;是一篇很久很久以前的文章&#xff0c;由于各种原因&#xff0c;留到了今天更新&#xff0c;非常惭愧呀&#xff01;目前在上学校开的一门网络课程&#xff0c;学校的课听不了一…

leetCode 1143.最长公共子序列 动态规划

1143. 最长公共子序列 - 力扣&#xff08;LeetCode&#xff09; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0 。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串…

Linux登录自动执行脚本

一、所有用户每次登录时自动执行。 1、在/etc/profile文件末尾添加。 将启动命令添加到/etc/profile文件末尾。 2、在/etc/profile.d/目录下添加sh脚本。 在/etc/profile.d/目录下新建sh脚本&#xff0c;设置每次登录自动执行脚本。有用户登录时&#xff0c;/etc/profile会遍…

一文带你读懂残差网络ResNet

&#x1f680; 作者 &#xff1a;“码上有钱” &#x1f680; 文章简介 &#xff1a;AI-残差算法 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac;简介 残差网络&#xff08;Residual Neural Network, ResNet&#xff09;是深度神经网络的一种。它通…

2.1 关系数据结构及形式化定义

思维导图&#xff1a; 2.1.1 关系 笔记&#xff1a; 关系数据库模型是一个简单但强大的方式来表示数据及其之间的关系。下面是这节的关键内容&#xff1a; - **关系模型核心概念** * 关系数据模型的核心是“关系”&#xff0c;它在逻辑上表现为一个二维表。 * 此表中&a…

Cesium问题——在使用贴图的方式加载图片时并未加载出来

文章目录 问题分析问题 Cesium在使用贴图的方式加载图片失败 分析 如果在Cesium中加载图片时,控制台显示成功(200状态码),但是预览显示却失败了,可能有以下几个原因: 图片格式不受支持:Cesium中通常支持常见的图片格式,如JPEG、PNG等。确保你使用的图片格式在Cesium中…

Spring Boot中实现发送文本、带附件和HTML邮件

SpringBoot实现发送邮箱 引言 在现代应用程序中&#xff0c;电子邮件通常是不可或缺的一部分。在Spring Boot中&#xff0c;你可以轻松地实现发送不同类型的邮件&#xff0c;包括文本、带附件和HTML邮件。本博客将向你展示如何使用Spring Boot发送这些不同类型的电子邮件。 步…

《从零开始学ARM》勘误

1. 50页 2 51页 3 236页 14.2.3 mkU-Boot 修改为&#xff1a; mkuboot 4 56页 修改为&#xff1a; 位[31&#xff1a;24]为条件标志位域&#xff0c;用f表示&#xff1b; 位[23&#xff1a;16]为状态位域&#xff0c;用s表示&#xff1b; 位[15&#xff1a;8]为扩展位域&…

前端页面布局之【Grid布局】详解

目录 &#x1f31f;前言&#x1f31f;浏览器支持&#x1f31f;Gird简介和基本概念&#x1f31f;属性介绍&#x1f31f; 父元素上的属性&#x1f31f; 设置grid布局&#x1f31f;设置网格的列数与行数&#x1f31f;通过网格单元的名字来布局 grid-template-areas&#x1f31f;设…

资深8年测试总结,web网页测试bug定位详细步骤,一文打通...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、前置条件 1&a…

助力农作物病虫害检测识别,基于yolov3—yolov8开发构建马铃薯作物甲虫检测识别系统

AI加持的智慧农业也是一个比较有前景的赛道&#xff0c;近些年来已经有很多不错的方向做出来成绩&#xff0c;基于AI的激光除草、灭虫等也是其中的一个热门&#xff0c;杂草相关的检测识别在我们之前的项目实例中已经有相关的实践了&#xff0c;这里本文的主要目的就是以农作物…

最短路径专题8 交通枢纽 (Floyd求最短路 )

题目&#xff1a; 样例&#xff1a; 输入 4 5 2 0 1 1 0 2 5 0 3 3 1 2 2 2 3 4 0 2 输出 0 7 思路&#xff1a; 由题意&#xff0c;绘制了该城市的地图之后&#xff0c;由给出的 k 个编号作为起点&#xff0c;求该点到各个点之间的最短距离之和最小的点是哪个&#xff0c;并…

分布式数据库(林子雨慕课课程)

文章目录 4. 分布式数据库HBase4.1 HBase简介4.2 HBase数据模型4.3 HBase的实现原理4.4 HBase运行机制4.5 HBase的应用方案4.6 HBase安装和编程实战 4. 分布式数据库HBase 4.1 HBase简介 HBase是BigTable的开源实现 对于网页搜索主要分为两个阶段 1.建立整个网页索引&#xf…

第八章 排序 十四、最佳归并树

目录 一、定义 二、多路最佳归并树 三、多路最佳归并树少了一个归并段 四、总结 一、定义 最佳归并树是指将若干个有序序列合并成一个有序序列的一种方式&#xff0c;使得所有合并操作的总代价最小的一棵二叉树。其中&#xff0c;代价通常指合并两个有序序列的操作次数或比…

挑选出优秀的项目管理软件,满足您的需求

Zoho Projects是很好的一个项目管理软件&#xff0c;不管是web端还是APP没有那些乱七八糟的广告&#xff0c;光是这一点&#xff0c;就让人用着很舒服。除此之外还有更多让人意想不到的惊喜&#xff0c;软件界面设置的井井有条&#xff0c;关键是软件有完全免费版的&#xff0c…

mp4视频太大怎么压缩变小?

mp4视频太大怎么压缩变小&#xff1f;确实&#xff0c;很多培训和教学都转向了线上模式&#xff0c;这使得我们需要下载或分享大量的在线教学视频。然而&#xff0c;由于MP4视频文件通常较大&#xff0c;可能会遇到无法打开或发送的问题。为了解决这个问题&#xff0c;我们可以…

WMS仓储管理系统在快消品生产企业中有哪些应用

随着企业规模的扩大和市场竞争的加剧&#xff0c;仓库管理在企业管理中的地位越来越重要。对于快消品生产企业来说&#xff0c;仓库管理更是关系到产品的质量和市场竞争力。为了提高仓库管理的效率和准确性&#xff0c;许多企业开始引入WMS仓储管理系统解决方案。 中小企业WMS系…

网络安全(黑客)——自学

前言&#xff1a; 想自学网络安全&#xff08;黑客技术&#xff09;首先你得了解什么是网络安全&#xff01;什么是黑客 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“…