红黑树与关联式容器

1.红黑树

相比于 AVL 树而言,红黑树并不是一颗平衡树,其节点的左、右子树高度差,长的不超过短的 2 倍。

红黑树的特点如下:

  1. 树的每一个节点都有一个颜色;
  2. 空节点是黑色;
  3. 根节点是黑色;
  4. 不能出现连续的红色节点;
  5. 从根节点到每一个叶子节点的路径上,黑色节点的数量是相同的;

如下图所示,为一颗典型的红黑树:

红黑树与关联式容器

1.1 插入操作

红黑树的插入过程与 BST 树的插入过程基本类型,不同的地方在于,红黑树插入新的节点后,需要进行调整,以满足红黑树的性质。

根据红黑树第五条性质,即从根节点到空叶节点的所有路径上的黑色节点的数目相同,为了减少操作的复杂度,对于新插入的节点,需要将其设置为红色。

当红黑树为空树时,我们插入节点后,将根节点更改为黑色,以满足红黑树的第三条性质,即根节点是黑色

当红黑树非空时,插入的节点为红色,此时要检查其父节点的颜色:

  • 如果父节点是黑色,那么插入完成。
  • 如果父节点是红色,那么为了保持红黑树的性质,则需要做出相对应的调整。

情况1

如果是下图所示的情况,即新节点的叔叔节点为红色节点,如果我们将祖父节点和父亲节点交换颜色,那么则会导致祖父节点和叔叔节点均为红色,不满足红黑树的第四条性质。因此,此时需要将父亲节点和叔叔节点均变为黑色,将祖父节点变为红色,然后递归向上调整整个红黑树。

红黑树与关联式容器

情况2

如果是下图所示的情况,即新节点的叔叔节点为黑色节点,且新节点、父亲节点和祖父节点在同一侧。此时我们也不能仅仅交换父亲节点和祖父节点的颜色,虽然这样做满足了没有连续的红色节点的性质,但是调整前左侧路径有 1 个黑色节点,右侧路径有 2 个黑色节点,调整后左、右路径都只有 1 个黑色节点,对于右侧路径而言,其调整后比调整前少了 1 个黑色节点,此时必然不会满足红黑树的第五个性质,即从根节点到每一个叶子节点的路径上,黑色节点的数量是相同的。因此,此时我们需要在此基础上再进行一次右旋操作:

红黑树与关联式容器

情况3

如果是下图所示的情况,即新节点的叔叔节点为黑色,但是新节点和父亲节点、祖父节点并不在同一侧。如果按照上一个情况的处理方式进行操作,即先交换父亲节点和祖父节点的颜色,然后由于叔叔节点所在的路径的黑色节点数目比操作前少了一个,因此进行一次右旋操作,可以看到,右旋后,有两个红色节点相连,不满足红黑树的性质。因此,当前这种情况并不能做出这样的调整,正确的处理方式是对当前节点和其父亲节点做一次左旋操作,将当前节点、父亲节点和祖父节点调整到同一侧,此时就会转变为上一种情况:

红黑树与关联式容器

红黑树插入操作的代码实现如下:

void insert(const T& val) {
	if (root_ == nullptr) {
		root_ = new Node(val);
		return;
	}
	
	Node* parent = nullptr;
	Node* cur    = root_;
	while (cur != nullptr) {
		if (cur->data_ > val) {
			parent = cur;
			cur    = cur->left_;
		} else if (cur->data_ < val) {
			parent = cur;
			cur    = cur->right_;
		} else {
			return;
		}
	}
	
	Node* node = new Node(val, parent, nullptr, nullptr, RED);
	if (parent->data_ > val) {
		parent->left_ = node;
	} else {
		parent->right_ = node;
	}
	
	// 如果新插入节点的父节点为红色 不满足红黑树性质 则调整红黑树
	if (RED == color(parent)) {
		fixAfterInsert(node);
	}
}

void fixAfterInsert(Node* node) {
	Node* father      = parent(node);
	Node* grandfather = parent(father);
	while (color(father) == RED) {
		if (left(grandfather) == father) {
			// 如果插入的节点在左子树中
			Node* uncle = right(grandfather);
			if (RED == color(uncle)) {
				// 情况1:叔叔为红色
				setColor(father, BLACK);
				setColor(grandfather, RED);
				setColor(uncle, BLACK);
				// 继续向上调整
				node = grandfather;
			} else {
				// 先处理情况3:叔叔节点为黑色且新节点、父节点、祖父节点不在一侧
				if (right(father) == node) {
					leftRotate(father);
					node        = father;
					father      = parent(node);
					grandfather = parent(father);
				}
				
				// 统一处理情况2:叔叔节点为黑色且新节点、父节点、祖父节点在一侧
				setColor(father, BLACK);
				setColor(grandfather, RED);
				rightRotate(grandfather);
				break; // 调整完成
			}
		} else {
			// 如果插入的节点在右子树中
			Node* uncle = left(grandfather);
			if (RED == color(uncle)) {
				// 情况1:叔叔为红色
				setColor(father, BLACK);
				setColor(grandfather, RED);
				setColor(uncle, BLACK);
				// 继续向上调整
				node = grandfather;
			} else {
				// 先处理情况3:叔叔节点为黑色且新节点、父节点、祖父节点不在一侧
				if (left(father) == node) {
					rightRotate(father);
					node        = father;
					father      = parent(node);
					grandfather = parent(father);
				}
				
				// 统一处理情况2:叔叔节点为黑色且新节点、父节点、祖父节点在一侧
				setColor(father, BLACK);
				setColor(grandfather, RED);
				leftRotate(grandfather);
				break; // 调整完成
			}
		}
	}
	
	// 将根节点强制为黑色 以满足红黑树的性质
	setColor(root_, BLACK);
}

1.2 删除操作

对于红黑树的删除操作而言,它是在 BST 树删除操作的基础上进行删除的,由于 BST 树的删除分为三种情况,即有两个子节点、有一个子节点、没有子节点,其中有两个子节点可以通过删除前驱节点/后继节点转化为第二种情况,那么总的来说,都是要将被删除节点的孩子节点放到其父节点的地址域中。因此,红黑树的删除操作可以分为如下几种情况:

  1. 如果删除的是一个红色节点,那么其子节点和父节点一定为黑色,此时就是一颗 BST 树的删除,无需做任何调整;
  2. 如果删除的是一个黑色节点,那么一定有某条路径上的黑色节点数目变少了,此时不满足红黑树第五条性质,所以需要做出相应的调整,分为如下两种情况:
    1. 如果补上来的孩子节点是红色,那么直接将这个孩子涂成黑色,调整完成;
    2. 如果补上来的孩子节点是黑色,此时需要从其兄弟节点处借一个黑色节点以弥补缺失的黑色节点的数目,此时又会分为四种情况;

情况1

如果是下图所示的情况,即删除的节点在左边,其右边的兄弟是黑色的,且兄弟的右孩子是红色的。那么首先应该交换兄弟节点和其父节点的颜色,然后对父节点进行左旋操作,最后再将兄弟节点的右孩子变为黑色即可:

红黑树与关联式容器

情况2

如果是下图所示的情况,即删除的节点在左边,其右边的兄弟是黑色,且兄弟的左孩子是红色。由于需要在兄弟节点上借一个黑色节点,且借完之后需要在兄弟节点处找到一个红色节点进行变色,弥补缺失的黑色节点。因此如果按照情况 1 的处理方式,对待删除节点的父节点进行一次左旋转操作,那么兄弟节点的红色节点就会跑到左子树上来,此时,兄弟节点便无法利用红色节点来弥补缺失的黑色节点,因此,此种处理方式不可取。

正确的处理方式应该是先对兄弟节点进行一次右旋操作,然后交换兄弟节点和红色节点的颜色,此时就会转变为情况1:

红黑树与关联式容器

情况3

如果是下图所示的情况,即删除的节点在左边,其右边的兄弟是黑色,且兄弟的两个孩子也都是黑色。此时,无法向兄弟借黑色节点,因为借之后兄弟无法弥补缺失的黑色节点。因此需要将所有路径上的黑色数目都减少一个,那么先将兄弟节点涂为红色,然后让 node 指向删除结点的父节点,如果父节点为红色,那么将其变为黑色即可;如果父节点为黑色,那么此时就需要查看父节点的兄弟节点,继续落在这几种情况的范围内,不断向上回溯调整:

红黑树与关联式容器

情况4

如果是下图所示的情况,即删除节点在左边,其右边的兄弟为红色,那么根据红黑树的性质,兄弟节点的父亲及其两个孩子节点均为黑色。此时,应该先交换兄弟节点和父亲节点的颜色,然后对父亲节点执行左旋操作,操作完成后,此时待删除结点的兄弟节点就会变为黑色,那么就会又转变为上面三种情况:

红黑树与关联式容器

红黑树的删除操作的代码实现如下:

void remove(const T& val) {
	if (root_ == nullptr) return;
	
	Node* cur = root_;
	while (cur != nullptr) {
		if (cur->data_ > val) {
			cur = cur->left_;
		} else if (cur->data_ < val) {
			cur = cur->right_;
		} else {
			break;
		}
	}
	
	// 没有找到目标节点 返回
	if (cur == nullptr) return;
	
	// 删除cur节点
	if (cur->left_ != nullptr && cur->right_ != nullptr) {
		// 情况3,找寻前驱节点
		Node* pre = cur->left_;
		while (pre->right_ != nullptr) {
			pre = pre->right_;
		}
		cur->data_ = pre->data_;
		cur        = pre;
	}
	
	// 处理情况1和情况2
	Node* child = cur->left_;
	if (child != nullptr) {
		child = cur->right_;
	}
	
	if (child != nullptr) {
		child->parent_ = cur->parent_;
		if (cur->parent_ == nullptr) {
			// 删除的是根节点 更新根节点
			root_ = child;
		} else {
			if (cur->parent_->left_ == cur) {
				cur->parent_->left_ = child;
			} else {
				cur->parent_->right_ = child;
			}
		}
		
		Color c = color(cur);
		delete cur;
		
		if (c == BLACK) {
			// 如果删除的是黑色节点 需要进行节点调整
			fixAfterRemove(child);
		}
	} else {
		if (cur->parent_ == nullptr) {
			// 删除的cur为根节点
			delete cur;
			root_ = nullptr;
			return;
		} else {
			// 删除的cur为叶子节点
			if (color(cur) == BLACK) {
				fixAfterRemove(cur);
			}
			
			if (cur->parent_->left_ == cur) {
				cur->parent_->left_ = nullptr;
			} else {
				cur->parent_->right_ = nullptr;
			}
			
			delete cur;
		}
	}
}

void fixAfterRemove(Node* node) {
	while (node != root_ && color(node) == BLACK) {
		if (left(parent(node)) == node) {
			// 如果删除的节点在左子树
			Node* brother = right(parent(node));
			if (color(brother) == RED) {
				// 情况4:兄弟节点为红色
				setColor(parent(node), RED);
				setColor(brother, BLACK);
				leftRotate(parent(node));
				brother = right(parent(node));
			}
			
			if (color(left(brother)) == BLACK
				&& color(right(brother)) == BLACK) {
				// 情况3:兄弟节点为黑色 且兄弟节点的左右孩子也为黑色
				setColor(brother, RED);
				node = parent(node);
			} else {
				if (color(right(brother)) != RED) {
					// 情况2:兄弟节点的左孩子为红色
					setColor(brother, RED);
					setColor(left(brother), BLACK);
					rightRotate(brother);
					brother = right(parent(node));
				}
				
				// 情况1:兄弟节点的右孩子为红色
				setColor(brother, color(parent(node)));
				setColor(parent(node), BLACK);
				setColor(right(brother), BLACK);
				leftRotate(parent(node));
				break;
			}
		} else {
			// 如果删除的节点在右子树
			Node* brother = left(parent(node));
			if (color(brother) == RED) {
				// 情况4:兄弟节点为红色
				setColor(parent(node), RED);
				setColor(brother, BLACK);
				rightRotate(parent(node));
				brother = left(parent(node));
			}
			
			if (color(left(brother)) == BLACK
				&& color(right(brother)) == BLACK) {
				// 情况3:兄弟节点为黑色 且兄弟节点的左右孩子也为黑色
				setColor(brother, RED);
				node = parent(node);
			} else {
				if (color(left(brother)) != RED) {
					// 情况2:兄弟节点的左孩子为红色
					setColor(brother, RED);
					setColor(right(brother), BLACK);
					leftRotate(brother);
					brother = left(parent(node));
				}
				
				// 情况1:兄弟节点的右孩子为红色
				setColor(brother, color(parent(node)));
				setColor(parent(node), BLACK);
				setColor(left(brother), BLACK);
				rightRotate(parent(node));
				break;
			}
		}
	}
	
	// 如果node指向的节点是红色 直接涂成黑色即可
	setColor(node, BLACK);
}

所有的代码实现:Kohirus-Github

1.3 红黑树与AVL树

红黑树与 AVL 树的比较如下:

操作 AVL 红黑树
是否为平衡树
增删查时间复杂度 (O(logn)) (O(logn))
插入时的最多旋转次数 2 2
删除时的最多旋转次数 (O(logn)) 3

2.RB-tree

RB-Tree 是一个被广泛应用的二叉搜索树,它是红黑树数据结构的实现,也是 SGI STL 唯一实现的一种搜寻树,作为关联式容器的底部机制之用。

2.1 节点设计

在 SGI STL 中,红黑树的节点设计如下:

红黑树与关联式容器

typedef bool __rb_tree_color_type;
const __rb_tree_color_type __rb_tree_red   = false;	// 红色为0
const __rb_tree_color_type __rb_tree_black = true;	// 黑色为1

struct __rb_tree_node_base {
    typedef __rb_tree_color_type color_type;
    typedef __rb_tree_node_base* base_ptr;
    
    color_type color;	// 结点颜色 非红即黑
	base_ptr parent;	// 父结点
    base_ptr left;		// 指向左结点
    base_ptr right;		// 指向右结点
    
	static base_ptr minimum(base_ptr x) {
        while (x->left != 0) x = x->left;	// 一直往左走 就会找到最小值
        return x;
    }
    
    static base_ptr maximum(base_ptr x) {
        while (x->right != 0) x = x->right;	// 一直往右走 就会找到最大值
        return x;
    }
};

template<class Value>
struct __rb_tree_node : public __rb_tree_node_base {
    typedef __rb_tree_node<Value>* link_type;
    Value value_field;	// 结点值
};

2.2 迭代器

为了更大的弹性,SGI 将 RB-tree 迭代器实现为两层,如图便是双层结点结构和双层迭代器结构之间的关系:

红黑树与关联式容器

由于树状结构的各种操作,最需要注意的就是边界情况的发送,也就是走到根节点时需要有特殊的处理,为了简化这种处理,SGI STL 为根节点再设计了一个父节点,名为【header】。同时让其左子节点指向最小结点,右子节点指向最大结点。

红黑树与关联式容器

RB-tree 的迭代器属于【双向迭代器】,但不具备随即定位能力。

其前进操作 operator++() 调用了基层迭代器的 increment(),用于获取当前结点的后继结点。

void increment()
{
    if (node->right != 0) {			// 如果有右子节点。状况(1)
        node = node->right;			// 就往右走
        while (node->left != 0)		// 然后一直往左子树走到底
            node = node->left;		// 即是解答
    }
    else {							// 如果没有右子节点。状况(2)
        base_ptr y = node->parent;	// 找出父节点
        while (node == y->right) {	// 如果当前结点本身是个右子节点
            node = y;				// 就一直上溯 直到不为右子节点为止
            y = y->parent;
        }
        if (node->right != y)		// 若此时的右子节点不等于此时的父节点
            node = y;				// 此时的父节点即为解答。状况(3)
    }								// 否则此时的node即为解答。状况(4)
}

状况1:当前节点有右子节点。如下图所示,节点 8 的后继结点即为节点 10:

红黑树与关联式容器

状况2:当前节点无右子节点。进而转入状况 3 或 4:

红黑树与关联式容器

状况3:此时的右子节点不等于此时的父节点。如图,节点 7 的后继节点即为节点 8:

红黑树与关联式容器

状况4:此时的右子节点等于此时的父节点。即当迭代器指向根节点而其无右子节点时,其后继元素即为 header 结点:

红黑树与关联式容器

其后退操作 operator--() 则调用了基层迭代器的 decrement(),用于获取当前结点的前驱节点。

void decrement()
{
    if (node->color == __rb_tree_red && // 如果是红节点
        node->parent->parent == node)	// 且父节点的父节点等于自己
        node = node->right;				// 状况(1) 右子节点即为解答
    else if (node->left != 0) {			// 如果有左子节点。状况(2)
        base_ptr y = node->left;		// 令y指向左子节点
        while (y->right != 0)			// 当y有右子节点时
            y = y->right;				// 一直往右子节点走到底
        node = y;						// 最后即为答案
    }
    else {								// 即非根节点 亦无左子节点
        base_ptr y = node->parent;		// 状况(3) 找出父节点
        while (node == y->left) {		// 当前节点身为左子节点
            node = y;					// 一直交替往上走 直到当前节点
            y = y->parent;				// 不为左子节点
        }
        node = y;						// 此时父节点即为答案
    }
}

状况1:当前节点为红色且父节点的父节点为自己。此时当前节点即为 header 节点,则其前驱节点为根节点。

红黑树与关联式容器

状况2:当前结点存在左子节点。如图,节点 8 的前驱节点为节点 7。

红黑树与关联式容器

状况3:即非根节点,亦无左子节点。如图,节点 10 的前驱节点为节点 8。

红黑树与关联式容器

2.3 空间配置

RB-tree 定义了自己的专属空间配置器 rb_tree_node_allocator,每次可恰恰配置一个节点。RB-tree 的构造方式有两种,一种是以现有的 RB-tree 复制一个新的 RB-tree,另一种是产生一棵空树。

如下图所示,左侧为 RB-tree 的初始状态,右侧为加入第一个结点后的状态。

红黑树与关联式容器

2.4 插入及平衡

RB-tree 的插入操作分为两种:insert_unique()insert_equal(),前者表示被插入节点的键值(key)在整棵树中必须是独一无二的,而后者则表示被插入结点的键值在整棵树中可以重复。

其中,insert_equal() 是从根节点开始寻找合适的插入点,遇到 “大” 则往左,遇到 “小于或等于” 则向右,最后调用 __insert(x,y,v) 进行插入操作,其中,x 表示新值插入点,y 表示插入点的父节点,v 表示新值,其返回值是一个 RB-tree 的迭代器,指向新增结点。而 insert_unique() 的返回值是一个 pair,第一元素是一个 RB-tree 的迭代器,指向新增结点,第二元素则表示插入成功与否。

任何插入操作,于结点插入完毕后,都要做一次调整操作,将树的状态调整到符合 RB-tree 的要求,其是通过 __rb_tree_rebalance() 来完成此功能,它是一个全局函数

而其旋转操作是通过全局函数 __rb_tree_rotate_left()__rb_tree_rotate_right() 来分别进行左旋和右旋。

3.set&multiset

set 的特性是,所有元素都会根据元素的键值自动被排序。set 的元素不像 map 那样可以同时拥有实值(key)和键值(value),set 元素的键值就是实值,实值就是键值。set 不允许两个元素有相同的键值。

标准的 STL set 以 RB-tree 作为底层机制,因此 set 的元素有自己的排列规则,为此,set的迭代器被定义为底层机制的 const_iterator,杜绝写入操作。

同时,set 具有与 list 相同的特性:当用户对它进行元素新增 insert 操作或者删除 erase 操作时,操作之前的所有迭代器,在操作完成之后依然有效。在默认情况下,set 采用递增排序。

multiset 的特性及用法和 set 完全相同,唯一的差别在于它允许键值重复。set 使用的是底层机制 RB-Tree 的 insert_unique(),而 multiset 使用的则是 insert_equal()

4.map&multimap

map 的特性是所有元素都会根据元素的键值自动被排序。其所有元素都是 std::pair 类型,同时拥有实值和键值,其中,第一元素被视为键值,第二元素被视为实值。而且,map 不允许两个元素拥有相同的键值。

pair 定义于 <stl_pair.h>,其实现如下:

template<class T1, class T2>
struct pair {
    typedef T1 first_type;
    typedef T2 second_type;
    T1 first;
    T2 second;
    pair() : first(T1()), second(T2()) {}
    pair(const T1& a, const T2& b) : first(a), second(b) {}
};

map 的迭代器既不是 constant iterators,也不是一种 mutable iterators,因为其迭代器不可以改变键值,但是可以改变实值。同时,map 具有与 list 相同的特性:当用户对它进行元素新增 insert 操作或者删除 erase 操作时,操作之前的所有迭代器,在操作完成之后依然有效。

红黑树与关联式容器

默认情况下,map 采用递增排序。

multimap 的特性及用法和 map 完全相同,唯一的差别在于它允许键值重复。map 使用的是底层机制 RB-Tree 的 insert_unique(),而 multimap 使用的则是 insert_equal()

原文链接: https://www.cnblogs.com/tuilk/p/17060946.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    红黑树与关联式容器

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/313187

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 下午12:43
下一篇 2023年2月16日 下午12:44

相关推荐