跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

C++ 二叉搜索树 (BST) 详解:原理、核心操作与实战实现

二叉搜索树 (BST) 是一种兼具有序性与高效操作的树形结构,中序遍历可得升序序列。本文详解其核心概念、性能分析(理想 O(logN) 至最差 O(N))及 C++ 模板实现。涵盖插入、查找、删除三大操作逻辑,重点解析删除节点时的替换策略。同时扩展 Key-Value 模型,演示字典查询与词频统计等实际应用场景,为后续学习平衡树奠定基础。

CoderByte发布于 2026/3/24更新于 2026/5/13 浏览
C++ 二叉搜索树 (BST) 详解:原理、核心操作与实战实现

C++ 二叉搜索树 (BST) 详解:原理、核心操作与实战实现

在数据结构中,二叉搜索树 (Binary Search Tree,简称 BST) 是一种兼具'有序性'与'高效操作'的树形结构。它通过特定的节点值排列规则,让增删查操作的时间复杂度在理想情况下可达到 O(log₂N),但最坏情况仍可能退化为 O(N)。本文将拆解其核心概念,结合实战代码,逐步讲解插入、查找、删除三大操作的实现细节。

一、二叉搜索树的核心概念

二叉搜索树又称二叉排序树。它要么是空树,要么满足以下分布规则:

  • 若左子树不为空,则左子树中所有节点的值 ≤ 根节点的值;
  • 若右子树不为空,则右子树中所有节点的值 ≥ 根节点的值;
  • 左、右子树也分别是二叉搜索树(递归定义)。

关键特性:中序遍历为有序序列

二叉搜索树的核心价值在于'中序遍历结果是升序序列'。这也是其被称为'排序树'的原因。

关于'相等值'的约定: BST 对相等值的处理可灵活定义,具体取决于场景:

  1. 不支持相等值插入(如 std::map/std::set 底层):插入时若值已存在,直接返回失败。
  2. 支持相等值插入(如 std::multimap/std::multiset 底层):相等值需统一插入左子树或右子树。

本文实现的 BST 默认不支持相等值插入。

二、性能分析:理想与最差情况

BST 的操作效率直接取决于树的'高度',而高度由节点插入顺序决定。

场景树的形态高度时间复杂度典型插入顺序
理想情况完全二叉树(接近平衡)log₂NO(log₂N)随机插入
最差情况单支树(退化为链表)NO(N)有序插入

虽然二分查找也能实现 O(log₂N) 的查找效率,但它依赖支持随机访问的结构(如数组),且插入/删除需挪动大量数据。而 BST 无需提前排序,且插入/删除仅需修改指针,避免了数据移动,在动态数据场景中更具优势。

三、BST 的实战实现

我们采用 C++ 模板实现,支持泛型 K(内置类型或自定义类型)。核心包含节点结构定义与三大操作(插入、查找、删除),并提供中序遍历接口验证有序性。

1. 节点结构定义

BST 的节点需存储'值'与'左右子树指针'。模板化设计使其可适配多种类型:

// BinarySearch.h
#include <iostream>
using namespace std;

template<class K>
class BSTNode {
public:
    // 构造函数:初始化列表指针为空,键值为传入值
    BSTNode(const K& key = 0) : _key(key), _left(nullptr), _right(nullptr) {}

    K _key;             // 节点键值
    BSTNode<K>* _left;  // 左子树指针
    BSTNode<K>* _right; // 右子树指针
};

2. 核心操作:Insert、Find、Erase

BST 类封装了树的根节点 _root,并通过辅助函数实现中序遍历。以下是三大核心操作的详细实现与解析。

2.1 插入操作 (Insert)

插入的核心逻辑是'按 BST 规则找到空位置,创建新节点并链接'。

  1. 若树为空 (_root == nullptr),直接新增结点赋值给 _root。
  2. 树非空时,用 cur 指针遍历树找到符合要求的空位置:
    • 若 cur->_key < 插入值:向右子树移动。
    • 若 cur->_key > 插入值:向左子树移动。
    • 若值相等:返回 false(本实现不支持重复值)。

找到空位置后,通过 father 指针(记录 cur 的父亲节点)将新节点链接到树中。注意这里需要根据大小关系判断挂接到父节点的左边还是右边,而不是比较指针地址。

template<class K>
class BSTree {
public:
    typedef BSTNode<K> Node;

    // 二叉搜索树的插入
    bool Insert(const K& key) {
        if (_root == nullptr) {
            _root = new Node(key);
            return true;
        }

        Node* cur = _root;
        Node* father = nullptr;
        while (cur) {
            if (cur->_key > key) {
                father = cur;
                cur = cur->_left;
            } else if (cur->_key < key) {
                father = cur;
                cur = cur->_right;
            } else {
                // 相同则不符合插入规则,返回 false
                return false;
            }
        }

        // 循环结束说明找到了插入位置
        cur = new Node(key);
        
        // 根据 key 与 father->_key 的大小关系确定挂接方向
        if (key < father->_key) {
            father->_left = cur;
        } else {
            father->_right = cur;
        }
        return true;
    }

    // 中序遍历
    void Print() {
        _Print(_root);
        cout << endl;
    }

private:
    void _Print(const Node* root) {
        if (root == nullptr) return;
        _Print(root->_left);
        cout << root->_key << " ";
        _Print(root->_right);
    }

private:
    Node* _root = nullptr;
};

测试代码:

void Test1() {
    int arr[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
    BSTree<int> bst1;
    for (auto e : arr) {
        bst1.Insert(e);
    }
    bst1.Print(); // 输出应为有序序列
}
2.2 查找操作 (Find)

查找逻辑与插入类似,按 BST 规则遍历树即可。

bool Find(const K& x) {
    Node* cur = _root;
    while (cur) {
        if (cur->_key > x) {
            cur = cur->_left;
        } else if (cur->_key < x) {
            cur = cur->_right;
        } else {
            return true;
        }
    }
    return false;
}
2.3 删除操作 (Erase)

删除是最复杂的核心操作。难点在于'删除节点后,需保持 BST 的规则不变'。根据删除节点 (cur) 的子节点数量,分为几种情况:

  1. 叶子节点(左右均为空):直接删除,将父节点指向该孩子的指针置空。
  2. 单侧子树(左空右不空,或反之):将父节点指向 cur 的指针,修改为指向 cur 的非空孩子。
  3. 双侧子树均非空:使用替换法。找 cur 右子树的'最小节点'(最左节点)或左子树的'最大节点'(最右节点),将其值赋给 cur,然后删除那个替换节点(此时替换节点必为前两种情况之一)。

关键点:选择'右子树最小节点'作为替换节点,是因为该节点的值是 cur 右子树中最小的,替换后仍满足 BST 规则(左子树 ≤ 根 ≤ 右子树)。

bool Erase(const K& x) {
    Node* cur = _root;
    Node* father = nullptr;

    // 先查找目标结点
    while (cur) {
        if (cur->_key > x) {
            father = cur;
            cur = cur->_left;
        } else if (cur->_key < x) {
            father = cur;
            cur = cur->_right;
        } else {
            // 找到待删除节点 cur
            // 情况 1 & 2: 左子树为空
            if (cur->_left == nullptr) {
                if (cur == _root) {
                    _root = cur->_right;
                } else {
                    if (father->_left == cur) {
                        father->_left = cur->_right;
                    } else {
                        father->_right = cur->_right;
                    }
                }
                delete cur;
                return true;
            }
            // 情况 3: 右子树为空
            else if (cur->_right == nullptr) {
                if (cur == _root) {
                    _root = cur->_left;
                } else {
                    if (father->_left == cur) {
                        father->_left = cur->_left;
                    } else {
                        father->_right = cur->_left;
                    }
                }
                delete cur;
                return true;
            }
            // 情况 4: 左右都不为空,替换法
            else {
                Node* min_right_father = nullptr;
                Node* min_right = cur->_right;
                // 找右子树最小节点
                while (min_right->_left) {
                    min_right_father = min_right;
                    min_right = min_right->_left;
                }
                
                // 交换键值
                cur->_key = min_right->_key;
                
                // 删除替换节点 (min_right)
                // 此时 min_right 必然没有左孩子,属于情况 1 或 2
                if (min_right_father == nullptr) {
                    // 最小节点就是 cur 的右孩子
                    cur->_right = min_right->_right;
                } else {
                    // 最小节点是某节点的左孩子
                    min_right_father->_left = min_right->_right;
                }
                delete min_right;
                return true;
            }
        }
    }
    return false;
}

四、BST 的扩展:Key/Value 模型

上述实现是'Key 模型'(仅存储键值,用于判断存在性)。但在实际场景中,'Key-Value 模型'(键值对应数据,如字典、统计次数)更加有用。我们可以将模板扩展为 template<class K, class V>。

1. Key-Value 模型实现要点

由于功能逻辑相似,主要区别在于节点多存一个 value,且部分函数需考虑 value 的存放。此外,由于 BST 是动态开辟内存,必须手动实现析构函数以防止内存泄漏。同时,实现了析构函数后,编译器生成的默认拷贝构造会导致浅拷贝问题(析构时重复释放),因此需要手动实现深拷贝的拷贝构造函数。

namespace key_value {
    template<class K, class V>
    class BSTNode {
    public:
        BSTNode(const K& key = 0, const V& value = 0)
            : _key(key), _value(value), _left(nullptr), _right(nullptr) {}
        K _key;
        V _value;
        BSTNode<K, V>* _left;
        BSTNode<K, V>* _right;
    };

    template<class K, class V>
    class BSTree {
    public:
        typedef BSTNode<K, V> Node;

        ~BSTree() {
            Destroy(_root);
            _root = nullptr;
        }

        // 深拷贝构造
        BSTree(const BSTree<K, V>& bst) {
            _root = Copy(bst._root);
        }

        BSTree() = default;

        // 插入
        bool Insert(const K& key, const V& value) {
            if (_root == nullptr) {
                _root = new Node(key, value);
                return true;
            }
            Node* cur = _root;
            Node* father = nullptr;
            while (cur) {
                if (cur->_key > key) {
                    father = cur;
                    cur = cur->_left;
                } else if (cur->_key < key) {
                    father = cur;
                    cur = cur->_right;
                } else {
                    return false;
                }
            }
            cur = new Node(key, value);
            if (key < father->_key) {
                father->_left = cur;
            } else {
                father->_right = cur;
            }
            return true;
        }

        // 查找 (返回节点指针以便修改 value)
        Node* Find(const K& x) {
            Node* cur = _root;
            while (cur) {
                if (cur->_key > x) {
                    cur = cur->_left;
                } else if (cur->_key < x) {
                    cur = cur->_right;
                } else {
                    return cur;
                }
            }
            return nullptr;
        }

        // 删除逻辑同 Key 模型,此处省略重复代码,逻辑一致
        bool Erase(const K& x) {
            // ... (参考上文 Erase 逻辑,增加 value 同步更新)
            // 简化示意:找到后 cur->_key = min_right->_key; cur->_value = min_right->_value;
            // 实际实现需完整复制上文 Erase 逻辑并处理 value
            return false; 
        }

    private:
        void _Print(const Node* root) {
            if (root == nullptr) return;
            _Print(root->_left);
            cout << root->_key << ":" << root->_value << endl;
            _Print(root->_right);
        }

        void Destroy(const Node* root) {
            if (root == nullptr) return;
            Destroy(root->_left);
            Destroy(root->_right);
            delete root;
        }

        Node* Copy(Node* root) {
            if (root == nullptr) return nullptr;
            Node* newroot = new Node(root->_key, root->_value);
            newroot->_left = Copy(root->_left);
            newroot->_right = Copy(root->_right);
            return newroot;
        }

    private:
        Node* _root = nullptr;
    };
}

2. 实战场景

场景 1:简单字典(中英互译)

树结构中存储 key (英文) 和 value (中文),搜索时输入英文,直接获取对应的中文。

void Test2() {
    key_value::BSTree<string, string> dict;
    dict.Insert("left", "左边");
    dict.Insert("right", "右边");
    dict.Insert("insert", "输入");
    
    string str;
    while (cin >> str) {
        auto ret = dict.Find(str);
        if (ret) {
            cout << "->" << ret->_value << endl;
        } else {
            cout << "无此单词" << endl;
        }
    }
}
场景 2:单词统计

读取文本,查找单词是否存在。不存在则插入 <单词,1>,存在则 ++ 次数。

void Test3() {
    string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "香蕉" };
    key_value::BSTree<string, int> countTree;

    for (const auto& str : arr) {
        auto ret = countTree.Find(str);
        if (ret == nullptr) {
            countTree.Insert(str, 1);
        } else {
            ret->_value++; // 找到节点后直接修改 value
        }
    }
    countTree.Print();
}

到此,二叉搜索树的基础概念和功能实现就讲解完了。虽然中序遍历的有序性与指针操作的灵活性是其核心优势,但其性能受插入顺序影响大,导致出现单支树退化问题。后续学习平衡树(AVL、红黑树)时,我们将重点解决这一退化问题。作为基础树形结构,BST 是理解复杂数据结构设计逻辑的重要基石。

目录

  1. C++ 二叉搜索树 (BST) 详解:原理、核心操作与实战实现
  2. 一、二叉搜索树的核心概念
  3. 二、性能分析:理想与最差情况
  4. 三、BST 的实战实现
  5. 1. 节点结构定义
  6. 2. 核心操作:Insert、Find、Erase
  7. 2.1 插入操作 (Insert)
  8. 2.2 查找操作 (Find)
  9. 2.3 删除操作 (Erase)
  10. 四、BST 的扩展:Key/Value 模型
  11. 1. Key-Value 模型实现要点
  12. 2. 实战场景
  13. 场景 1:简单字典(中英互译)
  14. 场景 2:单词统计
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • DeepSeek R1 在 RK3588 上的 RKLLM 转换与 Web 部署流程
  • Bright Data SERP API 测评:高精度全球化 SEO 数据引擎
  • 在 Jetson 上部署 OpenClaw 并接入飞书机器人实现远程交互
  • 在 IntelliJ IDEA 中修改 Git 远程仓库地址
  • 计算机网络基础知识详解:OSI 与 TCP/IP 模型
  • Stable Diffusion 云端版部署与个性化绘本生成指南
  • AIGC 时代如何利用 DeepSeek 辅助孩子学习编程
  • 解决 PKIX path building failed:SSL 证书导入 Java 信任库实战
  • Web1.0 到 Web3.0:互联网三次进化解析
  • Spring Boot 数据仓库与 ETL 工具集成实践
  • 腾讯混元图像 3.0 图生图模型开源,LMArena 评测跻身全球第一梯队
  • Python 自动化实战:wxauto 安装异常处理与核心功能详解
  • JavaScript 基础语法与核心概念详解
  • 前端实现视频画中画功能 - 主页面与小窗同步控制
  • Soft Actor-Critic (SAC) 算法详解与 PyTorch 实现
  • 基于 JsPDF 和 html2canvas 实现前端图表与列表数据多格式导出
  • C++ 仿函数详解:对象像函数一样调用
  • 分布式文件系统 HDFS 存储原理
  • 前端三年职业复盘:从传统软件到互联网实战
  • 一次红队渗透实战项目记录

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online