二叉搜索树 - C++ 实现
二叉搜索树 - C++ 实现
概述 Overview
二叉查找树(英语:Binary Search Tree, 后文中简称 BST), 也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree), 是在 20 世纪 60 年代为解决标记数据的高效存储问题而设计的, 由 Conway Berners-Lee 和 David Wheeler 发明.
具体指的是一棵空树或者具有下列性质的二叉树:
- 若任意节点的左子树不空, 则左子树上所有节点的值均小于它的根节点的值;
- 若任意节点的右子树不空, 则右子树上所有节点的值均大于它的根节点的值;
- 任意节点的左、右子树也分别为二叉查找树;
简单来说, 二叉搜索树中的每一个节点都满足: 左子树中的所有元素均小于该节点元素; 右子树中的所有元素均大于该节点元素. \(左子树元素 \le 本节点元素 \le 右子树元素\). 左小右大.
这种结构上的设计使得 BST 可以以二分查找思路实现 \(\Omega(\log n)\) (不是大o, 而是下限在logn)级别的快速增, 删, 查等操作, 因为在树中的每一步操作都能排除一半的元素. 完成一颗二叉搜索树的建立之后, 我们还可以以中序遍历的方式得到排序后的序列, 这也是二叉搜索树被称为排序二叉树的原因.
需要注意的是, 二叉搜索树的效率与在建立时输入的元素顺序有很大的关系. 在最坏情况下, 二叉搜索树会退化成链表. 树层数大大增加使得查找等操作需要消耗更多的时间)此时, 需要对树进行额外的优化 - 平衡, 来保证高效的运行效率. 在本文中我们不作讨论, 本文仅介绍朴素的二叉搜索树.
如果看完之后还是不太理解的话, 可以看看这个美国旧金山大学CS做的一个算法可视化. Binary Search Tree Visualization (usfca.edu) 在这个网站上详细地看到 BST 每一步的操作. 做的很好, 不妨去玩玩!
基本操作 Operations
和其他的数据结构类似, 二叉搜索树也有着这样的几个基本操作.
搜索 Search
️ 根据元素值直接搜索节点
假设要搜素的节点的元素值为 item.
那么搜索就是要从数的根节点 root 开始, 逐节点遍历该树.
若当前扫描到的节点的元素值小于 item, 则往右走; 若元素值大于 item 则左走. 直到扫描到目标节点或遇到 nil 节点时(未找到该元素)停止搜索, 返回结果.
具体的代码实现如下.
void insert(const int item)
{
TreeNode *scan = root;
TreeNode *prev = root;
while (scan != nullptr)
{
prev = scan;
scan = item < scan->data ? scan->left : scan->right;
}
TreeNode *newNode = new TreeNode(item);
if (root)
{
(item < prev->data ? prev->left : prev->right) = newNode;
newNode->parent = prev;
}
else
{
root = newNode;
}
return;
}
️ 找节点的前驱/后继
前驱节点指的是树在中序遍历的序列中, 目标结点的前一个结点(类似链表中前驱的定义).(当目标结点为第一个结点是返回nil)
e.g. 如一颗树的中序遍历序列为: {1
, 2
, 3
, 4
, 5
}, 那么元素值为 2
的节点的前驱就是元素值为 1
的节点; 元素值为 1
的节点的前驱就是 nil
.
在BST中, 因为BST 的定义, 其中序遍历序列就是树中所有元素按元素值大小排序而输出的序列.
因此, 前驱节点也可以被定义成所有小于目标元素的所有元素中的最大值 \(\max( \{item\ |\ item \in tree \and item < target\})\).
根据此定义我们来完成搜索前驱节点的算法.
由 BST 的定义我们可以得到一个结论,
设有一个节点 node, 在中序遍历的序列中:
如果 node 是父节点的左节点
左子树中的所有节点 < node < 父节点
如果 node 是父节点的右节点
父节点 < node < 左子树中的所有节点
(光这样子讲可能有点抽象, 想象一下把一颗 BST 拍扁, 得到的序列就是中序遍历的序列, 容易得到以上结论)
基于上面的结论, 请考虑以下这两种 case.
case 01/ 有左子树. 所有有可能的 node 的均在左子树
那就意味着, 我们只需要找到 node 左子树中最大的那个节点即可.
case 02/ 无左子树. 需要一直向祖先追溯. 直到找到第一个比 node 小的节点.
第一个找到的节点就是在中序遍历中最接近 node 节点的节点. 也就是 node 的前驱结点.
若找不到, 就说明 node 就是中序遍历中最小的元素, 返回 nil 节点.
具体代码实现如下.
TreeNode *p_pred(int item)
{
TreeNode *itemNode = p_find(item);
if (!itemNode)
return nullptr;
if (itemNode->left)
{
return p_max(itemNode->left);
}
else
{
TreeNode *scan = itemNode->parent;
while (scan && scan->left == itemNode)
{
scan = scan->parent;
itemNode = itemNode->parent;
}
return scan;
}
}
while 退出时, 变量满足: scan == nullptr.(无前驱) 或 node != prev.left ( 等价于 node->data < item) (有前驱)
对应上文提到的两种 case.
查找后继同理, 代码也大差不差. 这里不再赘述. 直接给出代码.
TreeNode *p_succ(int item)
{
TreeNode *itemNode = p_find(item);
if (!itemNode)
return nullptr;
if (itemNode->right)
{
return p_min(itemNode->right);
}
else
{
TreeNode *scan = itemNode->parent;
while (scan && scan->right == itemNode)
{
scan = scan->parent;
itemNode = itemNode->parent;
}
return scan;
}
}
️ 插入 Insertion
本文给出的是迭代的实现方法, 递归的方法应该很好想, 依据 BST 的定义来写就好了, 这里就省略不写了(偷懒 :p)
依据二叉搜索树的定义查找.
待插入的元素比当前扫描到的元素小就继续扫描左子树, 反之则继续扫描右子树, 直到扫描到nil节点就插入待插入的元素.
给出代码如下.
注意树为空的时候需要特殊处理哈.
void insert(const int item)
{
TreeNode *scan = root;
TreeNode *prev = root;
while (scan != nullptr)
{
prev = scan;
scan = item < scan->data ? scan->left : scan->right;
}
TreeNode *newNode = new TreeNode(item);
if (root)
{
(item < prev->data ? prev->left : prev->right) = newNode;
newNode->parent = prev;
}
else
{
root = newNode;
}
return;
}
删除 Deletion / Removal
删除的情况就相对比较多比较复杂了. BST 的删除需要考虑以下三种情况:
左右子树皆空.
删除节点是叶节点, 将对应的节点置为 nil.
左右子树中只有其中一个非空.
将节点的非空子树重新链接到节点的父节点即可.
左右子树均非空.
这种情况是最棘手的情况. 我们做详细讨论.
情况三很麻烦.
如果也像前两种情况直接删除掉节点, 会出现两个无父节点的子树, 和原节点对应的父节点. 如果这个父节点原本还有两个子树的话, 那就意味着我们要面对三个子树, 和两个待链接的指针, 就必须要合并其中两个子树, 这会使问题会变得相当困难.
我们能不能把第三种情况也转换成前两种情况呢?
我们再仔细揣摩一下前驱和后继的定义.
也许你已经发现了这个性质, BST 中某个节点的前驱和后继一定是满足 case1 或 case2 的. (可以用反证法和前驱后继定义证明. 如果是case3 那么他就不是前驱或后继, 因为还有比该节点更大/小的节点, 不符合定义中的"最")
要是不可以直接删除, 可以做到用替换节点做到等效的删除吗? 怎样替换才可以保持 BST 的性质呢?
保持 BST 结构上的性质 也可以说是 使树的中序遍历序列结果不变.
我们想到可以用这个节点的前驱或后继来替换掉这个待删除节点. (在后文代码中使用后继, 两个方案都可以实现, 留给读者自行思考~)
因为待删除节点是不重要的, 替换之后中序遍历序列是不变的! 此后, 只需要替换之后将多出来的一个前驱/后继删除掉即可.
也就是说, 如此操作之后, 我们就将删除 case3 (待删除节点)的情况转换为删除 case1/2 (删除多余前驱/后继)的情况了!
具体地说, 用上之前找后继的代码. 用后继节点元素值替换待删除节点的元素值. 然后删掉多余的节点.
代码见下.
这个是直接使用指针的版本, 会啰嗦一点, 因为还需要根据 delNode 节点反推这个节点的指针(二级指针). 如果用上引用的话会代码简洁不少.
public:
void remove(const int item)
{
TreeNode *delNode = p_find(item);
if (delNode)
p_remove(delNode);
}
private:
void p_remove(TreeNode *delNode)
{
if (delNode->isLeaf()) // case1
{
if (delNode->parent)
{
// judge which pointer of node to modify
if (delNode == delNode->parent->left)
{
delNode->parent->left = nullptr;
}
else
{
delNode->parent->right = nullptr;
}
}
else // when root == delNode;
{
root = nullptr;
}
delete delNode;
}
else if (delNode->left == nullptr) // case 2
{
// basically same as above
if (delNode->parent)
{
if (delNode == delNode->parent->left)
{
delNode->parent->left = delNode->right;
}
else
{
delNode->parent->right = delNode->right;
}
}
else // root == delNode;
{
root = delNode->right;
root->parent = nullptr;
}
delete delNode;
}
else if (delNode->right == nullptr)
{
// same as above
if (delNode->parent)
{
if (delNode == delNode->parent->left)
{
delNode->parent->left = delNode->left;
}
else
{
delNode->parent->right = delNode->left;
}
}
else // root == delNode;
{
root = delNode->left;
root->parent = nullptr;
}
delete delNode;
}
else
{
// case3
TreeNode *succNode = p_succ(delNode);
delNode->data = succNode->data;
p_remove(succNode);
}
}
这个是引用的版本.
public:
void remove_ref(const int item)
{
TreeNode *&delNode = p_find_ref(item);
if (delNode)
p_remove_ref(delNode);
}
private:
void p_remove_ref(TreeNode *&delNodeRef)
{
TreeNode *delPtr = delNodeRef;
if (delNodeRef->isLeaf())
{
delNodeRef = nullptr;
}
else if (!delNodeRef->left || !delNodeRef->right)
{
TreeNode *subTree = max(delNodeRef->right, delNodeRef->left);
if (root == delNodeRef)
subTree->parent = nullptr;
delNodeRef = subTree;
}
else
{
TreeNode *succNode = p_succ(delNodeRef);
delNodeRef->data = succNode->data;
p_remove(succNode);
}
delete delPtr;
}
如果还是不太理解的话, 可以看看这个美国旧金山大学 CS 做的一个算法可视化. Binary Search Tree Visualization (usfca.edu) 在这个网站上详细地看到 BST 中每一步的操作. 做的很好, 不妨去玩玩!
代码实现 Implement
下面的是 BST 类的完整实现.
class TreeNode
{
public:
TreeNode(int m_data) : data(m_data) {}
int data;
TreeNode *left = nullptr;
TreeNode *right = nullptr;
TreeNode *parent = nullptr;
bool isLeaf()
{
return left == nullptr && right == nullptr;
}
};
class BinarySearchTree
{
public:
void insert(const int item)
{
TreeNode *scan = root;
TreeNode *prev = root;
while (scan != nullptr)
{
prev = scan;
scan = item < scan->data ? scan->left : scan->right;
}
TreeNode *newNode = new TreeNode(item);
if (root)
{
(item < prev->data ? prev->left : prev->right) = newNode;
newNode->parent = prev;
}
else
{
root = newNode;
}
return;
}
void remove(const int item)
{
TreeNode *delNode = p_find(item);
if (delNode)
p_remove(delNode);
}
void remove_ref(const int item)
{
TreeNode *&delNode = p_find_ref(item);
if (delNode)
p_remove_ref(delNode);
}
int find(const int item)
{
TreeNode *node = p_find(item);
return node ? node->data : -1;
}
int pred(const int item)
{
TreeNode *node = p_pred(item);
return node ? node->data : -1;
}
int succ(const int item)
{
TreeNode *node = p_succ(item);
return node ? node->data : -1;
}
void print()
{
p_printAll(root);
putchar('\n');
}
private:
TreeNode *root = nullptr;
const TreeNode *empty = nullptr;
void p_printAll(TreeNode *node)
{
if (!node)
return;
p_printAll(node->left);
printf("%d ", node->data);
p_printAll(node->right);
}
void p_remove(TreeNode *delNode)
{
if (delNode->isLeaf()) // case1
{
if (delNode->parent)
{
// judge which pointer of node to modify
if (delNode == delNode->parent->left)
{
delNode->parent->left = nullptr;
}
else
{
delNode->parent->right = nullptr;
}
}
else // when root == delNode;
{
root = nullptr;
}
delete delNode;
}
else if (delNode->left == nullptr) // case 2
{
// basically same as above
if (delNode->parent)
{
if (delNode == delNode->parent->left)
{
delNode->parent->left = delNode->right;
}
else
{
delNode->parent->right = delNode->right;
}
}
else // root == delNode;
{
root = delNode->right;
root->parent = nullptr;
}
delete delNode;
}
else if (delNode->right == nullptr)
{
// same as above
if (delNode->parent)
{
if (delNode == delNode->parent->left)
{
delNode->parent->left = delNode->left;
}
else
{
delNode->parent->right = delNode->left;
}
}
else // root == delNode;
{
root = delNode->left;
root->parent = nullptr;
}
delete delNode;
}
else
{
// case3
TreeNode *succNode = p_succ(delNode);
delNode->data = succNode->data;
p_remove(succNode);
}
}
void p_remove_ref(TreeNode *&delNodeRef)
{
TreeNode *delPtr = delNodeRef;
if (delNodeRef->isLeaf())
{
delNodeRef = nullptr;
}
else if (!delNodeRef->left || !delNodeRef->right)
{
TreeNode *subTree = max(delNodeRef->right, delNodeRef->left);
if (root == delNodeRef)
subTree->parent = nullptr;
delNodeRef = subTree;
}
else
{
TreeNode *succNode = p_succ(delNodeRef);
delNodeRef->data = succNode->data;
p_remove(succNode);
}
delete delPtr;
}
TreeNode *p_find(const int item)
{
TreeNode *scan = root;
while (scan != nullptr && scan->data != item)
{
scan = item < scan->data ? scan->left : scan->right;
}
return scan;
}
TreeNode *&p_find_ref(const int item)
{
TreeNode *scan = root;
TreeNode *prev = nullptr;
while (scan != nullptr && scan->data != item)
{
prev = scan;
scan = item < scan->data ? scan->left : scan->right;
}
if (root->data == item) return root;
if (!scan) return (TreeNode *&) empty;
return prev->left == scan ? (*prev).left : (*prev).right;
}
TreeNode *p_pred(int item)
{
TreeNode *itemNode = p_find(item);
if (!itemNode)
return nullptr;
if (itemNode->left)
{
return p_max(itemNode->left);
}
else
{
TreeNode *scan = itemNode->parent;
while (scan && scan->left == itemNode)
{
scan = scan->parent;
itemNode = itemNode->parent;
}
return scan;
}
}
TreeNode *p_succ(int item)
{
TreeNode *itemNode = p_find(item);
if (!itemNode)
return nullptr;
if (itemNode->right)
{
return p_min(itemNode->right);
}
else
{
TreeNode *scan = itemNode->parent;
while (scan && scan->right == itemNode)
{
scan = scan->parent;
itemNode = itemNode->parent;
}
return scan;
}
}
TreeNode *p_succ(TreeNode *itemNode)
{
if (itemNode->right)
{
return p_min(itemNode->right);
}
else
{
TreeNode *scan = itemNode->parent;
while (scan && scan->right == itemNode)
{
scan = scan->parent;
itemNode = itemNode->parent;
}
return scan;
}
}
TreeNode *p_max(TreeNode *node)
{
if (!node)
return nullptr;
while (node->right)
{
node = node->right;
}
return node;
}
TreeNode *p_min(TreeNode *node)
{
if (!node)
return nullptr;
while (node->left)
{
node = node->left;
}
return node;
}
};
结构分析 Analysis
总结上文提到的所有操作, 对 BST 的每一步操作都是向树的深处再进一层, 而每一次进入一颗子树都意味着筛选掉了当前树中的一半节点, 对剩下来的一半节点继续操作.(二分思想)
由此, 我们可以容易的的得到一个结论: BST 中的最大查找次数取决于 BST 的最大深度, 即树高. 在最好情况下, 一个有着 n 个节点的树, 他的深度最小为 \(\log_2 n\). 此时, BST 可以达到最高效率.
但如果不是最好的情况呢? 很多时候 BST 生成的树不会是理想的, 常常会有一些不满的子树, 产生额外的深度. 此时, BST 就会退化成更低级的数据结构. 也就是说, 这个数据结构的时间复杂度下限为 \(\Omega(\log n)\), 而其上限仍然为 O(n), 与数组和链表差不多. (甚至插入删除还不如链表的 O(1).)
具体来说, 实际上, BST 的结构与插入元素的顺序有着很大的关系. 举个例子, 同一组数列的不同排列可以形成不同的树. 虽然他们序列中元素都是相同的, 但是生成的树的结构却不尽相同. 当插入元素的顺序已然有序的时候. 根据在基本操作中插入操作的实现. 此时, 在形成的树中, 元素只会在树的一侧堆积. 也就是说, 这个时候 BST 会退化成一个单链表. 使得操作效率大大降低.
那么怎么才能让 BST 的深度尽量的小呢? 具体地说, 是否有解决方案可以让 BST 保证每次的插入都可以调整成最优的结构, 不再退化成链表?
是有的! 平衡树的提出解决了 BST 的退化问题. 通过保证树中每一个节点的左右子树高度尽可能相同. 可以使整颗树的深度近似等于前文提到的最小深度 \(\log_2 n\).
本文中不再作详细讨论, 感兴趣可以自行搜索相关信息.
️ 参考资料 Reference
Wikipedia - 二叉搜索树 - 维基百科, 自由的百科全书 (wikipedia.org)
Wikipedia - Binary search tree - Wikipedia
OI-Wiki - AVL 树 - OI Wiki (oi-wiki.org)
二叉搜索树 - C++ 实现的更多相关文章
- [数据结构]——二叉树(Binary Tree)、二叉搜索树(Binary Search Tree)及其衍生算法
二叉树(Binary Tree)是最简单的树形数据结构,然而却十分精妙.其衍生出各种算法,以致于占据了数据结构的半壁江山.STL中大名顶顶的关联容器--集合(set).映射(map)便是使用二叉树实现 ...
- [LeetCode] Serialize and Deserialize BST 二叉搜索树的序列化和去序列化
Serialization is the process of converting a data structure or object into a sequence of bits so tha ...
- [LeetCode] Verify Preorder Sequence in Binary Search Tree 验证二叉搜索树的先序序列
Given an array of numbers, verify whether it is the correct preorder traversal sequence of a binary ...
- [LeetCode] Lowest Common Ancestor of a Binary Search Tree 二叉搜索树的最小共同父节点
Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BS ...
- [LeetCode] Binary Search Tree Iterator 二叉搜索树迭代器
Implement an iterator over a binary search tree (BST). Your iterator will be initialized with the ro ...
- [LeetCode] Convert Sorted List to Binary Search Tree 将有序链表转为二叉搜索树
Given a singly linked list where elements are sorted in ascending order, convert it to a height bala ...
- [LeetCode] Convert Sorted Array to Binary Search Tree 将有序数组转为二叉搜索树
Given an array where elements are sorted in ascending order, convert it to a height balanced BST. 这道 ...
- [LeetCode] Recover Binary Search Tree 复原二叉搜索树
Two elements of a binary search tree (BST) are swapped by mistake. Recover the tree without changing ...
- [LeetCode] Validate Binary Search Tree 验证二叉搜索树
Given a binary tree, determine if it is a valid binary search tree (BST). Assume a BST is defined as ...
- [LeetCode] Unique Binary Search Trees 独一无二的二叉搜索树
Given n, how many structurally unique BST's (binary search trees) that store values 1...n? For examp ...
随机推荐
- [网鼎杯 2018]Comment-1|SQL注入|二次注入
1.打开之后只有一个留言页面,很自然的就想到了二次注入得问题,顺带查看了下源代码信息,并没有什么提示,显示界面如下: 2.那先扫描一下目录,同时随便留言一个测试以下,但是显示需要登录,账户.密码给出了 ...
- [CF1527D] MEX Tree (lca)
题面 给你一棵 n n n 个结点的树,对于所有的 k ∈ [ 0 , n ] k\in[0,n] k∈[0,n] ,求出 M E X = k {\rm MEX}=k MEX=k 的路径数量. 一条路 ...
- Java开发学习(二十八)----拦截器(Interceptor)详细解析
一.拦截器概念 讲解拦截器的概念之前,我们先看一张图: (1)浏览器发送一个请求会先到Tomcat的web服务器 (2)Tomcat服务器接收到请求以后,会去判断请求的是静态资源还是动态资源 (3)如 ...
- SpringCache的基本使用
SpringCache SpringCache是一个框架,实现了基于注解的缓存功能.SpringCache提供了一层抽象,底层可以切换不同的cache实现.具体是通过CacheManager接口来统一 ...
- 【读书笔记】C#高级编程 第二十五章 事务处理
(一)简介 事务的主要特征是,任务要么全部完成,要么都不完成. (二)概述 事务由事务管理器来管理和协调.每个影响事务结果的资源都由一个资源管理器来管理.事务管理器与资源管理器通信,以定义事务的结果. ...
- Maven 过滤问题
<build> <resources> <resource> <directory>src/main/resources</directory&g ...
- Markdown学习 .md学习
# Markdown学习## 标题## 二级标题### 三级标题#### 四级标题## 字体**两个*是粗体***一个是斜体****三个是斜体加粗***~~两个~是删除线~~## 引用>走向人生 ...
- 使用Pipeline抽象业务生命周期流程
上篇关于流程引擎的文章还是快两年以前的<微服务业务生命周期流程管控引擎>,这中间各种低代码平台层出不穷,虽然有些仅仅是OA+表单的再度包装,但有些的确是在逻辑和操作单元层面进行了真正的高度 ...
- HashMap不安全后果及ConcurrentHashMap线程安全原理
Java集合HashMap不安全后果及ConcurrentHashMap 原理 目录 HashMap JDK7 HashMap链表循环造成死循环 HashMap数据丢失 JDK7 Concurrent ...
- P1829 [国家集训队]Crash的数字表格
P1829 [国家集训队]Crash的数字表格 原题传送门 前置芝士 莫比乌斯反演 乘法逆元 数论分块 正文 //补充:以下式子中的除法均为整除 由题目可以得知,这道题让我们所求的数,用一个式子来表达 ...