红黑树学习笔记

234 阅读10分钟

红黑树学习笔记

工具:

在哪里会使用红黑树

对红黑树的使用说的是:

a. key-value形式, 查找使用 b. 利用红黑树的中序遍历(按照顺序的)

  1. stl中的map, 是对红黑树整体的封装, 从某种概念上来说他不属于使用更多的是对红黑树的封装
  2. nginx
  3. 定时器
  4. 进程调度的cfs, 进程的集合(n个进程使用红黑树来存储), 在调度的过程中查找最公平的值, 利用了红黑树中序遍历是顺序的, 查找速度非常的快且是有序的, cfs把调度的时间作为key值, 每一次从左下角的树开始查找
  5. 内存管理, 每次调用malloc会分配一块内存, 这些内存块就会用红黑树存储起来.

为什么要用红黑树来存储?

块内存的表述方法:

  • 起始位置加长度
  • 开始位置和结束位置

讲内存开始的地址当作key, 用指针指向这个key, 再加上一个长度;

每分配一块内存就往红黑树上加一个节点

  • 这个地方还需要补充一下

红黑树的性质

  1. 每个结点是红的或是黑的 (其实啥颜色都可以, 反正是两个不同的颜色就好, 管他是啥颜色呢)
  2. 根结点是黑的
  3. 每个叶子结点是黑的
  4. 如果一个结点是红的, 则它的两个儿子结点都是黑的 (很容易从这句话推断出来, 不可以出现两个红色的结点同时相邻)
  5. 对每个结点, 从该结点到其子孙结点的所有路径上包含相同数目的黑结点 (非常重要的性质! 这决定了红黑树的平衡, 红黑树不是平衡所有结点, 平衡的是黑色结点的高度)

性质的例子

image.png

这里有一个前置条件: 所有叶子结点都隐藏, 并且为黑色, 故第3个性质是满足的

  1. 不满足第5个性质
  2. 符合1、2、3、4、5, 所以2是红黑树
  3. 不满足5
  4. 不满足1

完整的红黑树

image.png

红黑树的旋转

image.png

先忽略颜色的问题这不重要,旋转是树的特性和颜色无关

左旋

  • x右子树指向y左子树
  • y父结点指向x父结点
  • y左子树指向x

image.png

一种向左倒的感觉, 其实记住这三条代码也能出来不理解为什么是这么旋的感觉也无所谓, 反正是为了平衡

右旋

  • y左子树指向x右子树
  • x父结点指向y父结点
  • x右子树指向y

image.png

一种向右倒的感觉, 多画几次图就知道怎么旋转的了

其实只用记住一个左旋就好了, 右旋就是 x换成y, 左换成右就好了

旋转代码的实现

红黑树相关定义

结点数据类型
方便之后的拓展所以使用了typedef

typedef int KEY_TYPE;

结点

typedef struct _rbtree_node {
    unsigned char color;
    struct _rbtree_node *left;
    struct _rbtree_node *right;
    struct _rbtree_node *parent;

    KEY_TYPE key;
    void* value;
} rbtree_node;

红黑树
所有的空结点都指向 nil

typedef struct _rbtree {
    rbtree_node* root; // 根结点
    rbtree_node* nil; // 空结点, 所有空结点都指向这个
} rbtree;

左旋

void _left_rotate(rbtree* T, rbtree_node* x){
	rbtree_node *y = x->right;
	
	x->right = y->left; // x的右子树指向y的左子树
	if (y->left != T->nil) {
		y->left->parent = x; // x的右子树指向y的左子树
	}

	y->parent = x->parent; // y的父结点指向x的父结点
	if (x->parent == T->nil) { // 判断是否为根结点
		T->root = y;
	} else if (x == x->parent->left) { // 判断y要接到原来的左子树还是右子树
		x->parent->left = y;
	} else {
		x->parent->right = y; // 判断y要接到原来的左子树还是右子树
	}

	y->left = x; // y的左子树指向x
	x->parent = y;  // y的左子树指向x
}

右旋

void _right_rotate(rbtree* T, rbtree_node* y){
	rbtree_node* x = y->left;
	
	y->left = x->right; // y的左子树指向x的右子树
	if (x->right != T->nil) {
		x->right->parent = y; // y的左子树指向x的右子树
	}

	x->parent = y->parent; // x的父结点指向y的父结点
	if (y->parent == T->nil) { // 判断是否为根结点
		T->root = x;
	} else if (y == y->parent->right) { // 判断y要接到原来的左子树还是右子树
		y->parent->right = x;
	} else {
		y->parent->left = x; // 判断y要接到原来的左子树还是右子树
	}

	x->right = y; // x的右子树指向y
	y->parent = x; // x的右子树指向y
}

其实就和前面的一样, 把x换成y, 左右互换, 仔细看下代码逻辑都是一样的

插入结点

首先想想二叉树怎么做插入的, 先搜索, 搜索的逻辑是和正常的二叉树是一样的逻辑, 但是会一直往下做搜索直到碰到叶子节点

但是如果插入的时候遇到值相等的时候, 需要根据对应的业务来处理

再插入的时候需要考虑颜色的因素, 严格遵守红黑树的性质

再插入一个结点之前, 这个红黑树一定是满足红黑树的性质, 故我们在加入一个新的结点的时候默认这个结点是红色的因为他不会去违背第5条性质(对每个结点, 从该结点到其子孙结点的所有路径上包含相同数目的黑结点)

如果加个黑的, 处理异常复杂, 可能每次都要旋转, 每次都需要去处理第5条

红色的好判断, 我们只需要去验证第4条性质, 第4条性质非常好验证只需要判断一下父结点的颜色

在调整插入之前, 我们需要知道不管从哪个黑色节点的视角看过去他都是一个红黑树, 所以我们只需要关心父节点和祖父节点的关系

前提:

  1. 父结点是祖父的左子树
  2. 插入之前必须是个正常的红黑树

问一下自己

  1. 要不要牵扯到祖父结点的颜色? 祖父结点都不用去思考, 肯定是黑色的! 性质

  2. 为什么需要知道叔父结点的颜色? 方便旋转, 为了满足红黑树性质第5条

橙色是待插入

1. 叔父节点是红色

image.png

把祖父结点变成红色, 把父节点和叔父结点变成黑色, 同时要保证当前遍历的节点始终是红色的

image.png

2. 叔父结点是黑色, 当前结点是左子树

image.png

3. 叔父结点是黑色, 当前结点是右子树

image.png

  1. 左旋
  2. 并使在下边的父节点的颜色变黑, 祖父节点变红
  3. 右旋

image.png

image.png

image.png

void rbtree_insert(rbtree* T, rbtree_node* z)
{
	rbtree_node* x = T->root; // dameon节点, 供遍历使用
	rbtree_node* y = T->nil; // 这个的用处是在退出循环的时候, y一定会是x的父节点
	while (x != T->nil) {
		y = x; // 保证y是x的父结点
		if (z->key < x->key) {
			x = x->left;
		} else if (z->key > x->key) {
			x = x->right;
		} else {
			// 如果有相同的这里有两个处理方式
			// 可以直接返回不做插入
			// 也可以覆盖原本的结点
			// 取决于应用场景
#if 1
			return;
#else
			x->key = z->key;
			return;
#endif
		}
	}

	z->parent = y;
	if (y == T->nil) {
		// 插入的第一个结点, 也就是说是根结点
		T->root = z;
	} else if (z->key < y->key) {
		y->left = z;
	} else {
		y->right = z;
	}

	// 新插入的叶子结点都置空
	z->left = T->nil;
	z->right = T->nil;
	z->color = RED; // 结点默认为红色, 因为比较好判断和操作


	rbtree_insert_fixup(T, z);
}
void rbtree_insert_fixup(rbtree* T, rbtree_node* z)
{
	// 父节点是红色的需要调整
	while (z->parent->color == RED) {
		// 父节点是祖父节点的左子树还是右子树
		// 左子树
		if (z->parent == z->parent->parent->left) {
			rbtree_node* y = z->parent->parent->right; // 叔父结点
			// 1. 叔父结点是红色
			if (y->color == RED) {
				// 把祖父结点变成红色, 把父节点和叔父结点变成黑色
				// 父节点变黑
				z->parent->color = BLACK;
				// 叔父结点变黑
				y->color = BLACK;
				// 祖父结点变红
				z->parent->parent->color = RED;

				// 将当前结点变成祖父结点, 继续向上检查红黑树是否需要调整
				z = z->parent->parent;
			} else {
				// 3. 叔父结点是黑色, 当前结点是右子树
				if (z == z->parent->right) {
					// 左旋完之后, 会把父节点旋转成为子节点
					z = z->parent;
					_left_rotate(T, z);
				}
				// 2. 叔父结点是黑色, 当前结点是左子树 这种情况会直接进行右旋平衡
				z->parent->color = BLACK;
				z->parent->parent->color = RED;
				// 右旋祖父结点
				_right_rotate(T, z->parent->parent);
			}
		} else {
			rbtree_node* y = z->parent->parent->left;
			if (y -> color == RED) {
				z->parent->color = BLACK;
				y->color = BLACK;
				z->parent->parent->color = RED;
				z = z->parent->parent;
			} else {
				// 3. 叔父结点是黑色, 当前结点是左子树
				if (z == z->parent->left) {
					z = z->parent;
					_right_rotate(T, z);
				}
				// 2. 叔父结点是黑色, 当前结点是左子树 这种情况会直接进行左平衡
				z->parent->color = BLACK;
				z->parent->parent->color = RED;
				_left_rotate(T, z->parent->parent);
			}
		}
	}

	// 把根节点的颜色置为黑色
	T->root->color = BLACK;
}

删除节点

image.png

删除172这个节点

  1. 首先找出第一个172大的数, 也就是206
  2. 206这个节点复制到172这个位置
  3. 把原本206这个位置删除
  4. 原本237的左子节点就会变成224
  5. 并从x开始调整

删除的节点如果是黑色的才需要调整, 如果删除的是红色的不需要调整

当前节点是父节点的左子树

  1. 当前节点的兄弟节点是红色

  2. 当前节点的兄弟节点是黑色, 而且兄弟节点的两个孩子都是黑色的

  3. 当前结点的兄弟结点是黑色的, 而且兄弟结点的 左孩子是红色的, 右孩子是黑色的

  4. 当前结点的兄弟结点是黑色的, 而且兄弟结点的右孩子是红色的

rbtree_node* rbtree_delete(rbtree* T, rbtree_node* z)
{
	rbtree_node* y = T->nil;
	rbtree_node* x = T->nil;

	if ((z->left == T->nil) || (z->right == T->nil)) {
		y = z;
	} else {
		y = rbtree_successor(T, z); // 1. 找出第一个比自己大的数
	}
	
	if (y->left != T->nil) {
		x = y->left;
	} else if (y->right != T->nil) {
		x = y->right;
	}

	x->parent = y->parent;
	if (y->parent == T->nil) {
		T->root = x;
	} else if (y == y->parent->left) {
		y->parent->left = x;
	} else {
		y->parent->right = x;
	}

	if (y != z) {
		z->key = y->key;
		z->value = y->value;
	}
	
	if (y->color == BLACK) {
		rbtree_delete_fixup(T, x);
	}
	return y;
}

void rbtree_delete_fixup(rbtree* T, rbtree_node* x)
{
	while ((x != T->root) && (x->color == BLACK)) {
		if (x == x->parent->left) {

			rbtree_node *w= x->parent->right;
			if (w->color == RED) {
				w->color = BLACK;
				x->parent->color = RED;

				_left_rotate(T, x->parent);
				w = x->parent->right;
			}

			if ((w->left->color == BLACK) && (w->right->color == BLACK)) {
				w->color = RED;
				x = x->parent;
			} else {

				if (w->right->color == BLACK) {
					w->left->color = BLACK;
					w->color = RED;
					_right_rotate(T, w);
					w = x->parent->right;
				}

				w->color = x->parent->color;
				x->parent->color = BLACK;
				w->right->color = BLACK;
				_left_rotate(T, x->parent);

				x = T->root;
			}

		} else {

			rbtree_node *w = x->parent->left;
			if (w->color == RED) {
				w->color = BLACK;
				x->parent->color = RED;
				_right_rotate(T, x->parent);
				w = x->parent->left;
			}

			if ((w->left->color == BLACK) && (w->right->color == BLACK)) {
				w->color = RED;
				x = x->parent;
			} else {

				if (w->left->color == BLACK) {
					w->right->color = BLACK;
					w->color = RED;
					_left_rotate(T, w);
					w = x->parent->left;
				}

				w->color = x->parent->color;
				x->parent->color = BLACK;
				w->left->color = BLACK;
				_right_rotate(T, x->parent);

				x = T->root;
			}

		}
	}

	x->color = BLACK;

}
  • 还需要完善

搜索节点

红黑树也是二叉树, 按照二叉搜索树的那一套就行

rbtree_node* rbtree_search(rbtree* T, KEY_TYPE key)
{
	rbtree_node* node = T->root;
	while (node != T->nil) {
		if (key < node->key) {
			node = node->left;
		} else if (key > node->key) {
			node = node->right;
		} else {
			return node;
		}
	}
	
	return T->nil;
}

源码: rbtree.c

欢迎补充和指出问题