C++二叉搜索树进阶

一文搞懂二叉搜索树进阶|链式存储、插入、删除完整教程

📖 本文来源于课堂讲义,面向初学C++的同学,尽量少晦涩术语,用生活例子讲懂指针,配套全部例题与代码。

学习时长:约1.5小时,分为2个课时

📚 课程总览思维导图

🎯 教学目标

知识与技能

  1. 二叉树的链式存储
  2. 使用new运算符新建结点
  3. 二叉搜索树(BST)插入操作
  4. 二叉搜索树(BST)删除操作

过程与方法

  1. 综合知识完成复杂代码编写
  2. 融会贯通,优化程序逻辑

情感态度价值观

锻炼细心观察、遇到难题不放弃的学习品质

✨重点:链式存储、new运算符、BST插入、BST删除

⚠️难点:估算时间复杂度、独立解决问题、灵活做题


第一部分:链式存储二叉树

之前上课我们学过二叉树的4种遍历(层序、先序、中序、后序)、查找最大值最小值。

如果树固定不变,我们可以用数组下标保存结点(静态数组);如果需要随时增加、删掉结点,就需要链式存储

🧩生活类比理解指针

想象你有很多张小纸条(树结点),每一张纸条写一个数字。

  • 静态数组:所有纸条整齐摆在一排柜子,柜子编号就是下标,想找哪一张直接报柜子号。柜子数量提前定死,不能随便增加柜子。
  • 链式存储(指针) :每一张纸条,除了写数字,额外画2个箭头。一个箭头指向左边纸条、一个箭头指向右边纸条。箭头什么都不指的时候,就是空指针nullptr。>

nullptr就是箭头悬空,什么都没指向,C++专门代表空指针,比旧的NULL更安全。

每一个结点(纸条)三要素:

  1. 保存的值

  2. 左箭头(左孩子指针)

  3. 右箭头(右孩子指针)

    ┌───────────────┐
    │ value:数字 │
    │ left:←箭头 │
    │ right:→箭头 │
    └───────────────┘

案例1:只有1个结点,左右箭头全部悬空nullptr

案例2:3个结点,根结点1箭头指向2、3

复制代码
graph TD
1((1)) --> 2((2))
1 -->3((3))

1.1 new运算符:动态造纸条(结点)

静态数组:提前摆好全部纸条,数量写死。

new运算符:程序运行的时候,随时现场新造一张纸条,造多少张都可以。

生活类比:静态数组=提前打印好一堆纸条;new=需要的时候,当场打印一张新纸条。

两种写法:

  1. 只开辟内存,自己手动填内容

    // 造一张空纸条
    Node* node = new Node;
    node->value = 12; //写上数字12
    node->left = nullptr; //左箭头悬空
    node->right = nullptr; //右箭头悬空

  2. 使用构造函数,造纸条同时直接填好数字>

构造函数:纸条出厂设置,一打印出来就把数字写好,箭头直接悬空。函数名字必须和结构体名字一模一样。

复制代码
struct Node{
    int value;
    Node* left;
    Node* right;
    //构造函数:出厂设置
    Node(int val){
        value = val;
        left = nullptr;
        right = nullptr;
    }
};
// new直接造已经写好12的纸条
Node* node = new Node(12);

示例:链式保存这棵树

复制代码
graph TD
1((1)) -->2((2))
1 -->3((3))

完整步骤:

  1. 写好带构造函数的Node结构体

  2. new出3张纸条

    Node* n1 = new Node(1);
    Node* n2 = new Node(2);
    Node* n3 = new Node(3);

  3. 修改箭头,建立连接

    n1->left = n2; //1的左箭头指向纸条2
    n1->right = n3; //1的右箭头指向纸条3

n1就是根,拿着第一张纸条,顺着箭头就能访问整棵树。

1.2 链式二叉树遍历

和数组版逻辑一样,区别:以前传数组下标,现在传纸条指针。

递归:中序遍历(左→根→右)

复制代码
void inOrder(Node* root){
    if(root == nullptr) return; //箭头悬空,到此结束
    inOrder(root->left);   //顺着左箭头走
    cout << root->value << " "; //打印当前纸条数字
    inOrder(root->right);  //顺着右箭头走
}

层序遍历(队列BFS,广度优先)

队列里面存放纸条的指针(箭头),不是数字。

复制代码
#include<queue>
void bfs(Node* root){
    if(root == nullptr) return;
    queue<Node*> q;
    q.push(root); //根纸条入队
    while(!q.empty()){
        Node* node = q.front(); q.pop();
        cout << node->value << " ";
        if(node->left != nullptr) q.push(node->left);
        if(node->right != nullptr) q.push(node->right);
    }
}

📝学以致用练习题1

题目:链式存储下面二叉树,输出层序遍历、中序遍历,各输出一行

复制代码
graph TD
1((1)) -->2((2))
1 -->3((3))
2 -->4((4))
2 -->5((5))
3 -->6((6))

✅输出

复制代码
1 2 3 4 5 6
4 2 5 1 3 6

完整代码

复制代码
#include<iostream>
#include<queue>
using namespace std;

struct Node{
    int value;
    Node* left;
    Node* right;
    Node(int val){
        value = val;
        left = right = nullptr;
    }
};

//层序遍历
void bfs(Node* root){
    if(root == nullptr) return;
    queue<Node*> q;
    q.push(root);
    while(!q.empty()){
        Node* node = q.front(); q.pop();
        cout << node->value << " ";
        if(node->left != nullptr) q.push(node->left);
        if(node->right != nullptr) q.push(node->right);
    }
}

//中序遍历
void inOrder(Node* root){
    if(root == nullptr) return;
    inOrder(root->left);
    cout << root->value << " ";
    inOrder(root->right);
}

int main(){
    //新建全部结点
    Node* n4 = new Node(4);
    Node* n5 = new Node(5);
    Node* n2 = new Node(2);
    n2->left = n4;
    n2->right = n5;

    Node* n6 = new Node(6);
    Node* n3 = new Node(3);
    n3->right = n6;

    Node* n1 = new Node(1);
    n1->left = n2;
    n1->right = n3;

    bfs(n1);
    cout << endl;
    inOrder(n1);
    return 0;
}

第二部分:二叉搜索树 BST 插入 & 删除

二叉搜索树规则:左子树全部数字 < 当前结点数字 < 右子树全部数字

2.1 BST插入操作

生活类比:整理书架。来了一本新书,和当前手里书对比。比当前书小放左边,比当前书大放右边;如果位置是空,就把新书放这里;书已经存在,不重复摆放。

插入4种情况:

  1. 当前位置是空(nullptr):新建结点放这里
  2. 当前结点数字等于要插入数字,已经存在,什么都不干
  3. 待插入数字 < 当前结点数字:去左子树继续找位置
  4. 待插入数字 > 当前结点数字:去右子树继续找位置

图示例子:往BST插入数字6

复制代码
graph TD
8((8)) -->4((4))
8 -->12((12))
4 -->1((1))
4 -->5((5))
12 -->10((10))
12 -->14((14))
5 -.->|插入6|6((6))

递归插入代码,注意!必须接收返回值更新指针!

复制代码
//返回插入完成之后这颗子树的根指针
Node* insert(Node* root, int num){
    if(root == nullptr){
        return new Node(num); //空位,造新结点
    }
    if(root->value == num){
        return root; //重复,直接返回
    }
    if(root->value < num){
        root->right = insert(root->right, num); //右边插入
    }else{
        root->left = insert(root->left, num); //左边插入
    }
    return root;
}

调用示范:

复制代码
Node* root = nullptr;
root = insert(root, 8); //一定要接住返回值!

构造二叉搜索树

把一串数字,循环调用insert逐个插入。第一个插入数字直接成为树根。

复制代码
Node* root = nullptr;
int arr[] = {14,3,5,7,1,20,8};
for(int i=0;i<7;i++){
    root = insert(root, arr[i]);
}

2.2 BST删除操作(最难!3种场景)

生活类比:整理书架拿走一本书。分三种情况:

①这本书旁边没有别的书(叶子结点),直接拿走。

②这本书只有一边有书,拿走它,把剩下那一堆书接上来。

③这本书左右两边都有书,不能直接拿走。找**右边书架最小的书(或者左边书架最大书)**放到这个位置,再把原来那本书删掉。

图示案例原始树:

复制代码
graph TD
8((8)) -->4((4))
8 -->12((12))
4 -->1((1))
4 -->5((5))
12 -->10((10))
12 -->14((14))

场景1:删除叶子结点(度0,没有孩子)

例如删除5,直接把父结点指向它的箭头改成悬空。

复制代码
graph TD
8((8)) -->4((4))
8 -->12((12))
4 -->1((1))
12 -->10((10))
12 -->14((14))

场景2:结点只有1个孩子(度1)

删除4,4只有左孩子1。把4拿走,让8的左箭头直接指向1。

复制代码
graph TD
8((8)) -->1((1))
8 -->12((12))
12 -->10((10))
12 -->14((14))

场景3:结点有左右两个孩子(度2)

例如删除12。

方案:拿右子树最小结点 顶替12的值,然后再把右子树里面那个最小结点删掉。

补充:也可以拿左子树最大值顶替。

复制代码
graph TD
8((8)) -->4((4))
8 -->14((14))
4 -->1((1))
4 -->5((5))
14 -->10((10))
14 -->16((16))

辅助函数:找一棵树最小结点(一直往左边走到底)

复制代码
Node* findMin(Node* root){
    if(root == nullptr) return nullptr;
    while(root->left != nullptr){
        root = root->left;
    }
    return root;
}

删除完整递归代码

复制代码
Node* remove(Node* root, int num){
    if(root == nullptr) return nullptr;
    // 找要删除的结点
    if(num < root->value){
        root->left = remove(root->left, num);
    }else if(num > root->value){
        root->right = remove(root->right, num);
    }else{
        //找到了待删除结点
        //情况1、2:没有孩子 / 只有一个孩子
        if(root->left == nullptr){
            return root->right;
        }else if(root->right == nullptr){
            return root->left;
        }else{
            //情况3:左右都有孩子
            Node* temp = findMin(root->right);
            root->value = temp->value;
            root->right = remove(root->right, temp->value);
        }
    }
    return root;
}

⚠️调用的时候,一定要接收返回值:root = remove(root,14);

2.3 BST如何存储重复数字?

原生BST结点不允许重复。如果需要存重复值,改造结构体:增加计数变量cnt

复制代码
struct Node{
    int value;
    int cnt; //记录这个数字出现多少次
    Node* left;
    Node* right;
    Node(int val){
        value = val;
        cnt = 1;
        left = right = nullptr;
    }
};
  1. 插入重复值:找到结点,cnt++,不新建结点
  2. 删除:
    • 如果cnt>1:直接cnt--,结点保留
    • 如果cnt==1:真正删除结点
  3. 遍历输出:每一个结点循环输出cnt次value

📝学以致用练习题2

题目描述

输入4行:

  1. 第一行n
  2. 第二行n个不重复整数,依次插入构建BST
  3. 第三行:要删除的数字
  4. 第四行:要插入的数字

输出一共6行:

  1. 原始树中序遍历
  2. 原始树后序遍历
  3. 删除数字之后中序
  4. 删除数字之后后序
  5. 再插入数字之后中序
  6. 再插入数字之后后序

样例输入

复制代码
7
14 3 5 7 1 20 8
14
4

样例输出

复制代码
1 3 5 7 8 14 20
1 8 7 5 3 20 14
1 3 5 7 8 20
1 8 7 5 3 20
1 3 4 5 7 8 20
1 4 8 7 5 3 20

完整可运行代码

复制代码
#include<iostream>
using namespace std;

struct Node{
    int value;
    Node* left;
    Node* right;
    Node(int val){
        value = val;
        left = right = nullptr;
    }
};

Node* insert(Node* root, int num){
    if(root == nullptr){
        return new Node(num);
    }
    if(root->value == num) return root;
    if(root->value < num){
        root->right = insert(root->right, num);
    }else{
        root->left = insert(root->left, num);
    }
    return root;
}

Node* findMin(Node* root){
    if(root == nullptr) return nullptr;
    while(root->left != nullptr) root = root->left;
    return root;
}

Node* remove(Node* root, int num){
    if(root == nullptr) return nullptr;
    if(num < root->value){
        root->left = remove(root->left, num);
    }else if(num > root->value){
        root->right = remove(root->right, num);
    }else{
        if(root->left == nullptr){
            return root->right;
        }else if(root->right == nullptr){
            return root->left;
        }else{
            Node* temp = findMin(root->right);
            root->value = temp->value;
            root->right = remove(root->right, temp->value);
        }
    }
    return root;
}

//中序
void inOrder(Node* root){
    if(root == nullptr) return;
    inOrder(root->left);
    cout << root->value << " ";
    inOrder(root->right);
}

//后序
void postOrder(Node* root){
    if(root == nullptr) return;
    postOrder(root->left);
    postOrder(root->right);
    cout << root->value << " ";
}

int main(){
    int n;
    cin >> n;
    Node* root = nullptr;
    for(int i=0;i<n;i++){
        int x; cin >> x;
        root = insert(root, x);
    }
    inOrder(root); cout << endl;
    postOrder(root); cout << endl;

    int delNum; cin >> delNum;
    root = remove(root, delNum);
    inOrder(root); cout << endl;
    postOrder(root); cout << endl;

    int insNum; cin >> insNum;
    root = insert(root, insNum);
    inOrder(root); cout << endl;
    postOrder(root); cout << endl;
    return 0;
}

✨课堂总结

  1. 链式存储:结点用指针箭头相连,适合频繁增删;静态数组适合固定不变的树。
  2. new运算符:运行时动态创建结点;构造函数可以直接初始化结点。
  3. BST插入:递归找空位,一定要接收返回值更新根指针
  4. BST删除分3类:叶子结点;单孩子结点;双孩子结点(右子树最小替换)。
  5. 保存重复元素,新增计数cnt成员。

🧠举一反三拓展题:二叉搜索树最近公共祖先

根据学生不同进度,可以课堂完成或者课后练习。

生活类比

家谱。找两个人最近的共同长辈。BST有性质,可以不用递归遍历整棵树,直接利用大小关系快速查找。

简单思路:

  1. 如果当前结点比两个数字都大,公共祖先一定在左子树
  2. 如果当前结点比两个数字都小,公共祖先一定在右子树
  3. 其余情况,当前结点就是最近公共祖先。
复制代码
Node* lca(Node* root, int a, int b){
    if(root == nullptr) return nullptr;
    if(root->value > a && root->value > b){
        return lca(root->left,a,b);
    }
    if(root->value < a && root->value < b){
        return lca(root->right,a,b);
    }
    return root;
}

💡博客补充小贴士

  1. insertremove千万不要忘记写root = 函数(...),不然新建/删除结点不会生效!
  2. nullptr就是箭头什么都不指,递归函数第一句永远先判断if(root==nullptr) return,防止程序崩溃。
  3. BST中序遍历输出永远是从小到大排好序的序列。
相关推荐
HugoStudio_SWAN20 分钟前
【擦除重绘】C++ 控制台动画:弹跳 Logo DVD 屏保效果
开发语言·c++·学习·程序人生
kyle~2 小时前
C++_STL---迭代器失效
开发语言·c++
Brilliantwxx3 小时前
【C语言】 初入嵌入式C语言复习(基础+进阶面试题)
c语言·开发语言
熊野君3 小时前
附录与 Codex 实操手册
开发语言·人工智能·产品经理
wuminyu3 小时前
深入剖析 Panama Off-heap 的性能损耗与开销
java·linux·c语言·jvm·c++
rhythm-ring4 小时前
宏定义续行符 \ 的使用与踩坑
c语言·c++
XZ-0700014 小时前
week2-1-可视化
开发语言·python
Java后端的Ai之路4 小时前
14、Python - 责任链模式
服务器·开发语言·人工智能·python·责任链模式
吴声子夜歌5 小时前
Java——基本类型
java·开发语言