数据结构-二叉搜索树(BST)

目录

什么是二叉搜索树

二叉搜索树的特性 

(1)顺序性

(2)局限性

二叉搜索树的应用 

二叉搜索树的操作

(1)查找节点

(2)插入节点

(3)删除节点

(4)中序遍历 


什么是二叉搜索树

        如图所示,二叉搜索树(binary search tree)满足以下条件。

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

二叉搜索树

        二分搜索树有着高效的插入、删除、查询操作。

        平均时间的时间复杂度为 O(log n),最差情况为 O(n)。二分搜索树与堆不同,不一定是完全二叉树,底层不容易直接用数组表示故采用链表来实现二分搜索树。只有在高频添加、低频查找删除数据的场景下,数组比二叉搜索树的效率更高。

查找元素插入元素删除元素
普通数组O(n)O(n)O(n)
顺序数组O(logn)O(n)O(n)
二分搜索树O(logn)O(logn)O(logn)

二叉搜索树的特性 

(1)顺序性

        二分搜索树可以当做查找表的一种实现。

        我们使用二分搜索树的目的是通过查找 key 马上得到 value。minimum、maximum、successor(后继)、predecessor(前驱)、floor(地板)、ceil(天花板、rank(排名第几的元素)、select(排名第n的元素是谁)这些都是二分搜索树顺序性的表现。

(2)局限性

        二分搜索树在时间性能上是具有局限性的。

        在理想情况下,二叉搜索树是“平衡”的,这样就可以在 log⁡𝑛 轮循环内查找任意节点。

        然而,如果我们在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为如图所示的链表,相应的,二叉搜索树的查找操作是和这棵树的高度相关的,而此时这颗树的高度就是这颗树的节点数 n,这时各种操作的时间复杂度也会退化为 𝑂(𝑛) 。

二叉搜索树退化


二叉搜索树的应用 

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

二叉搜索树的操作

(1)查找节点

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

  • 若 cur.val < num ,说明目标节点在 cur 的右子树中,因此执行 cur = cur.right 。
  • 若 cur.val > num ,说明目标节点在 cur 的左子树中,因此执行 cur = cur.left 。
  • 若 cur.val = num ,说明找到目标节点,跳出循环并返回该节点。

bst_search_step4

        二叉搜索树的查找操作与二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 𝑂(log⁡𝑛) 时间。示例代码如下:

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

        通过查找节点的方法,我们可以完成98. 验证二叉搜索树 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
bool isValidBSTHelper(struct TreeNode* root, long min_val, long max_val) {
    // 如果根节点为空,直接返回 true,因为空树也是 BST
    if (root == NULL) {
        return true;
    }

    // 检查当前节点值是否在范围内
    if (root->val <= min_val || root->val >= max_val) {
        return false;
    }

    // 递归检查左右子树,更新范围
    return isValidBSTHelper(root->left, min_val, root->val) && 
           isValidBSTHelper(root->right, root->val, max_val);
}

bool isValidBST(struct TreeNode* root) {
    // 使用 long 类型的最小值和最大值作为初始范围
    return isValidBSTHelper(root, LONG_MIN, LONG_MAX);
}

(2)插入节点

        给定一个待插入元素 num ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作流程如图所示。

  1. 查找插入位置:与查找操作相似,从根节点出发,根据当前节点值和 num 的大小关系循环向下搜索,直到越过叶节点(遍历至 None )时跳出循环。
  2. 在该位置插入节点:初始化节点 num ,将该节点置于 None 的位置。

在二叉搜索树中插入节点

        在代码实现中,需要注意以下两点。

  • 二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插入,直接返回。
  • 为了实现插入节点,我们需要借助节点 pre 保存上一轮循环的节点。这样在遍历至 None 时,我们可以获取到其父节点,从而完成节点插入操作。

        代码范例如下,与查找节点相同,插入节点使用 𝑂(log⁡𝑛) 时间。

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

        通过插入节点的方法,我们可以完成701. 二叉搜索树中的插入操作 - 力扣(LeetCode)

struct TreeNode* createTreeNode(int val) {
    struct TreeNode* ret = malloc(sizeof(struct TreeNode));
    // 设置节点值
    ret->val = val;
    // 左右子节点为空
    ret->left = ret->right = NULL;
    // 返回新创建的节点
    return ret;
}

struct TreeNode* insertIntoBST(struct TreeNode* root, int val) {
    // 如果根节点为空,直接将新节点作为根节点返回
    if (root == NULL) {
        root = createTreeNode(val);
        return root;
    }
    // 定义游标节点为根节点
    struct TreeNode* pos = root;
    // 循环查找插入位置
    while (pos != NULL) {
        // 如果 val 小于当前节点值
        if (val < pos->val) {
            // 如果当前节点的左子节点为空,将新节点插入左子节点位置
            if (pos->left == NULL) {
                pos->left = createTreeNode(val);
                break;
            } else {
                // 否则继续向左子树查找插入位置
                pos = pos->left;
            }
        } else {
            // 如果 val 大于等于当前节点值
            // 如果当前节点的右子节点为空,将新节点插入右子节点位置
            if (pos->right == NULL) {
                pos->right = createTreeNode(val);
                break;
            } else {
                // 否则继续向右子树查找插入位置
                pos = pos->right;
            }
        }
    }
    // 返回根节点
    return root;
}

(3)删除节点

        先在二叉树中查找到目标节点,再将其删除。与插入节点类似,我们需要保证在删除操作完成后,二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足。因此,我们根据目标节点的子节点数量,分 0、1 和 2 三种情况,执行对应的删除节点操作。

        如图所示,当待删除节点的度为 0 时,表示该节点是叶节点,可以直接删除。

在二叉搜索树中删除节点(度为 0 )

        如下图所示,当待删除节点的度为 1 时,将待删除节点替换为其子节点即可。

在二叉搜索树中删除节点(度为 1 )

        当待删除节点的度为 2 时,我们无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,因此这个节点可以是右子树的最小节点或左子树的最大节点

假设我们选择右子树的最小节点(中序遍历的下一个节点),则删除操作流程如下图所示。

  1. 找到待删除节点在“中序遍历序列”中的下一个节点,记为 tmp 。
  2. 用 tmp 的值覆盖待删除节点的值,并在树中递归删除节点 tmp 。

bst_remove_case3_step4

        删除节点操作同样使用 𝑂(log⁡𝑛) 时间,其中查找待删除节点需要 𝑂(log⁡𝑛) 时间,获取中序遍历后继节点需要 𝑂(log⁡𝑛) 时间。示例代码如下:

/* 删除节点 */
// 由于引入了 stdio.h ,此处无法使用 remove 关键词
void removeItem(BinarySearchTree *bst, int num) {
    // 若树为空,直接提前返回
    if (bst->root == NULL)
        return;
    TreeNode *cur = bst->root, *pre = NULL;
    // 循环查找,越过叶节点后跳出
    while (cur != NULL) {
        // 找到待删除节点,跳出循环
        if (cur->val == num)
            break;
        pre = cur;
        if (cur->val < num) {
            // 待删除节点在 root 的右子树中
            cur = cur->right;
        } else {
            // 待删除节点在 root 的左子树中
            cur = cur->left;
        }
    }
    // 若无待删除节点,则直接返回
    if (cur == NULL)
        return;
    // 判断待删除节点是否存在子节点
    if (cur->left == NULL || cur->right == NULL) {
        /* 子节点数量 = 0 or 1 */
        // 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点
        TreeNode *child = cur->left != NULL ? cur->left : cur->right;
        // 删除节点 cur
        if (pre->left == cur) {
            pre->left = child;
        } else {
            pre->right = child;
        }
        // 释放内存
        free(cur);
    } else {
        /* 子节点数量 = 2 */
        // 获取中序遍历中 cur 的下一个节点
        TreeNode *tmp = cur->right;
        while (tmp->left != NULL) {
            tmp = tmp->left;
        }
        int tmpVal = tmp->val;
        // 递归删除节点 tmp
        removeItem(bst, tmp->val);
        // 用 tmp 覆盖 cur
        cur->val = tmpVal;
    }
}

        除了迭代方法外,我们还可以使用递归方法来删除节点,下面的力扣题给出的方法就是递归方法。450. 删除二叉搜索树中的节点 - 力扣(LeetCode)

// 从二叉搜索树 root 中删除值为 key 的节点
struct TreeNode* deleteNode(struct TreeNode* root, int key) {
    // 如果根节点为空,直接返回 NULL
    if (root == NULL) {
        return NULL;
    }
    // 如果 key 小于当前节点值,递归删除左子树中的节点
    if (root->val > key) {
        root->left = deleteNode(root->left, key);
        return root;
    }
    // 如果 key 大于当前节点值,递归删除右子树中的节点
    if (root->val < key) {
        root->right = deleteNode(root->right, key);
        return root;
    }
    // 如果当前节点值等于 key
    if (root->val == key) {
        // 如果当前节点没有左右子节点,直接返回 NULL
        if (!root->left && !root->right) {
            return NULL;
        }
        // 如果只有右子节点,返回右子节点
        if (!root->right) {
            return root->left;
        }
        // 如果只有左子节点,返回左子节点
        if (!root->left) {
            return root->right;
        }
        // 如果既有左子节点又有右子节点
        // 找到右子树中最小的节点作为当前节点的替代节点
        struct TreeNode *successor = root->right;
        while (successor->left) {
            successor = successor->left;
        }
        // 递归删除右子树中的最小节点
        root->right = deleteNode(root->right, successor->val);
        // 将替代节点的右子树连接到当前节点的右子树
        successor->right = root->right;
        // 将替代节点的左子树连接到当前节点的左子树
        successor->left = root->left;
        // 返回替代节点作为当前节点的父节点的子节点
        return successor;
    }
    // 返回根节点
    return root;
}

(4)中序遍历 

        如图所示,二叉树的中序遍历遵循“左 → 根 → 右”的遍历顺序,而二叉搜索树满足“左子节点 < 根节点 < 右子节点”的大小关系。

        这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:二叉搜索树的中序遍历序列是升序的。

        利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 𝑂(𝑛) 时间,无须进行额外的排序操作,非常高效。

二叉搜索树的中序遍历序列

        利用二叉搜索树中序遍历升序,我们可以完成 530. 二叉搜索树的最小绝对差

void traversal(struct TreeNode* cur, struct TreeNode** pre, int *result) {
    if (cur == NULL) return;
    //BST中序遍历是升序
    traversal(cur->left, pre, result);   // 左
    if (*pre != NULL){       // 中
        *result = fmin(*result, cur->val - (*pre)->val);
    }
    *pre = cur; // 记录前一个
    traversal(cur->right, pre, result);  // 右
}


int getMinimumDifference(struct TreeNode* root) {
    int result = 114514;
    struct TreeNode* pre = NULL; // 初始值为NULL
    traversal(root, &pre, &result); // 传递pre的指针的指针
    return result;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Vivado那些事儿】使用 Python 提取 ILA 数据

ILA应该是调试AMD-Xilinx FPGA最常用的IP。 在调试中&#xff0c;我们希望 ILA 中的波形可以提供有关设计问题的所有信息&#xff0c;但情况并非如此。对于复杂的调试&#xff0c;我们还需要将 ILA 捕获的真实数据存储到可以进一步处理的文件中。根据放置 ILA 的位置&#xff0…

C语言阶段的题目解析

前言 我们C语言已经学习的差不多了&#xff0c;但是C语言之中存在的一些问题与难点我们还不一定能够又快又好地解决&#xff0c;为了夯实我们的基础&#xff0c;我们来练习几道稍微有点难度的C语言习题吧 例题一 题目 int main(void) {unsigned char i 7;int j 0;for (; i…

【PyTorch 实战3:YOLOv5检测模型】10min揭秘 YOLOv5 检测网络架构、工作原理以及pytorch代码实现(附代码实现!)

YOLOv5简介 YOLOv5&#xff08;You Only Look Once, Version 5&#xff09;是一种先进的目标检测模型&#xff0c;是YOLO系列的最新版本&#xff0c;由Ultralytics公司开发。该模型利用深度学习技术&#xff0c;能够在图像或视频中实时准确地检测出多个对象的位置及其类别&…

千锤百炼之每日算法(三)

题外话 不会再水了,先把算法任务完成! 正题 第一题 简写单词 规定一种对于复合词的简写方式为只保留每个组成单词的首字母,并将首字母大写后再连接在一起 比如“College English Test"可以简写成“CET",“Computer Science 可以简写为"CS"&#xff0c;I a…

【深度学习】【Lora训练1】StabelDiffusion,Lora训练过程,秋叶包,Linux,SDXL Lora训练

文章目录 一、环境搭建指南二、个性化安装流程三、启动应用四、打开web五、开始训练 19.27服务器 一、环境搭建指南 打造一个高效且友好的开发环境&#xff1a; 项目源码获取&#xff1a; 通过以下命令轻松克隆项目及所有子模块至您的Linux系统&#xff1a; git clone --recu…

《ElementUI 基础知识》el-tabs header 监听鼠标中键滚动时左右滑动(ElementPlus同样适用)

前言 收到需求&#xff0c;可监听 el-tabs 头在鼠标 hover 时。滑动鼠标中键&#xff0c;可左右滑动&#xff01; 效果 鼠标中键上下滑动时&#xff1b;向上滑&#xff0c;向左移动&#xff1b;向下滑&#xff0c;向右移动&#xff1b; 实现 代码56 - 60行&#xff0c;添加…

寝室快修|基于SprinBoot+vue的贵工程寝室快修小程序(源码+数据库+文档)

贵工程寝室快修目录 目录 基于SprinBootvue的贵工程寝室快修小程序 一、前言 二、系统设计 三、系统功能设计 1学生信息管理 2 在线报修管理 3公告信息管理 4论坛信息管理 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&a…

操作系统安全:安全审计,Windows系统日志详解,Windows事件ID汇总

「作者简介」&#xff1a;2022年北京冬奥会网络安全中国代表队&#xff0c;CSDN Top100&#xff0c;就职奇安信多年&#xff0c;以实战工作为基础对安全知识体系进行总结与归纳&#xff0c;著作适用于快速入门的 《网络安全自学教程》&#xff0c;内容涵盖系统安全、信息收集等…

C++深度解析教程笔记2

C深度解析教程笔记2 第3课 - 进化后的 const 分析实验-C与C的const区别实验-C与C的const区别&const作用域 第4课 - 布尔类型和引用小结 本文学习自狄泰软件学院 唐佐林老师的 C深度解析教程&#xff0c;图片全部来源于课程PPT&#xff0c;仅用于个人学习记录 第3课 - 进化后…

Unity 递归实现数字不重复的排列组合

实现 private void Permutation(List<int> num, int leftIndex, List<string> strs) {if (leftIndex < num.Count){for (int rightIndex leftIndex; rightIndex < num.Count; rightIndex){Swap(num, leftIndex, rightIndex);Permutation(num, leftIndex 1…

HarmonyOS 鸿蒙下载三方依赖 ohpm环境搭建

前言 ohpm&#xff08;One Hundred Percent Mermaid &#xff09;是一个集成了Mermaid的命令工具&#xff0c;可以用于生成关系图、序列图、等各种图表。我们可以使用ohpm来生成漂亮且可读性强的图表。 本期教大家如何搭建ophm环境&#xff1a; 一、在DevEco Studio中&#…

前端可以掌握的nginx相关操作

一、前言&#xff1a; 在日常开发中&#xff0c;前端工程师可以把打好的前端包直接放到测试服务器上&#xff0c;自己再验证功能是否改好&#xff0c;这样可以提高开发效率&#xff0c;写篇笔记记录一下我个人用到的命令 二、使用的工具 用MobaXterm完成相关操作&#xff0c…

Vue3 + TS 项目实战 - 后台管理系统 - 按钮权限

前期回顾 网站的打赏 —— 新一代的思路-CSDN博客https://blog.csdn.net/m0_57904695/article/details/136704914?spm1001.2014.3001.5501 目录 &#x1f6a9; XX银行_系统管理_按钮权限控制_前端_提测单 项目信息 提测版本信息 功能列表 测试范围 测试环境 ✅ 步…

[paper note]代码生成评估模型-CodeBLEU原理分析

论文信息 论文标题&#xff1a;CodeBLEU: a Method for Automatic Evaluation of Code Synthesis 发表时间&#xff1a;2020年9月 论文原文&#xff1a;CodeBLEU: a Method for Automatic Evaluation of Code Synthesis 论文内容 摘要 评价指标对一个领域的发展起着至关重…

大厂常见算法50题-替换空格

专栏持续更新50道算法题&#xff0c;都是大厂高频算法题&#xff0c;建议关注, 一起巧‘背’算法! 文章目录 题目解法一 String类replace方法解法二 遍历替换总结 题目 解法一 String类replace方法 String类自带的replace&#xff0c;方法传入两个char类型的参数&#xff0c;分…

分类预测 | Matlab实现CNN-GRU-SAM-Attention卷积门控循环单元融合空间注意力机制的数据分类预测

分类预测 | Matlab实现CNN-GRU-SAM-Attention卷积门控循环单元融合空间注意力机制的数据分类预测 目录 分类预测 | Matlab实现CNN-GRU-SAM-Attention卷积门控循环单元融合空间注意力机制的数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现CNN-GRU…

蓝牙低能耗安全连接 – 数值比较

除了 LE Legacy 配对之外&#xff0c;LE Secure Connections 是另一种配对选项。 LE 安全连接是蓝牙 v4.2 中引入的增强安全功能。它使用符合联邦信息处理标准 (FIPS) 的算法&#xff08;称为椭圆曲线 Diffie Hellman (ECDH)&#xff09;来生成密钥。对于 LE 安全连接&#xff…

【Stream流基础篇】Java中的函数、函数对象、函数接口和方法引用及转换

什么是函数 在数学中&#xff0c;函数是这样定义的&#xff1a;它是给定一个数集A&#xff0c;假设其中的元素为x&#xff0c;对A中的元素x施加对应法则f&#xff0c;记作f&#xff08;x&#xff09;&#xff0c;得到另一数集B&#xff0c;假设B中的元素为y&#xff0c;则y与x…

pytorch中的过拟合和欠拟合

基本概念 我们知道&#xff0c;所谓的神经网络其实就是一个复杂的非线性函数&#xff0c;网络越深&#xff0c;这个函数就越复杂&#xff0c;相应的表达能力也就越强&#xff0c;神经网络的训练则是一个拟合的过程。   当模型的复杂度小于真实数据的复杂度&#xff0c;模型表…

GMSSL编译iOS

一、GMSSL-2.x 国密SDK源码下载&#xff0c;对GMSSL库进行编译生成对应的静态库。执行如下命令&#xff1a; cd到SDK源码目录 cd /Users/xxxx/Downloads/GMSSLV2-master查看SDK适用环境 ./config上图中错误解决方法 使用文本编辑器打开SDK目录下Configure、test/build.info、…