红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。
BST
二叉查找树(Binary Search Tree,简称BST)是一棵二叉树,它的 左子节点的值比父节点的值要小,右节点的值要比父节点的值大。它的 高度决定了它的 查找效率。
在理想的情况下,二叉查找树增删查改的时间复杂度为O(logN)(其中N为节点数),最坏的情况下为O(N)。当它的高度为logN+1
时,我们就说二叉查找树是平衡的。
- BST的查找操作
T key = a search key
Node root = point to the root of a BST
while(true){
if(root==null){
break;
}
if(root.value.equals(key)){
return root;
}
else if(key.compareTo(root.value)<0){
root = root.left;
}
else{
root = root.right;
}
}
return null;
从程序中可以看出,当BST查找的时候,先与当前节点进行比较:
- 如果相等的话就返回当前节点;
- 如果少于当前节点则继续查找当前节点的左节点;
- 如果大于当前节点则继续查找当前节点的右节点。
直到当前节点指针为空或者查找到对应的节点,程序查找结束。
- BST的插入操作
Node node = create a new node with specify value
Node root = point the root node of a BST
Node parent = null;
//find the parent node to append the new node
while(true){
if(root==null)break;
parent = root;
if(node.value.compareTo(root.value)<=0){
root = root.left;
}else{
root = root.right;
}
}
if(parent!=null){
if(node.value.compareTo(parent.value)<=0){//append to left
parent.left = node;
}else{//append to right
parent.right = node;
}
}
插入操作先通过循环查找到待插入的节点的父节点,和查找父节点的逻辑一样,都是比大小,小的往左,大的往右。找到父节点后,对比父节点,小的就插入到父节点的左节点,大就插入到父节点的右节点上。
- BST的删除操作
删除操作的步骤如下:
1.查找到要删除的节点。
2. 如果待删除的节点是叶子节点,则直接删除。
3.如果待删除的节点不是叶子节点,则先找到待删除节点的中序遍历的后继节点,用该后继节点的值替换待删除的节点的值,然后删除后继节点。
(此处所参考的资料描述有问题,并没有这么简单,在这里我们仅作为借鉴,并对次进行详细补充)
- 叶子节点
- 只有左子树/只有右子树的节点
- 左右子树都存在的节点
对于这三种的处理情况:
case 1:叶子节点直接将节点删除(不影响树的结构)
case 2:要删除的结点用一个指针进行保存,而自己跳到自己的左/右子树上,即parent牺牲自己抱住了lchild/rchild(孩子在路中央呆若木鸡,眼看着大卡车迎面而来,父亲眼疾手快将孩子推向一边,而让冰冷的钢铁撞击在自己炙热的身躯上,代替了孩子的牺牲)
case3:见后面的具体分析
树的结构定义:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
首先我们给出解BST问题的一个代码框架,我们定义这里删除BST中某个结点的函数为deleteNode(TreeNode* root, int key),其中TreeNode* root为已经构造好了的一棵符合规范的BST,而int key则是我们即将要删除的目标
TreeNode* deleteNode(TreeNode* root, int key) {
if ( root == nullptr )
return root;
if ( key == root->val ) {
// 定位到目标进行删除操作...
}
// 若key小于当前定位到的root->val,则说明root->val应该继续往左走才有机会定位到key
else if ( key < root->val )
root->left = deleteNode(root->left, key);
// 若key大于当前定位到的root->val,则说明root->val应该往右走才有机会定位到当前key
else
root->right = deleteNode(root->right, key);
}
case1 叶子结点:
直接进行删除(直接使用delete删除其实是不严谨,这里我们忽略删除的细节,具体讲解整体思路部分)
if ( key == root->val ) {
// 定位到目标进行删除操作...
// case1 : 叶子结点的删除操作
if ( root->left == nullptr && root->right == nullptr )
delete root;
}
case2 只有左子树/只有右子树的结点
保存这个被删除的结点,把孩子推向一边,有左子树就把孩子推到左边,有右子树就把孩子推到右边
TreeNode* p;
TreeNode* q;
if ( key == root->val ) {
// 定位到目标进行删除操作...
// case1 : 叶子结点的删除操作
if ( root->left == nullptr && root->right == nullptr )
delete root;
// case2 : 只有左子树/只有右子树的结点
// 重接左子树
if ( root->right == nullptr ) {
p = root;
root = root->left;
delete p;
}
// 重接右子树
else if ( root->left == nullptr ) {
p = root;
root = root->right;
delete p;
}
}
case3 左右子树都存在的节点
在开始讲解最关键的这一部分之前,首先介绍一个BST中直接前驱/直接后继的概念:
如果不能理解这个概念,那么我给你一个建议:找一棵普通的树,完完整整的结合中序遍历的代码过一遍遍历的顺序,好好的去理解中序遍历回溯的时机。 在理解了上述概念后,直接后继留给你们去推导。 其次需要注意的是,这里我们所说的直接前驱/后继是数组进行中序遍历时遍历到的当前结点的上一个元素! 好,接着往下讲,我们又知道,BST的中序遍历结果是一个升序数组,但是结合BST的这点性质,可以得出一个结论:BST中某个结点的前驱/后继结点 就是 BST中序遍历得到的已排序数组中前一个/后一个元素。 还是看上面的例子,5的直接前驱结点是4,也是中序遍历得到的排序数组中5的前一个元素,这不是巧合!
不难发现:我们的直接前驱/后继结点所属的情况一定属于case1和case2,即一定为叶子结点or只有左/右子树的结点。假设我们将要删除的结点伪装成其直接前驱/后继结点,再将直接前驱/后继结点删除,神不知鬼不觉的“算法版狸猫换太子”岂不美哉?好,现在我们已经清晰的转化了问题,我们来开始写代码:
TreeNode* p;
TreeNode* q;
if ( key == root->val ) {
// 定位到目标进行删除操作...
// case1 : 叶子结点的删除操作
if ( root->left == nullptr && root->right == nullptr )
delete root;
// case2 : 只有左子树/只有右子树的结点
// 重接左子树
if ( root->right == nullptr ) {
p = root;
root = root->left;
delete p;
}
// 重接右子树
else if ( root->left == nullptr ) {
p = root;
root = root->right;
delete p;
}
else {
// case3 : 左子树右子树都存在的结点
// 我们首先按伪装成前驱结点来写代码,找到当前结点root的左结点的最右边的结点
q = root->left;
while ( q->right ) {
q = q->right;
}
// 狸猫换太子
root->val = q->val;
// ...
}
}
不知道你是否敏感,我们在狸猫换太子之后出现了问题。正如我上面所说,直接前驱结点也是分两种情况的:i.一种是当前的结点的左结点的最右边的结点 ii.那么如果出现当前结点的左结点没有右子树,则当前结点的左结点就是当前结点的前驱结点我们如何得知这里的前驱结点属于哪一种情况的呢?
这里又不得不提一个链表问题中常用的思想:用一个指针pre记录之前遍历过的位置。 我们不难发现,假设我们用一个指针p记录指针q上一次遍历到的结点,那么当我对应情况 i时,我的p结点只要继承q结点的“遗产”就好了(下面会具体讲遗产是什么),而对应到情况 ii 的时候,q刚到达当前结点的左结点,p根本就没有挪动的必要。不难看出,我们可以用一个指针p在不同情况会处于不同的位置上来区分两种情况。
TreeNode* p;
TreeNode* q;
if ( key == root->val ) {
// 定位到目标进行删除操作...
// case1 : 叶子结点的删除操作
if ( root->left == nullptr && root->right == nullptr )
delete root;
// case2 : 只有左子树/只有右子树的结点
// 重接左子树
if ( root->right == nullptr ) {
p = root;
root = root->left;
delete p;
}
// 重接右子树
else if ( root->left == nullptr ) {
p = root;
root = root->right;
delete p;
}
else {
// case3 : 左子树右子树都存在的结点
// 我们首先按伪装成前驱结点来写代码,找到当前结点root的左结点的最右边的结点
// p指向当前结点,q指向当前结点的左结点
p = root;
q = root->left;
// 假设q有右子树就不断往右推进,p随之跟进,而假设q没有右子树,p和q都滞留在原地
while ( q->right ) {
p = q;
q = q->right;
}
// 狸猫换太子,不难理解,我们即将要删除的并不是root,而是q
root->val = q->val;
// 情况i:直接前驱为当前的结点的左结点的最右边的结点
if ( p != root )
// 直接前驱结点是一定不会存在右子树这一说了,已经推到了最右边,所以左子树是他留给p的遗产(当然如果q是个叶子结点的就是一个穷光蛋,什么都不剩),接到p的右子树上
p->right = q->left;
// 情况ii:当前结点的左结点就是当前结点的前驱结点,将遗产接到p的左子树上
else
p->left = q->left;
delete q;
}
}
别忘记之前我们所讲的模版,这一部分只是属于if ( key == root->val )这个代码段的,下面附上完整代码,我在基准情形那一块做了点优化,即当树中仅有一个结点且这个结点的val值就是序要删除的值的时候直接返回nullptr:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
TreeNode* p;
TreeNode* q;
if ( root == nullptr || root->val == key && root->left == nullptr && root->right == nullptr )
return nullptr;
else {
if ( key == root->val ) {
if ( root->left == nullptr && root->right == nullptr )
delete root;
if ( root->right == nullptr ) {
p = root;
root = root->left;
delete p;
}
else if ( root->left == nullptr ) {
p = root;
root = root->right;
delete p;
}
else {
p = root;
q = root->left;
while ( q->right ) {
p = q;
q = q->right;
}
root->val = q->val;
if ( p != root )
p->right = q->left;
else
p->left = q->left;
delete q;
}
}
else if ( key < root->val )
root->left = deleteNode(root->left, key);
else
root->right = deleteNode(root->right, key);
return root;
}
}
};
- BST的存在的问题
BST存在的主要问题是,数在插入的时候会导致树倾斜,不同的插入顺序会导致树的高度不一样,而树的高度直接的影响了树的查找效率。理想的高度是logN,最坏的情况是所有的节点都在一条斜线上,这样的树的高度为N。
学习参考
1.BST删除操作