数据结构 | 二叉搜索树

一、二叉搜索树定义

1. 标准定义

二叉搜索树(Binary Search Tree,简称BST )是在普通二叉树基础上增加有序约束的特殊二叉树,由 n(n≥0)个节点构成。空BST 无任何节点;非空BST所有节点关键字不允许重复,且必须遵守全局有序规则。

2. 核心性质

整棵树统一有序判定规则:任意节点,其左子树 内所有节点关键字 < 当前节点关键字 < 其右子树内所有节点关键字。

补充考点:对二叉搜索树进行中序 遍历,输出序列一定是升序 ;该特征是区分普通二叉树与BST最直观判定依据,考试高频出题点。

3. 专业基础名词:直接前驱、直接后继

直接前驱 指中序遍历序列里,当前节点的前一个节点,分三类查找情况:

  • 情况 1:当前节点左子树不为空,前驱为左子树中数值最大的节点,持续向右遍历左子树末端即可
  • 情况 2:当前节点左子树 为空,向上回溯父节点,若回溯节点关键字小于当前节点,继续向上回溯;若回溯节点关键字大于当前节点,该节点即为前驱
  • 情况 3:持续回溯直至父节点为 NULL,代表该节点无直接前驱(序列第一个元素)

直接后继 指中序遍历序列里,当前节点的下一个节点,分三类查找情况:

  • 情况 1:当前节点右子树不为空,后继为右子树中数值最小的节点,持续向左遍历右子树末端即可
  • 情况 2:当前节点右子树 为空,向上回溯父节点,若回溯节点关键字小于当前节点,继续向上回溯;若回溯节点关键字大于当前节点,该节点即为后继
  • 情况 3:持续回溯直至父节点为 NULL,代表该节点无直接后继(序列最后一个元素)

二、BST 存储结构

1. 存储设计思路

普通二叉树二叉链表仅存储左右孩子指针,无法向上回溯祖先节点,获取前驱、后继逻辑繁琐。本文采用三叉链表 存储结构,在原有左、右孩子指针基础上新增parent父节点指针,支持双向查找,大幅简化删除、前驱后继查询逻辑。

2. 结构体定义

分为两层结构体:BSTNode 为单个节点单元,BSTree封装根节点指针统一管理整棵树。

cpp 复制代码
// 保持原样使用前置声明
typedef int ELEMTYPE; // 假设元素类型为int

struct BSTNode {
    ELEMTYPE data;
    BSTNode* leftchild;
    BSTNode* rightchild;
    BSTNode* parent;
};

struct BSTree {
    BSTNode* root;
};

3. 三叉链表优缺点

优点:支持向上回溯父节点,快速获取前驱、后继;删除双分支节点逻辑简化,无需递归遍历查找父节点。

缺点:每个节点多存储一根父指针 ,内存开销更大;增删节点时需同步维护parent指针,操作不当极易出现野指针、指针指向错乱。

适配范围:频繁执行删除、查找前驱 / 后继的业务场景;仅做单纯查找、遍历场景推荐使用二叉链表节省内存。

三、底层工具函数

工具函数为业务操作提供底层支撑,仅由普通对外函数内部调用,不对外提供使用。

1. BuyNode 创建新节点

实现思路:动态分配节点内存,使用 C++ new 创建节点,构造函数自动初始化左右孩子、父指针全部置空,返回新节点地址。

代码实现

cpp 复制代码
// 申请新节点
BSTNode* BuyNode()
{
	BSTNode* pnewnode = new BSTNode;
	return pnewnode;
}

2. Get_PreNode 获取当前节点直接前驱

实现思路: 断言校验节点非空;分两大分支处理,左子树 存在取左子树最大值;左子树 不存在则向上回溯父节点匹配前驱规则。

代码实现

cpp 复制代码
BSTNode* Get_PreNode(BSTNode* node) {
	assert(node != nullptr);

	BSTNode* p = node;
	//当前节点左子树不为空,找左子树最大值
	if (node->leftchild != nullptr) {
		p = p->leftchild;
	  	while (p->rightchild != nullptr)
			p = p->rightchild;
	}
	//当前节点左子树为空,向上回溯父节点
	else {
		p = p->parent;
		while (p != nullptr && p->data > node->data) {
			p = p->parent;
		}
		//循环退出两种情况:找到前驱 / 回溯至nullptr无先驱
	}
	return p;
}

3. Get_NextNode 获取当前节点直接后继

实现思路: 断言校验节点非空;分两大分支处理,右子树 存在取右子树最小值;右子树 不存在则向上回溯父节点匹配后继规则。

代码实现

cpp 复制代码
BSTNode* Get_NextNode(BSTNode* node) {
	assert(node != nullptr);

	BSTNode* p = node;
	//当前节点右子树不为空,找右子树最小值
	if (node->rightchild != nullptr) {
		p = p->rightchild;
		while (p->leftchild != nullptr)
			p = p->leftchild;
	}
	//当前节点右子树为空,向上回溯父节点
	else {
		p = p->parent;
		while (p!= nullptr&&p->data<node->data) {
			p = p->parent;
		}
		//循环退出两种情况:找到后继 / 回溯至nullptr无后继
	}
	return p;
}

四、BST 五大基础操作

1. Init_BST 初始化二叉搜索树

实现思路: 断言校验树结构体指针合法;将根指针置空,代表空树。

代码实现

cpp 复制代码
BSTNode* Init_BST(BSTree* pTree) {
	assert(pTree != nullptr);
	pTree->root = nullptr;

	return pTree->root;
}

2. Insert_BST 插入节点

实现思路

  • 断言校验树指针,单独处理空树场景,直接新建节点作为根节点
  • 双指针遍历:p 遍历查找插入位置,pp 记录 p 的父节点
  • 遍历过程匹配有序规则:待插入值小于当前节点往左走,大于则往右走
  • 若循环找到等值节点,BST不允许重复值,直接返回
  • 创建新节点,根据大小关系挂载至父节点左 / 右分支,绑定父子指针

代码实现

cpp 复制代码
bool Insert_BST(BSTree* pTree, ELEMTYPE val) {
	assert(pTree != nullptr);
	//空树直接创建根节点
	if (pTree->root == nullptr) {
		BSTNode* pnewnode = BuyNode();
		pnewnode->data = val;

		pTree->root = pnewnode;
		pnewnode->parent = nullptr;
		return true;
	}
	//双指针遍历寻找插入位置
	BSTNode* p=pTree->root;
	BSTNode* pp=nullptr;
	while (p!=nullptr&&p->data!=val) {
		pp = p;
		if (p->data > val) 
			p = p->leftchild;
		else 
			p = p->rightchild;
	}
	//存在重复节点,插入失败
	if (p != nullptr && p->data == val)
		return false;

	BSTNode* pnewnode = BuyNode();
	pnewnode->data = val;
	//挂载到父节点对应分支,绑定父指针
	if (pnewnode->data>pp->data) 
		pp->rightchild = pnewnode;
	else
		pp->leftchild = pnewnode;
	pnewnode->parent = pp;

	return true;
}

3. Search_BST 循环查找节点

实现思路

  • 断言校验根节点非空,指针 p 从根节点开始遍历
  • 循环判定:当前指针不为空且关键字不匹配,按大小规则向左 / 右子树查找
  • 循环退出两种分支:p 为 nullptr 代表查找失败;p 不为 nullptr 代表找到目标节点

代码实现

cpp 复制代码
BSTNode* Search_BST(BSTNode* root,ELEMTYPE val) {
	assert(root!=nullptr);
	BSTNode* p = root;
	while (p!=nullptr&&p->data!=val) {
		if (val < p->data) {
			p = p->leftchild;
		}
		else {
			p = p->rightchild;
		}
	}
	return p;
}

4. Delete_BST 删除节点

(1)三类删除场景对比

  • 零孩子(叶子节点):直接断开父指针,释放当前节点,逻辑最简单
  • 单孩子(仅左 / 右子树):唯一子节点直接顶替当前节点位置,更新父指针
  • 双孩子(左右子树均存在):无法直接替换子树,采用狸猫换太子方案,转换为前两种场景

(2)狸猫换太子核心原理

直接前驱、直接后继节点的固有特性:二者最多只存在单侧子树,不存在同时拥有左右两个孩子的情况。将后继节点数据覆盖到待删节点,删除逻辑转移至后继节点,规避复杂双分支替换操作,大幅降低代码复杂度。

补充考点:前驱、后继均可替换,二者效果完全一致,工程中常用后继实现。

5. Show_InOrder_BST 中序遍历打印

实现思路: 利用栈模拟递归流程,BST 中序遍历输出严格升序 ;tag 标记控制左子树 持续入栈逻辑,弹出节点打印后处理右子树

代码实现

cpp 复制代码
void Show_InOrder_BST(BSTNode* root) {
	assert(root != nullptr);
	std::stack<BSTNode*> st;
	st.push(root);

	bool tag = true;
	while (!st.empty()) {
		while (tag&&st.top()->leftchild != nullptr) {
			st.push(st.top()->leftchild);
		}
		BSTNode* tmp = st.top();
		st.pop();
		printf("%d ", tmp->data);

		if (tmp->rightchild != nullptr) {
			st.push(tmp->rightchild);
			tag = true;
		}
		else {
			tag = false;
		}
	}
}

五、BST 删除逻辑专项解析

1. Delete_BST0:仅处理 0 个子节点

实现思路: 先查找目标节点,存在且无左右孩子;断开父节点对应分支指针,释放目标节点内存。

代码实现

cpp 复制代码
bool Delete_BST0(BSTree* pTree, ELEMTYPE val) {
	assert(pTree != nullptr);
	BSTNode* p = Search_BST(pTree->root, val);
	if (p == nullptr)
		return false;
	//仅处理叶子节点
	if (p->leftchild != nullptr || p->rightchild != nullptr)
		return false;
	
	BSTNode* father = p->parent;
	if (father == nullptr)
		pTree->root = nullptr;
	else {
		if (p->data<father->data)
			father->leftchild = nullptr;
		else
			father->rightchild = nullptr;
	}
	delete p;
	p = nullptr;
	return true;
}

2. Delete_BST1:处理 0/1 个子节点

实现思路: 查找目标节点,判定至多只有单侧子节点;取出唯一子节点,直接顶替待删节点位置,同步修改父指针后释放内存。

代码实现

cpp 复制代码
bool Delete_BST1(BSTree* pTree, ELEMTYPE val) {
	assert(pTree != nullptr);
	BSTNode* p = Search_BST(pTree->root, val);
	if (p == nullptr)
		return false;
	//存在两个子节点不处理
	if(p->leftchild != nullptr && p->rightchild != nullptr)
		return false;

	BSTNode* father = p->parent;
	//取出唯一存在的子节点
	BSTNode* child = p->leftchild != nullptr ? p->leftchild : p->rightchild;
	if (father == nullptr)
		pTree->root = child;
	else {
		if (p->data < father->data) {
			father->leftchild = child;
		}
		else {
			father->rightchild = child;
		}
	}

	delete p; 
	p = nullptr;
	return true;
}

3. Delete_BST:整合全场景删除

实现思路

  • 查找待删除节点,无节点直接返回
  • 场景 1:零 / 单分支直接复用单分支删除逻辑
  • 场景 2:存在左右双孩子,执行**狸猫换太子,**获取当前节点直接后继,用后继关键字覆盖待删节点数据
  • 将删除目标转移至后继节点,后继最多仅单分支,复用单分支删除逻辑完成释放

代码实现

cpp 复制代码
bool Delete_BST(BSTree* pTree, ELEMTYPE val) {
	assert(pTree != nullptr);
	BSTNode* p = Search_BST(pTree->root, val);
	if (p == nullptr)
		return false;
	//零分支直接释放
	if (p->leftchild == nullptr && p->rightchild == nullptr)
	{
		Delete_BST0(pTree, val);
		return true;
	}
	//双分支节点:狸猫换太子
	if (p->leftchild != nullptr && p->rightchild != nullptr) {
		BSTNode* cat = Get_NextNode(p);
		p->data = cat->data;
		p = cat;
	}
	//统一处理单分支/转换后的单分支节点
	BSTNode* father = p->parent;
	BSTNode* child = p->leftchild != nullptr ? p->leftchild : p->rightchild;
	if (father == nullptr)
		pTree->root = child;
	else {
		if (p->data < father->data) {
			father->leftchild = child;
		}
		else {
			father->rightchild = child;
		}
	}

	delete p;
	p = nullptr;
	return true;
}

六、总结

1. 结构本质区别

普通二叉树无数值约束 ,仅限制节点度≤2;BST 强制全局有序数值唯一,左小右大,是带排序规则的特殊二叉树。

2. 遍历特性区别

普通二叉树所有遍历无序BST 独有中序升序 特性,可快速完成有序输出,先 / 后序遍历不具备有序效果。

3. 性能与工程选型

理想平衡BST 增删查为 O(logn) ,斜树退化 O(n)三叉链表适配高频删改、取前驱后继场景,普通刷题、简单遍历优先用二叉链表,减少指针维护 bug。

| 对比维度 | 普通二叉树 | 二叉搜索树(BST) |
| 节点规则 | 仅限制度≤2,无数值大小约束 | 左小右大 、数值不重复、全局有序 |
| 中序遍历 | 序列无序 | 严格升序(标志性特征) |
| 查找效率 | O(n) | 平衡 O (logn)、斜树 O (n) |

核心用途 层级存储、遍历 排序、检索、动态增删
相关推荐
盐焗鹌鹑蛋2 小时前
【C++】继承
开发语言·c++
love530love2 小时前
用自然语言让 AI Agent 卸载软件 —— 以卸载 Visual Studio 2026 为例
ide·人工智能·windows·agent·visual studio·ai agent·marvis
Yang_jie_032 小时前
笔记:数据结构(顺序表)
数据结构·windows·笔记
会周易的程序员2 小时前
使用 pybind11 封装 C++ 日志库 microLog 为 Python 模块
java·c++·python·物联网·架构·日志·aiot
WZF-Sang2 小时前
网络基础——2
服务器·网络·c++·学习·网络编程·php
-西门吹雪4 小时前
现代C++ 并发编程-学习指南
java·jvm·c++
aaPIXa6226 小时前
C++ TensorRT Edge-LLM 边缘推理框架:从原理到实战
开发语言·c++·edge
一个初入编程的小白6 小时前
数据结构:栈
数据结构
我命由我123457 小时前
方差(实例实操、与标准差的区别)
java·数据结构·算法·数据分析·java-ee·intellij-idea·idea