最近在分析分析红黑树
时,感觉上来就挑最难的树结构之一进行分析,难度太大,所以特意写这篇二叉搜索树
分析的博客作为铺垫。那么为啥挑二叉搜索树
进行分析捏?其实红黑树
也是一种更为复杂的二叉搜索树
,建议阅读一下我的另外一篇博客 数据结构之二叉树、AVL树、红黑树、Trie树、B树、B+树、B*树浅析 。为了帮助大家理解红黑树
,先写这篇博客分析二叉搜索树
。下面将主要分析二叉搜索树
的查找、插入、删除三种操作,并且附上C++代码实现。
二叉搜索树详解目录
一、二叉搜索树
简述
二叉搜索树
大致定义为二叉树的左子树任意节点的值小于根节点的值,右子树任意节点的值大于根节点的值,并且左子树、右子树同样也符合二叉搜索树
的定义(递归定义)。中序遍历顺序为左根右
,所以二叉搜索树
的典型特征是中序遍历序列有序。
二、二叉搜索
树相关操作
为了能让大家更好的理解二叉搜索树
,将提供C++的编码实现。下面是TreeNode
结构体实现:
/**
* 这里额外添加了parent指针,主要是为了访问父节点方便
*/
struct TreeNode {
int value;
// 三个指针分别指向父节点、左子树根节点、右子树根节点
TreeNode *parent;
TreeNode *left;
TreeNode *right;
TreeNode(int value) : value(value), parent(NULL), left(NULL), right(NULL) {}
TreeNode(int value, TreeNode *parent, TreeNode *left, TreeNode *right) {
this->value = value;
this->parent = parent;
this->left = left;
this->right =right;
}
};
查找
、插入
、删除
节点三个操作的复杂度依次增加,如果觉得有点压力,请按照顺序依次阅读,稳扎稳打。
1、二叉搜索树
中节点的查找
二叉搜索树
设计成左 < 根 < 右
(中序遍历有序),一个很重要的动机就是快速查找。有过一点算法基础的应该能想到有一种搜索策略非常相似,没错就是二分搜索
,每次将target
与搜索区间(递增有序)的中间值mid
比较,如果target
< mid
则缩小搜索区间为[left, mid - 1]
,如果target
> mid
则缩小搜索区间为[mid + 1, right]
,否则target
== mid
。辣么再来看看二叉搜索树中查找的伪代码。
root 指向二叉搜索树的根,target为需要搜索的值
while (root != NULL) {
if root->value == target
成功搜索到了 return
else if root->value > target
// 注意二叉搜索树的特征:root的右子树比root->value都大,root的左子树比root->value都小
// 既然root->value > target,那么只可能出现在左子树,转移root到左子树
root = root->left
else
// 否则root->value < target
// 注意二叉搜索树的特征:root的右子树比root->value都大,root的左子树比root->value都小
// 既然root->value < target,那么只可能出现在右子树,转移root到右子树
root = root->right
}
// root == NULL,二叉搜索树中没有target
C++代码实现:
// 在二叉搜索树中查找target,存在返回对应的指针,否则返NULL
TreeNode *searchNode(TreeNode *root, int target){
while (root != NULL) {
if (root->value == target) {
break;
}
else if (root->value > target) {
// 注意二叉搜索树的特征:root的右子树比root->value都大,root的左子树比root->value都小
// 既然root->value > target,那么只可能出现在左子树,转移root到左子树
root = root->left;
}
else {
// 否则root->value < target
// 注意二叉搜索树的特征:root的右子树比root->value都大,root的左子树比root->value都小
// 既然root->value < target,那么只可能出现在右子树,转移root到右子树
root = root->right;
}
}
return root;
}
2、二叉搜索树
中节点的插入
二叉搜索树
中查找充分利用左 < 根 < 右
特性,辣么插入也能用上这个特性么?答案是显然的。
首先思考一下,我们插入节点后是不是还需要维持二叉树
仍然满足二叉搜索树
特性,这是必须的,要不能你的二叉搜索树
就变成一次性的了。辣么我们就先要找到它真实应该插入的位置,保证中序遍历为递增有序。下面是删除的伪代码。
root 指向二叉搜索树的根,value为需要插入的值
if root == NULL
// 二叉搜索树为空,插入的节点即是根节点
root = new TreeNode(value)
else
// 我们需要找到插入的位置
TreeNode *ptr = root;
while (true) {
if ptr->value == value
// 树中已经存在这个value,不进行插入(这里简化逻辑)
break
else if ptr->value > value
// 注意二叉搜索树的特征:ptr的右子树比ptr->value都大,ptr的左子树比ptr->value都小
// 既然ptr->value > value,value只能插入ptr左子树
if ptr->left == NULL
// 如果ptr左子树为空,则插入的节点正好做左子树的根
ptr->left = new TreeNode(value)
break
else
// 否则转移到左子树,继续查找
ptr = ptr->left
else
// 注意二叉搜索树的特征:ptr的右子树比ptr->value都大,ptr的左子树比ptr->value都小
// 既然ptr->value < value,value只能插入ptr右子树
if ptr->right == NULL
// 如果ptr右子树为空,则插入的节点正好做右子树的根
ptr->right = new TreeNode(value)
break
else
// 否则转移到右子树,继续查找
ptr = ptr->right
}
return root
C++代码实现:
// 在二叉搜索树中插入value,如果二叉树中已经存在则不进行插入(简化处理逻辑)
TreeNode *searchNode(TreeNode *root, int value) {
if (root == NULL) {
// 二叉搜索树为空,插入的节点即是根节点
root = new TreeNode(value);
}
else {
// 我们需要找到插入的位置
TreeNode *ptr = root;
while (true) {
if (ptr->value == value) {
// 树中已经存在这个value,不进行插入(这里简化逻辑)
break;
}
else if (ptr->value > value) {
// 注意二叉搜索树的特征:ptr的右子树比ptr->value都大,ptr的左子树比ptr->value都小
// 既然ptr->value > value,value只能插入ptr左子树
if (ptr->left == NULL) {
// 如果ptr左子树为空,则插入的节点正好做左子树的根
ptr->left = new TreeNode(value);
break;
}
else {
// 否则转移到左子树,继续查找
ptr = ptr->left;
}
}
else {
// 注意二叉搜索树的特征:ptr的右子树比ptr->value都大,ptr的左子树比ptr->value都小
// 既然ptr->value < value,value只能插入ptr右子树
if (ptr->right == NULL) {
// 如果ptr右子树为空,则插入的节点正好做右子树的根
ptr->right = new TreeNode(value);
break;
}
else {
// 否则转移到右子树,继续查找
ptr = ptr->right;
}
}
}
}
return root;
}
3、二叉搜索树
中节点的删除
在二叉搜索树
删除节点,首先我们应该在树中查找到这个节点的位置吧,然后将其移除,并且移除后我们需要进行调整,使其任然满足二叉搜索树
。这个删除操作可以分成好几种情况,需要分别讨论。
①、删除叶节点
删除叶节点
,只要将其移除即可,不需要进行任何调整操作。
②、删除非叶节点
删除非叶节点
可以细分为两种,第一种是删除有右子树的节点,删除节点后需要将右子树中序遍历第一个节点填充到删除节点A位置(为什么选右子树中序遍历第一个节点?因为整棵树的中序遍历序列中,节点A的下一个节点就是其右子树中序遍历的第一个节点)。
右子树中序遍历第一个节点为某个节点的左子节点,直接将最左的左子节点填补到已删除节点的位置。
a、删除节点有右子树
右子树中序遍历序列中的第一个节点为某个没有左子节点的节点B。将节点B填入已删除节点的位置,并且将节点B的右子树置于节点B的位置。
b、删除节点没有右子树
第二种是删除没有右子树的节点A,此时寻找整颗二叉树中序遍历中节点A的下一个节点,稍微复杂一点,需要利用parent指针。找到远祖父节点B,并且使得节点A在远祖父节点B的左子树中
!远祖父节点B.value替换到节点A.value后,它自己也需要它的下一个节点来填充。
其实根本不需要进行删除操作,只要寻找到这个节点中序遍历序列的下一个节点,然后直接替换即可。
C++代码实现:
// 在二叉搜索树中插入value,如果二叉树中已经存在则不进行插入(简化处理逻辑)
TreeNode *deleteNode(TreeNode *root, TreeNode *targetPtr) {
if (targetPtr == NULL || root == NULL) {
return root;
}
if (root == targetPtr && root->right == NULL) {
// 一、删除的是根节点,并且根节点没有右子树
// 处理:切断targetPtr与左子树的关联,返回左子树,释放targetPtr
root = root->left;
if (root != NULL) {
root->parent = NULL;
}
targetPtr->left = NULL;
delete targetPtr;
}
else if (targetPtr->left == targetPtr->right) {
// 二、删除的叶节点
// targetPtr->left == targetPtr->right,只能是同时为NULL
// 操作:切断parent与targetPtr的关联,返回root,释放targetPtr
if (targetPtr->parent->left == targetPtr) {
targetPtr->parent->left = NULL;
} else {
targetPtr->parent->right = NULL;
}
// 切断targetPtr 与targetPtr->parent的关联
targetPtr->parent = NULL;
delete targetPtr;
} else if (targetPtr->right == NULL) {
// 三、删除节点右子树为空 需要找到远祖父节点
TreeNode *pParent = targetPtr;
// 祖父节点B,并且使得节点targetPtr在远祖父节点B的左子树
while (pParent->parent != NULL && pParent->parent->right == pParent) {
pParent = pParent->parent;
}
if (pParent->parent == NULL) {
// 如果targetPtr不存在一个远祖父节点B,使得leftPtr在远祖父B的左子树
// 操作:只能删除这个节点,并且把左子树放到当前节点的位置
targetPtr->parent->right = targetPtr->left;
targetPtr->left->parent = targetPtr->parent;
// 切断targetPtr与parent、left的关系
targetPtr->parent = NULL;
targetPtr->left = NULL;
delete targetPtr;
}
else {
// 否则pParent->parent->value替换targetPtr->value,再删除pParent->parent(递归)
targetPtr->value = pParent->parent->value;
root = deleteNode(root, pParent->parent);
}
}
else {
// 四、删除节点存在右子树,直接在右子树寻找中序遍历的第一个节点
TreeNode * leftPtr = targetPtr->right;
// 一直往left寻找
while (leftPtr->left != NULL) {
leftPtr = leftPtr->left;
}
// 将leftPtr->value替换到targetPtr->value
targetPtr->value = leftPtr->value;
// targetPtr->right就是leftPtr
if (targetPtr->right == leftPtr) {
// 将targetPtr->right指向leftPtr右子树
targetPtr->right = leftPtr->right;
// 如果targetPtr->right != NULL,还需要设置parent
if (leftPtr->right != NULL) {
leftPtr->right->parent = targetPtr;
}
}
else {
// 否则leftPtr->parent与leftPtr切断关系
leftPtr->parent->left = NULL;
}
// 将leftPtr于其parent切断关系并释放
leftPtr->parent = NULL;
delete leftPtr;
}
return root;
}
TreeNode *deleteNodeByValue(TreeNode *root, int value) {
// 首先查找value所在的位置
TreeNode *targetPtr = searchNode(root, value);
if (targetPtr == NULL) {
// value都没找到还删除啥...
return root;
}
return deleteNode(root, targetPtr);
}
三、思考与总结
可以看出二叉搜索树
的查找、插入还是比较简单的,删除稍微复杂一点。不过二叉搜索树
可能存在退化成链表的缺陷,比如给你一个本来递增有序的序列让你插入到一颗空二叉搜索树中,这时就退化成链表了。
因此我们需要将二叉搜索树
增加平衡
的特性,即AVL树
或红黑树
。后面有时间会更新一篇红黑树
的博客,敬请期待~