详解二叉搜索树 --- key模型和key/value模型

news2025/2/27 5:43:11

🍀作者阿润菜菜
📖专栏数据结构


一、认识二叉搜索树的key模型和key/value模型

  • key的模型是指每个节点只有一个键值,用于确定节点在树中的位置。节点的键值必须满足二叉搜索树的性质,即左子节点的键值小于父节点的键值,右子节点的键值大于父节点的键值。这种模型比较简单,但是不能存储额外的信息。
  • key/value模型是指每个节点有一个键值和一个数据值,键值用于确定节点在树中的位置,数据值用于存储节点的附加信息。节点的键值仍然必须满足二叉搜索树的性质,但是数据值可以是任意类型或对象。这种模型比较灵活,可以实现一些高级功能,比如映射或字典。

例如,如果我们要用二叉搜索树存储一些人的姓名和年龄,我们可以用key/value模型,把姓名作为键值,把年龄作为数据值。这样我们就可以根据姓名快速查找或插入一个人的信息。

在key/value模型中,我们需要定义一个内部类来表示节点,每个节点包含一个键值、一个数据值、一个左子链接和一个右子链接。我们还可以定义一个节点计数变量来记录以该节点为根的子树中的节点个数,这样可以方便地实现一些有序符号表的操作。

在key的模型中,我们只需要定义一个内部类来表示节点,每个节点包含一个键值、一个左子链接和一个右子链接。我们不需要定义一个数据值或一个节点计数变量。

本文以key模型进行讲解🌟 🌟 🌟

二、K结构的二叉搜索树实现

在二叉搜索树中,我们可以实现以下基本操作

  • 搜索:给定一个键值,在二叉搜索树中查找是否存在对应的节点。我们可以使用递归或迭代的方法来实现搜索。搜索算法基于二叉搜索树的性质:如果要查找的键值等于当前节点的键值,就返回当前节点;如果要查找的键值小于当前节点的键值,就在左子树中继续查找;如果要查找的键值大于当前节点的键值,就在右子树中继续查找;如果遇到空链接,就说明查找失败。
  • 插入:给定一个键值和一个数据值,在二叉搜索树中插入一个新的节点。我们也可以使用递归或迭代的方法来实现插入。插入算法基于二叉搜索树的性质:如果要插入的键值等于当前节点的键值,就更新当前节点的数据值;如果要插入的键值小于当前节点的键值,就在左子树中继续插入,并将返回的新的左子树赋给当前节点的左子链接;如果要插入的键值大于当前节点的键值,就在右子树中继续插入,并将返回的新的右子树赋给当前节点的右子链接;如果遇到空链接,就创建一个新的节点并返回它。
  • 删除:给定一个键值,在二叉搜索树中删除对应的节点。删除算法比较复杂,因为我们需要考虑三种情况:
    • 如果要删除的节点是叶子节点,就直接删除它,并返回空。
    • 如果要删除的节点只有一个子节点,就用该子节点替换它,并删除原来的节点。
    • 如果要删除的节点有两个子节点,就找到它的中序后继(即右子树中最小的节点或者左子树最大),并交换它们的键值,然后递归(或者非递归法)地在左子树中删除该键值。

我们可以使用递归或迭代的方法来实现删除。递归方法基于二叉搜索树的性质:如果要删除的键值等于当前节点的键值,就按照上述三种情况处理;如果要删除的键值小于当前节点的键值,就在左子树中继续删除,并将返回的新的左子树赋给当前节点的左子链接;如果要删除的键值大于当前节点的键值,就在右子树中继续删除,并将返回的新的右子树赋给当前节点的右子链接;如果遇到空链接,就说明查找失败。

迭代方法基于循环和栈来实现。首先,我们使用一个循环来查找要删除的节点,并记录它的父节点和左右方向。然后,我们使用一个栈来存储从根节点到要删除的节点的路径。接着,我们按照上述三种情况处理要删除的节点,并更新它的父节点和左右方向。最后,我们使用一个循环来更新从根节点到要删除的节点的路径上所有节点的计数变量。

Erase() 函数

  • 首先,检查根节点是否为空,如果为空,直接返回空。
  • 然后,比较要删除的键值和根节点的键值,如果小于根节点的键值,就递归地在左子树中删除该键值,并将返回的新的左子树赋给根节点的左子指针;如果大于根节点的键值,就递归地在右子树中删除该键值,并将返回的新的右子树赋给根节点的右子指针。
  • 最后,如果要删除的键值和根节点的键值相等,就分三种情况处理
    • 如果根节点是叶子节点,就直接删除它,并返回空。
    • 如果根节点只有一个子节点,就用该子节点替换根节点,并删除原来的根节点。
    • 如果要删除的节点有两个子节点,就找到它的中序后继(即右子树中最小的节点或者左子树最大),并交换它们的键值,然后递归(或者非递归法)地在左子树中删除该键值。

这样,就可以保证删除一个节点后,二叉搜索树的性质仍然成立。

代码示例: 已加上注释

bool _EraseR(Node*& root, const K& key) {
  if (root == nullptr) return false;
  if (root->_key < key) {
    return _EraseR(root->_left, key);
  } else if (root->_key > key) {
    return _EraseR(root->_right, key);
  } else {
    Node* del = root;  //创建一个指针del指向要删除的节点
    //开始准备删除
    if (root->_right == nullptr) {  //如果要删除的节点没有右子节点
      root = root->_left;           //就用它的左子节点替换它
    } else if (root->_left == nullptr) {  //如果要删除的节点没有左子节点
      root = root->_right;                //就用它的右子节点替换它
    } else {                        //如果要删除的节点有两个子节点
      Node* maxleft = root->_left;  //创建一个指针maxleft指向它的左子树
      while (maxleft->_right) {  //找到左子树中最右边的节点,即中序后继
        maxleft = maxleft->_right;
      }
      swap(root->_key, maxleft->_key);  //交换要删除的节点和中序后继的键值
      return _EraseR(root->_left, key);  //递归地在左子树中删除该键值
    }

    delete del;  //释放要删除的节点的内存空间

    return true;
  }
}

同时还有非递归方法: 直接看代码和注释

 bool Erase(const K& key) {
  Node* parent = nullptr;
  Node* cur = _root;

  while (cur) {
    if (cur->_key < key) {
      parent = cur;
      cur = cur->_right;
    } else if (cur->_key > key) {
      parent = cur;
      cur = cur->_left;
    } else {
      // 1、左为空
      if (cur->_left == nullptr) {  // 如果当前节点没有左子树
        if (cur == _root) {         // 如果当前节点是根节点
          _root = cur->_right;  // 将根节点改为当前节点的右子树
        } else {                // 如果当前节点不是根节点
          if (parent->_left == cur) {  // 如果当前节点是父节点的左孩子
            parent->_left =
                cur->_right;  // 将父节点的左孩子改为当前节点的右子树
          } else {            // 如果当前节点是父节点的右孩子
            parent->_right =
                cur->_right;  // 将父节点的右孩子改为当前节点的右子树
          }
        }
        delete cur;  // 释放当前节点的内存
      }
      // 2、右为空
      else if (cur->_right == nullptr) {  // 如果当前节点没有右子树
        if (cur == _root) {               // 如果当前节点是根节点
          _root = cur->_left;  // 将根节点改为当前节点的左子树
        } else {               // 如果当前节点不是根节点
          if (parent->_left == cur) {  // 如果当前节点是父节点的左孩子
            parent->_left = cur->_left;  // 将父节点的左孩子改为当前节点的左子树
          } else {  // 如果当前节点是父节点的右孩子
            parent->_right =
                cur->_left;  // 将父节点的右孩子改为当前节点的左子树
          }
        }
        delete cur;  // 释放当前节点的内存
      }
      // 3、左右都不为空
      else  // 如果要删除的节点有两个子节点
      {
        // 找右树最小节点替代,也可以是左树最大节点替代
        Node* pminRight =
            cur;  // 定义一个指针指向当前节点,用来记录后继节点的父节点
        Node* minRight =
            cur->_right;  // 定义一个指针指向当前节点的右子树,用来寻找后继节点
        while (
            minRight->_left)  // 循环找到右子树中最小的节点,也就是最左边的节点
        {
          pminRight = minRight;        // 更新后继节点的父节点
          minRight = minRight->_left;  // 更新后继节点
        }

        cur->_key = minRight->_key;  // 将后继节点的值复制到当前节点

        if (pminRight->_left == minRight)  // 如果后继节点是它父节点的左孩子
        {
          pminRight->_left =
              minRight
                  ->_right;  // 将后继节点的父节点的左孩子改为后继节点的右子树
        } else  // 如果后继节点是它父节点的右孩子
        {
          pminRight->_right =
              minRight
                  ->_right;  // 将后继节点的父节点的右孩子改为后继节点的右子树
        }

        delete minRight;  // 释放后继节点的内存
      }

      return true;
    }
  }
  return false;
}

Find() 函数

搜索:给定一个键值,在二叉搜索树中查找是否存在对应的节点。我们可以使用递归或迭代的方法来实现搜索。
搜索算法基于二叉搜索树的性质:

  1. 如果要查找的键值等于当前节点的键值,就返回当前节点;
  2. 如果要查找的键值小于当前节点的键值,就在左子树中继续查找;
  3. 如果要查找的键值大于当前节点的键值,就在右子树中继续查找;如果遇到空链接,就说明查找失败。

循环:

        //查找
            bool Find(const K& key)
            {
                Node* cur = _root;
                while (cur)
                {
                    if (cur->_key > key)
                    {
                        cur = cur->_left;
                    }
                    else if (cur->_key < key)
                    {
                        cur = cur->_right;
                    }
                    else
                    {
                        return true;
                    }
                }
                return false;
            }

递归:

bool _FindR(Node* root, const K& key)
         {
             if (root == nullptr)
                 return false;

             if (root->_key == key)
                 return true;

             if (root->_key < key)
             {
                 return FindR(root->_right);
             }
             else
             {
                 return FindR(root->_left);
             }
         }

Insert() 函数

插入:给定一个键值和一个数据值,在二叉搜索树中插入一个新的节点。我们也可以使用递归或迭代的方法来实现插入。
插入算法基于二叉搜索树的性质:

  1. 如果要插入的键值等于当前节点的键值,就更新当前节点的数据值;
  2. 如果要插入的键值小于当前节点的键值,就在左子树中继续插入,并将返回的新的左子树赋给当前节点的左子链接;
  3. 如果要插入的键值大于当前节点的键值,就在右子树中继续插入,并将返回的新的右子树赋给当前节点的右子链接;如果遇到空链接,就创建一个新的节点并返回它。

循环:


// 插入一个键值为key的节点到二叉搜索树中
bool Insert(const K& key) {
  // 如果根节点为空,直接创建一个新节点作为根节点
  if (_root == nullptr) {
    _root = new Node(key);
    return true;
  }

  // 定义一个父节点指针和一个当前节点指针,从根节点开始遍历
  Node* parent = nullptr;
  Node* cur = _root;
  while (cur) {
    // 如果当前节点的键值小于要插入的键值,向右子树查找,并更新父节点
    if (cur->_key < key) {
      parent = cur;
      cur = cur->_right;
    }
    // 如果当前节点的键值大于要插入的键值,向左子树查找,并更新父节点
    else if (cur->_key > key) {
      parent = cur;
      cur = cur->_left;
    }
    // 如果当前节点的键值等于要插入的键值,说明已经存在,返回false
    else {
      return false;
    }
  }

  // 创建一个新节点,键值为key
  cur = new Node(key);
  // 根据父节点的键值判断新节点是左孩子还是右孩子,并链接
  if (parent->_key < key) {
    parent->_right = cur;
  } else {
    parent->_left = cur;
  }

  // 返回true表示插入成功
  return true;
}

递归方式:

 bool _Insert(Node*& root, const K& key)
         {
             if (root = nullptr)
             {
                 root = new Node(key);
                 return true;
             }

             if (root->_key < key)
             {
                 return _InsertR(root->_right, key);
             }
             else if (root->_key > key)
             {
                 return _InsertR(root->_left, key);
             }
             else
             {
                 return false;
             }
         }

copy( ) 函数

这个复制函数是属于先复制后链接的方法。它的思想是先创建一个新的节点,然后再递归地复制它的左右子树,并将它们链接到新节点上。这样,每个节点都会被复制一次,并且保持了原始树的结构和顺序。你可以这样理解:

  • 假设原始树是这样的:
    A
   / \
  B   C
 / \ / \
D  E F  G
  • 那么复制函数会先创建一个新的节点A’,然后递归地复制A的左子树和右子树,并将它们链接到A’上,得到这样的树:
    A'
   / \
  B'  C'
 / \ / \
D' E'F' G'
  • 其中,每个节点都是原始树对应节点的副本,例如B’是B的副本,C’是C的副本,以此类推。

代码:

  Node* copy(Node* root) //先复制后链接
         {
             if (root == nullptr)
             {
                 return nullptr;
             }

             Node* newRoot = new Node(root->_key);
             newRoot->_left = copy(root->_left);
             newRoot->_right = copy(root->_right);

             return newRoot;
         }

整体代码

贴上码云仓库的连接:二叉搜索树K模型实现

另一个知识点:
例如查找给定的键是否存在于二叉搜索树中,调用私有的_FindR函数。这种方式是为了把_root作为参数传递给私有函数,这样私有函数就可以递归地操作二叉搜索树的节点。如果不这样做,私有函数就无法访问_root,因为它是一个私有的成员变量。

三、 二叉搜索树的性能分析

二叉搜索树的性能分析主要取决于树的高度,即从根节点到最深的叶子节点的层数。树的高度决定了每次操作需要访问的节点个数,因为每次操作都是沿着树的路径进行的。

二叉搜索树的高度又取决于树的形状,即节点在树中的分布。树的形状又取决于插入节点的顺序。如果插入节点的顺序是随机的,那么二叉搜索树会趋向于平衡,即左右子树的高度相差不大。如果插入节点的顺序是有序的,那么二叉搜索树会退化为链表,即只有一条单边路径。

我们可以用以下公式来估计二叉搜索树操作的平均时间复杂度:

  • 如果插入节点是随机顺序的,那么二叉搜索树的高度约为 ,其中 为节点个数,为自然对数底。因此,每次操作需要比较约 次键值。
  • 如果插入节点是有序顺序的,那么二叉搜索树的高度约为 ,其中 为节点个数。因此,每次操作需要比较约 次键值。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
可以看出,随机顺序插入节点可以使二叉搜索树保持较低的高度,从而提高操作效率。

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

**问题:**如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插
入关键码,二叉搜索树的性能都能达到最优?后续介绍平衡二叉搜索树 — AVL树和红黑树。

在这里插入图片描述

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

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

相关文章

SpringBoot 中使用Guava实现单机令牌桶限流

SpringBoot项目中如何对接口进行限流&#xff0c;有哪些常见的限流算法&#xff0c;如何优雅的进行限流。 首先就让我们来看看为什么需要对接口进行限流&#xff1f; 为什么要进行限流&#xff1f; 因为互联网系统通常都要面对大并发大流量的请求&#xff0c;在突发情况下&a…

你还还还没学会RabbitMQ?-----------RabbitMQ详解及快速入门(工作模式)

你像天外来物一样&#xff0c;求之不得&#xff08;咳咳&#xff0c;指offer&#xff09;&#x1f339; 文章目录什么是MQ&#xff1f;MQ的优势与劣势使用MQ需要满足的条件常见的MQ产品关于RabbitMQ生产者消费者工作模式订阅模式路由模式通配符模式什么是MQ&#xff1f; Messa…

机器学习——支持向量机的训练

目录 实践SVM分类 测试1-1​编辑 测试1-2 SVM核心 支持向量机函数 分类器SVC的主要属性: 分类器SVC的主要方法: 回归器SVR的主要属性: 支持向量机在鸢尾花分类中的应用 实践SVM分类 (1)参数C的选择: C为惩罚系数,也称为正则化系数: C越小模型越受限&#xff08;即单个数据…

【设计模式】从Mybatis源码中学习到的10种设计模式

文章目录一、前言二、源码&#xff1a;学设计模式三、类型&#xff1a;创建型模式1. 工厂模式2. 单例模式3. 建造者模式四、类型&#xff1a;结构型模式1. 适配器模式2. 代理模式3. 组合模式4. 装饰器模式五、类型&#xff1a;行为型模式1. 模板模式2. 策略模式3. 迭代器模式六…

长江流域9省2市可视化(不展示业务信息水质及真实断面)

一、处理9省2市地理信息为geojson集成到项目 shp转geojson关键Java代码 /*** shp转换为Geojson* param shpPath* return*/ public static Map shape2Geojson(String shpPath,String filePath){Map map new HashMap();FeatureJSON fjson new FeatureJSON();try{StringBuffer …

阶段二33_面向对象高级_IO[转换流,对象流]

知识点&#xff1a; 1.转换流&#xff1a;InputStreamReader&#xff0c;OutputStreamWriter2.对象流&#xff1a;ObjectInputStream&#xff0c;ObjectOutputStream一.转换流 1.转换流原理图 2.转换流概述 转换流就是来进行字节流和字符流之间转换的 InputStreamReader是从…

p75 应急响应-数据库漏洞口令检索应急取证箱

数据来源 必须知识点&#xff1a; 第三方应用由于是选择性安装&#xff0c;如何做好信息收集和漏洞探针也是获取攻击者思路的重要操作&#xff0c; 除去本身漏洞外&#xff0c;提前预知或口令相关攻击也要进行筛选。排除三方应用攻击行为&#xff0c;自查漏洞分析攻击者思路&a…

表白墙(服务器版)

文章目录一、准备工作二、前后端交互后端前端三、数据库版本一、准备工作 我们之前实现过这样一个表白墙&#xff0c;具体前端代码参考 表白墙 这篇文章 但是我们之前写的这个表白墙有一些问题&#xff1a; 1.如果我们刷新页面/重新开启&#xff0c;之前的数据就不见了 2.我们…

python pyc文件

参考自 What are pyc files in Python 和Python什么情况下会生成pyc文件&#xff1f; - 知乎 加上了我自己的理解 官方文档有这么解释 A program doesnt run any faster when it is read from a ‘.pyc’ or ‘.pyo’ file than when it is read from a ‘.py’ file; the o…

C生万物 | 一探指针函数与函数指针的奥秘

文章目录一、指针函数1、定义2、示例二、函数指针1、概念理清2、如何调用函数指针&#xff1f;3、两道“有趣”的代码题O(∩_∩)O< 第一题 >< 第二题 >4、函数指针数组概念明细具体应用&#xff1a;转移表✔5、指向函数指针数组的指针三、实战训练 —— 回调函数1、…

Pix4D软件简易使用方法

一、实验目的 学习无人机处理软件 Pix4D 的各项基本功能模块&#xff0c;掌握处理无人机影像的一般处理流程及质量评价。学习新建项目&#xff0c;对图像进行初始化操作以便后处理。学会制作正射影像图&#xff0c;生成质量报告&#xff0c;并对其进行分析。 二、实验内容 &…

抽象轻松MySqL

第一步安装下载MySQL 手把手教你下载安装 第一步打开官方网站 这里提供两种——第一种懒人版&#xff1a;MySQL点击蓝色字会有链接 第二种手动版本&#xff1a;百度搜索Mysql&#xff08;注意不要点.cn的因为有点翻译问题&#xff09; 点开后的图如下 接下来开始装备下载 点…

Disentangled Graph Collaborative Filtering

代码地址&#xff1a;https://github.com/ xiangwang1223/disentangled_graph_collaborative_filtering Background&#xff1a; 现有模型在很大程度上以统一的方式对用户-物品关系进行建模(将模型看做黑盒&#xff0c;历史交互作为输入&#xff0c;Embedding作为输出。)&…

【C++进阶之路】初始C++

文章目录一.C的发展历史时代背景产生原因发型版本二.C的应用场景三.C 的学习成本C的难度C的学习阶段21天精通C的梗一.C的发展历史 时代背景 20世纪60年代——软件危机。部分原因:C语言等计算机语言是面向过程语言&#xff0c;在编写大型程序需要高度抽象与建模&#xff0c;此…

HTML中表格标签<table><tr><tb><th>中单元格的合并问题

前情知晓 层级关系如下&#xff1a; <table><tr><td> </td><th> </th></tr></table> <table>...</table> 用于定义一个表格开始和结束 <tr>...</tr> 定义一行标签&#xff0c;一组行标签内可以建立…

【前端】从零开始读懂Web3

序言 用心生活&#xff0c;用力向上&#xff0c;微笑前行&#xff0c;就是对生活最好的回馈。 本专栏说明&#xff1a; 主要是记录在分享知识的同时&#xff0c;不定时给大家送书的活动。 参与方式&#xff1a; 赠书数量&#xff1a;本次送书 3 本&#xff0c;评论区抽3位小伙伴…

Python进阶特性(类型标注)

1.4 Python进阶特性(类型标注) 1.4.1 类型标注介绍 Python属于动态类型语言&#xff0c;只有在运行代码的时候才能够知道变量类型&#xff0c;那么这样容易产生传入参数类型不一致的问题&#xff0c;导致出现意想不到的bug。这也是动态类型语言天生的一个问题。 所以在Python…

【Spring】— Spring中Bean的装配方式

Spring中Bean的装配方式Bean的装配方式1.基于XML的装配2.基于Annotation的装配3.自动装配Bean的装配方式 Bean的装配可以理解为依赖关系注入&#xff0c;Bean的装配方式即Bean依赖注入的方式。Spring容器支持多种形式的Bean装配方式&#xff0c;如基于XML的装配、基于Annotatio…

电力系统中针对状态估计的虚假数据注入攻击建模与对策(Matlab代码实现)

&#x1f352;&#x1f352;&#x1f352;欢迎关注&#x1f308;&#x1f308;&#x1f308; &#x1f4dd;个人主页&#xff1a;我爱Matlab &#x1f44d;点赞➕评论➕收藏 养成习惯&#xff08;一键三连&#xff09;&#x1f33b;&#x1f33b;&#x1f33b; &#x1f34c;希…

免费部署属于自己的chatGPT网站,欢迎大家试玩

最近我发现了一个非常nice的部署网站的工具&#xff0c; railway&#xff0c;这个网站是国外的&#xff0c;所以部署出来的项目域名是国外的&#xff0c;并不需要担心封号&#xff0c;也不需要进行域名注册&#xff0c;部署成功之后会自动生成域名&#xff0c;在国内就能够正常…