本文介绍红黑树的相关知识。本文主要参考开课吧胡船长 的课程。本文的代码见red_black_tree.py
定义
红黑树是一种二叉搜索树。
首先,我们定义一下树的节点。如下代码,每个节点包含左孩子(left)和右孩子(right),val表示该节点的值。
1 2 3 4 5 6 class TreeNode : def __init__ (self, val, color, left, right ): self.val = val self.color = color self.left = left self.right = right
二叉搜索树
二叉搜索树是指,左子树中的节点均小于该节点,右子树中的节点均大于该节点。简单地说,就是
左子树 < 该节点 < 右子树 \text{左子树} \lt \text{该节点} \lt \text{右子树}
左子树 < 该节点 < 右子树
这条性质使得检索从 O ( n ) O(n) O ( n ) 降到 O ( log n ) O(\log n) O ( log n ) ,类似二分查找,每次可以排除一半的候选。
检索代码如下:
1 2 3 4 5 6 7 8 9 10 11 def search (self, val ): def _search (node ): if node is self.NIL: return self.NIL if val == node.val: return node elif val < node.val: return _search(node.left) else : return _search(node.right) return _search(self.root)
知道了二叉搜索树的主要性质,我们就遇到如下问题。
给定一些元素,如何构造二叉搜索树?
有很多关于二叉搜索树的构造方法。红黑树就是其中一种。所以红黑树算法,主要是为了解决二叉搜索树的构造问题 。
红黑树的5条性质
红黑树在二叉搜索树的基础上,为每个节点定义了颜色信息。这些颜色信息仅供红黑树算法使用。
下面是红黑树算法的五条性质。
每个节点非黑即红
根节点是黑色
叶子节点是黑色
如果一个节点是红色,则其子节点是黑色
从根节点出发到所有叶子节点的路径上,黑色节点数量相同
每个节点非黑即红
看似废话,实则有用。因为红黑树除了红色和黑色,还有第三种颜色,双重黑色。双重黑色节点会在删除节点后出现,随后算法会消除双重黑色。这个性质是说调整好的树没有节点是双重黑色。
1 2 3 4 5 def check1 (self, node ): if node is None : return True flag = node.color == self.RED or node.color == self.BLACK return flag and self.check1(node.left) and self.check1(node.right)
根节点是黑色
这条性质最容易检查和满足。每次调整完树,将根节点染成黑色即可。
1 2 def check2 (self, node ): return self.tree.root.color == self.BLACK
叶子节点是黑色
这里的叶子节点为NIL节点,即没有值的None节点。对于树中某个节点,若其没有左孩子,则表示其左孩子为NIL节点。一般的,NIL节点没有值,其颜色为黑色。
另一点需要说明的是,整棵树只有一个NIL节点实例,各个叶子节点均指向这个实例,即共享颜色。若NIL节点为红色,则所有叶子节点为红色。
1 2 3 4 5 def check3 (self, node ): if node is self.NIL: return node.color == self.BLACK assert node is not None return self.check3(node.left) and self.check3(node.right)
如果一个节点是红色,则其子节点是黑色
这条性质保证了没有双红的情况,即父节点为红色,子节点不能是红色。
1 2 3 4 5 6 7 def check4 (self, node ): if node is self.NIL: return True flag = True if node.color == self.RED: flag = node.left.color == self.BLACK and node.right.color == self.BLACK return flag and self.check4(node.left) and self.check4(node.right)
从根节点出发到所有叶子节点的路径上,黑色节点数量相同
某种程度上说,黑色节点控制了红黑树的平衡。注意由于根节点和叶子节点均为黑色,则一棵存在元素的树,路径黑色节点数量至少是2。
1 2 3 4 5 6 7 8 9 10 11 def check5 (self, node ): def black_count (node ): if node is self.NIL: return 1 left_count = black_count(node.left) right_count = black_count(node.right) if left_count != right_count or left_count == -1 or right_count == -1 : return -1 else : return left_count + node.color return black_count(self.tree.root) != -1
小结
根据以上的5条性质,我们可以得到一个结论。
红黑树中,从根节点到叶子节点的最长路径,至多是最短路径的 2 2 2 倍。
红黑树的添加调整
如何在二叉搜索树中添加元素
算法很简单,先在该树中尝试查找该元素。由于不在树中,一定找不到。但在寻找的过程中,会找到一个叶子节点,意思是这个元素应该在这里,但目前这里没有元素。那我们就把这个新元素插入到这里。
1 2 3 4 5 6 7 8 def _add (node ): if node is self.NIL: node = TreeNode(val, self.RED, self.NIL, self.NIL) elif val < node.val: node.left = _add(node.left) else : node.right = _add(node.right) return self._adjust_add(node)
然而这种策略会造成二叉树的不平衡,所以我们在插入后,需要调整一下树的平衡。
插入红色节点
首先,我们需要确定插入的节点是红色还是黑色。会发现,当插入
黑色:一定会调整。插入那条路径会多一个黑色节点。
红色:可能会调整。新节点的父节点是黑色时,不用调整;是红色时,需要调整。
综合来看,我们设定每次插入红色节点即可。
这里有一个问题:插入红色节点,需要调整时,为什么不将此节点设为黑色,反正要调整,没准黑色更好调整?
原因是这样写起代码来比较复杂,需要更多地分类讨论。且也没有看到有明显的改进。
故我们插入节点时,新节点的默认颜色为红色。
从爷爷的位置看
当我们一直插入红色时,势必会出现冲突。因为红色节点不能直接相连(简称为双红 ,即父子节点均为红色)。
当出现冲突时,我们需要调整树中节点的颜色。注意我们在调整颜色时,是一个递归过程。即叶子节点先出现的冲突,然后我们逐层向上调整。
调整插入节点的一个核心思路是从爷爷的位置看 ,下面我们来分析具体怎么看。
爷爷要把黑色给父辈,父辈们同意了
双红出现后,最简单的解决方法是,父节点向上要一个黑色。(思考一下为什么爷爷节点一定是黑色)
但爷爷节点要给父节点黑色时,需要同时给两个孩子。也就是要求父节点的兄弟也是红色,这时才能将爷爷的黑色下移一层。(在调整过程中,让每条路径的黑色节点数量不变)
如下图。这种调整可能会将冲突上移。也就是爷爷节点出现双红的情况。
爷爷要把黑色给父辈,父辈们不同意
当父辈们出现黑色时,爷爷的黑色就不能传给父辈。这时父亲就需要抢占爷爷的黑色,具体的,通过旋转将父亲节点旋转到爷爷节点的位置。
下面的动画展示左旋的过程:
下面的动画展示右旋的过程:
当我们想让右边的孩子成为根节点,就是通过左旋,让右节点向左移动,成为中间的根节点。反之,右旋让左孩子向右移动,成为中间的根节点。
在红黑树中,父节点抢占爷爷节点会出现两种情况。
父节点可以直接抢占爷节点
如下图,父节点抢了爷爷的位置,并且抢了爷爷的黑色。爷爷只能接受父节点原来的红色。调整完之后,双红消失。
上述过程成功的关键在于,图中绿圈节点没有冲突。可以设想绿圈节点为红色,则这样调整过后,绿圈节点和父节点又形成双红。总结规律可以发现,无论左旋还是右旋,变更父节点的节点(即动图中的between E and S)不能是红色。这个节点在旋转前后,父节点发生变化,我们称此节点为变父节点。
父节点需要保证变父节点为黑色
当变父节点为红色时,我们只需要将其旋转为父节点即可。如下图。我们先将变父节点旋转为父节点,再让此父节点抢占爷节点。
红色上移
进行完旋转操作后,我们可以让红色上移。如下图,简单地染色即可实现。红色上移的目的是让树中的红色节点尽可能少。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def add (self, val ): def _add (node ): if node is self.NIL: node = TreeNode(val, self.RED, self.NIL, self.NIL) elif val < node.val: node.left = _add(node.left) else : node.right = _add(node.right) return self._adjust_add(node) self.root = _add(self.root) self.root.color = self.BLACK
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 def _adjust_add (self, node ): if node.left.color == self.BLACK and node.right.color == self.BLACK: return node recolor = False if node.right.color == self.BLACK and node.left.color == self.RED: if node.left.right.color == self.RED: node.left = self._rot_left(node.left) if node.left.left.color == self.RED: node = self._rot_right(node) recolor = True elif node.left.color == self.BLACK and node.right.color == self.RED: if node.right.left.color == self.RED: node.right = self._rot_right(node.right) if node.right.right.color == self.RED: node = self._rot_left(node) recolor = True elif node.left.color == self.RED and node.right.color == self.RED: recolor = True if recolor: node.color = self.RED node.left.color = self.BLACK node.right.color = self.BLACK return node
红黑树的删除调整
python中的红黑树实现-SortedList
相关阅读