一文搞懂二叉搜索树进阶|链式存储、插入、删除完整教程
📖 本文来源于课堂讲义,面向初学C++的同学,尽量少晦涩术语,用生活例子讲懂指针,配套全部例题与代码。
学习时长:约1.5小时,分为2个课时
📚 课程总览思维导图

🎯 教学目标
知识与技能
- 二叉树的链式存储
- 使用
new运算符新建结点 - 二叉搜索树(BST)插入操作
- 二叉搜索树(BST)删除操作
过程与方法
- 综合知识完成复杂代码编写
- 融会贯通,优化程序逻辑
情感态度价值观
锻炼细心观察、遇到难题不放弃的学习品质
✨重点:链式存储、new运算符、BST插入、BST删除
⚠️难点:估算时间复杂度、独立解决问题、灵活做题
第一部分:链式存储二叉树
之前上课我们学过二叉树的4种遍历(层序、先序、中序、后序)、查找最大值最小值。
如果树固定不变,我们可以用数组下标保存结点(静态数组);如果需要随时增加、删掉结点,就需要链式存储。
🧩生活类比理解指针
想象你有很多张小纸条(树结点),每一张纸条写一个数字。
- 静态数组:所有纸条整齐摆在一排柜子,柜子编号就是下标,想找哪一张直接报柜子号。柜子数量提前定死,不能随便增加柜子。
- 链式存储(指针) :每一张纸条,除了写数字,额外画2个箭头。一个箭头指向左边纸条、一个箭头指向右边纸条。箭头什么都不指的时候,就是空指针
nullptr。>
nullptr就是箭头悬空,什么都没指向,C++专门代表空指针,比旧的NULL更安全。
每一个结点(纸条)三要素:
-
保存的值
-
左箭头(左孩子指针)
-
右箭头(右孩子指针)
┌───────────────┐
│ 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=需要的时候,当场打印一张新纸条。
两种写法:
-
只开辟内存,自己手动填内容
// 造一张空纸条
Node* node = new Node;
node->value = 12; //写上数字12
node->left = nullptr; //左箭头悬空
node->right = nullptr; //右箭头悬空 -
使用构造函数,造纸条同时直接填好数字>
构造函数:纸条出厂设置,一打印出来就把数字写好,箭头直接悬空。函数名字必须和结构体名字一模一样。
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))
完整步骤:
-
写好带构造函数的
Node结构体 -
new出3张纸条
Node* n1 = new Node(1);
Node* n2 = new Node(2);
Node* n3 = new Node(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种情况:
- 当前位置是空(
nullptr):新建结点放这里 - 当前结点数字等于要插入数字,已经存在,什么都不干
- 待插入数字 < 当前结点数字:去左子树继续找位置
- 待插入数字 > 当前结点数字:去右子树继续找位置
图示例子:往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;
}
};
- 插入重复值:找到结点,
cnt++,不新建结点 - 删除:
- 如果
cnt>1:直接cnt--,结点保留 - 如果
cnt==1:真正删除结点
- 如果
- 遍历输出:每一个结点循环输出cnt次value
📝学以致用练习题2
题目描述
输入4行:
- 第一行n
- 第二行n个不重复整数,依次插入构建BST
- 第三行:要删除的数字
- 第四行:要插入的数字
输出一共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;
}
✨课堂总结
- 链式存储:结点用指针箭头相连,适合频繁增删;静态数组适合固定不变的树。
- new运算符:运行时动态创建结点;构造函数可以直接初始化结点。
- BST插入:递归找空位,一定要接收返回值更新根指针。
- BST删除分3类:叶子结点;单孩子结点;双孩子结点(右子树最小替换)。
- 保存重复元素,新增计数
cnt成员。
🧠举一反三拓展题:二叉搜索树最近公共祖先
根据学生不同进度,可以课堂完成或者课后练习。
生活类比
家谱。找两个人最近的共同长辈。BST有性质,可以不用递归遍历整棵树,直接利用大小关系快速查找。
简单思路:
- 如果当前结点比两个数字都大,公共祖先一定在左子树
- 如果当前结点比两个数字都小,公共祖先一定在右子树
- 其余情况,当前结点就是最近公共祖先。
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;
}
💡博客补充小贴士
- 写
insert、remove千万不要忘记写root = 函数(...),不然新建/删除结点不会生效! nullptr就是箭头什么都不指,递归函数第一句永远先判断if(root==nullptr) return,防止程序崩溃。- BST中序遍历输出永远是从小到大排好序的序列。