通用算法 - [树结构] - 红黑树

<1> 红黑树

参考:30张图带你彻底理解红黑树
红黑树是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,可以是RED或者BLACK。通过对任何一条从根到叶子的路径上各个着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。本章主要介绍了红黑树的性质、左右旋转、插入和删除。重点分析了在红黑树中插入和删除元素的过程,分情况进行详细讨论。一棵高度为h的二叉查找树可以实现任何一种基本的动态集合操作,如SEARCH(查找)、PREDECESSOR(前驱)、SUCCESSOR(后继)、MIMMUM(最小)、MAXMUM(最大)、INSERT(插入)、DELETE(删除)等。当二叉查找树的高度较低时,这些操作执行的比较快,但是当树的高度较高时,这些操作的性能可能不比用链表好。**红黑树(red-black tree)是一种平衡的二叉查找树,它能保证在最坏情况下,基本的动态集合操作运行时间为O(lgn)。**本章内容有些复杂,看了两天,才大概清楚其插入和删除过程,日后需要经常回顾,争取完全消化掉。红黑树的用途非常广泛,例如STL中的map就是采用红黑树实现的,效率非常之高,有机会可以研究一下STL的源代码。

1、红黑树的性质

红黑树中的每个结点包含五个域:color、key、left、right和parent。如果某结点没有一个子结点或父结点,则该结点相应的指针parent域包含值为NIL(NIL并是是空指针,此处有些迷惑,一会解释)。把NIL视为指向红黑树的外结点(叶子)的指针,而把带关键字的结点视为红黑树的内结点。红黑树结点结构如下所示:

 1 #define RED  0
 2 #define BLACK 1
 3 struct RedBlackTreeNode
 4 { 
 5     T key;
 6     struct RedBlackTreeNode * parent;
 7     struct RedBlackTreeNode * left;
 8     struct RedBlackTreeNode * right;
 9     int color;
10 };

红黑树的性质如下:

(1)每个结点或是红色,或是黑色。
(2)根结点是黑色。
(3)每个叶子结点(NIL)是黑色。
(4)如果有一个结点是红色,则它的两个儿子都是黑色。
(5)任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

如下是一棵红黑树:
在这里插入图片描述
从图可以看出NIL不是空指针,而是一个叶子结点。实际操作的时候可以将NIL视为哨兵,这样便于对黑红色进行操作。红黑树的操作主要是对内部结点操作,因为内部结点存储了关键字的值。书中为了便于讨论,忽略了叶子结点的,如是上图红黑树变成如下图所示:
在这里插入图片描述
书中给出了黑高度的概念:从某个结点x出发(不包含该结点)到达一个叶子结点的任意一条路径上,黑色结点的个数称为该结点的黑高度。由红黑树的性质(5)可知,从该结点出发的所有下降路径都有相同的黑色结点个数。红黑树的黑高度定义为其根结点的黑高度。
  书中给出了一个引理来说明为什么红黑树是一种好的查找树,并对引理进行了证明(采用归纳法进行证明的,需要很强的归纳推理知识,正是我的不足之处,看书的痛苦在于此)。
引理:一棵有n个内结点的红黑树的高度之多为2lg(n+1)。
关于红黑树的一些疑问:
1.红黑树和平衡二叉树(AVL树)的区别与联系
2.红黑树为啥增加插入的节点要置为红节点
3.红黑树在哪里用的比较多,它相对其他的平衡搜索树有何优点?
对应解答:
1、 红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。

红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构 能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。

平衡二叉树严格的高度控制,左右子树的高度差距不能大于1,这会导致插入删除有较多的旋转调整的步骤,并且其树的高度一定是lgn,也就是说这种树的平均时间复杂度就是O(lgn)
但是红黑树,只用保证任何节点到叶子节点均包含相同数目的黑色节点,通过颜色来约束树的形状,主要有以下几个特征:

1.红黑树的高度总是低于2lg(n+1),n为节点的个数
2.红黑树的时间复杂度为O(h(x))= O(2lg(n+1))=O(lgn)
3.任何插入删除导致的不平衡都可以在三次旋转操作内完成平衡,降低了实现的复杂度。

查找较多的可以选择用AVL Tree、插入删除较多的可以用RBTree。
2、我们可以看插入的点为红色和黑色时可能导致的情况:
假设新插入的节点是黑色,那么不管原来的红黑树是什么样的,这样一定会破坏平衡,因为原来的树是平衡的,现在在这一条路径上多了一个黑色节点,必然违反了性质5(不记得的时候多看几遍性质,并理解是最好的)。
假设是新插入的节点是红色,那么既有有可能会破坏平衡,也有可能不破坏平衡。
破坏平衡主要可能是违反了性质4(如果一个节点为红色,那么它的两个孩子节点必须为黑色),比如下图中,新插入的一个key=21的节点,并把该节点置为红色,由于它的父节点22为红色,导致红色节点的孩子中出现了红色节点。
不破坏平衡的情况也有可能出现,比如下图中,如果我插入一个key=0的节点。把0这个节点置为红色,并不会影响原来树的平衡,因为0的父节点是黑色。

如下图:
在这里插入图片描述

2、红黑树的应用

红黑树是一种近似平衡的二叉搜索树,通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。用于搜索时,插入删除次数多的情况下我们就用红黑树来取代AVL。
红黑树的应用比较广泛:

· 广泛用在C++的STL中。map和set都是用红黑树实现的。
· 著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块。
· epoll在内核中的实现,用红黑树管理事件块
· nginx中,用红黑树管理timer等
· Java的TreeMap实现

3、红黑树常见的面试题

1.stl中的set底层用的什么数据结构?
2.红黑树的数据结构怎么定义的?
3.红黑树有哪些性质?
4.红黑树的各种操作的时间复杂度是多少?
5.红黑树相比于BST和AVL树有什么优点?
6.红黑树相对于哈希表,在选择使用的时候有什么依据?
7.如何扩展红黑树来获得比某个结点小的元素有多少个?
8.扩展数据结构有什么步骤?
9 为什么一般hashtable的桶数会取一个素数?

详细解答:

1.STL中的set底层用的什么数据结构?
红黑树。

2.红黑树的数据结构怎么定义?

 1. enum Color  
 2. {  
 3.           RED = 0,  
 4.           BLACK = 1  
 5. };  
 6.   
 7. struct RBTreeNode  
 8. {  
 9.            struct RBTreeNode*left, *right, *parent;  
 10.            int   key;  
 11.            int data;  
 12.            Color color;  
 13. };  

3.红黑树有哪些性质?
一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:

1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。 5)对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。

4.红黑树的各种操作的时间复杂度是多少?
能保证在最坏情况下,基本的动态集合操作的时间均为O(lgn)。

5.红黑树相比于BST和AVL树有什么优点?
红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。

相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。

红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,AVL树在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,但是他们的查找效率都是O(logN),所以红黑树应用还是高于AVL树的。

实际上插入 AVL 树和红黑树的速度取决于你所插入的数据.如果你的数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的。

6.红黑树相对于哈希表,在选择使用的时候有什么依据?
权衡三个因素:== 查找速度, 数据量, 内存使用,可扩展性。==

总体来说,hash查找速度会比map快,而且查找速度基本和数据量大小无关,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n) 小,hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash。但若你对内存使用特别严格, 希望程序尽可能少消耗内存,那么一定要小心,hash可能会让你陷入尴尬,特别是当你的hash对象特别多时,你就更无法控制了,而且 hash的构造速度较慢。

红黑树并不适应所有应用树的领域。如果数据基本上是静态的,那么让他们待在他们能够插入,并且不影响平衡的地方会具有更好的性能。如果数据完全是静态的,例如,做一个哈希表,性能可能会更好一些。

在实际的系统中,例如,需要使用动态规则的防火墙系统,使用红黑树而不是散列表被实践证明具有更好的伸缩性。Linux内核在管理vm_area_struct时就是采用了红黑树来维护内存块的。
红黑树通过扩展节点域可以在不改变时间复杂度的情况下得到结点的秩。

7.如何扩展红黑树来获得比某个结点小的元素有多少个?
这其实就是求节点元素的顺序统计量,当然任意的顺序统计量都可以需要在O(lgn)时间内确定。
在每个节点添加一个size域,表示以结点 x 为根的子树的结点树的大小
则有size[x] = size[[left[x]] + size [right[x]] + 1;,这时候红黑树就变成了一棵顺序统计树。

利用size域可以做两件事:

1). 找到树中第i小的结点;

 1. OS-SELECT(x;,i)  
 2. r = size[left[x]] + 1;  
 3. if i == r  
 4.      return x  
 5. elseif i < r  
 6.      return OS-SELECT(left[x], i)  
 7. else return OS-SELECT(right[x],  i)  

思路:size[left[x]]表示在对x为根的子树进行中序遍历时排在x之前的个数,递归调用的深度不会超过O(lgn);

2).确定某个结点之前有多少个结点,也就是我们要解决的问题;

 1. OS-RANK(T,x)  
 2. r = x.left.size + 1;  
 3. y = x;  
 4. while y != T.root  
 5.          if y == y.parent.right  
 6.                  r = r + y.parent.left.size +1  
 7.          y = y.p  
 8. return r  

思路:x的秩可以视为在对树的中序遍历中,排在x之前的结点个数加上一。最坏情况下,OS-RANK运行时间与树高成正比,所以为O (lgn).

8.扩展数据结构有什么步骤?

1).选择基础数据结构;
2).确定要在基础数据结构种添加哪些信息;
3).验证可用基础数据结构上的基本修改操作来维护这些新添加的信息;
4).设计新的操作。

9 为什么一般hashtable的桶数会取一个素数
设有一个哈希函数
H( c ) = c % N;
当N取一个合数时,最简单的例子是取 2 n 2^n 2n,比如说取 2 3 = 8 2^3=8 23=8,这时候
H( 11100(二进制) ) = H( 28 ) = 4
H( 10100(二进制) ) = H( 20 )= 4

这时候c的二进制第4位(从右向左数)就”失效”了,也就是说,无论第c的4位取什么值,都会导致H( c )的值一样.这时候c的第四位就根本不参与H( c )的运算,这样H( c )就无法完整地反映c的特性,增大了导致冲突的几率.

取其他合数时,都会不同程度的导致c的某些位”失效”,从而在一些常见应用中导致冲突.
但是取质数,基本可以保证c的每一位都参与H( c )的运算,从而在常见应用中减小冲突几率

4、红黑树的代码实现

<2> 自平衡的二叉查找树(AVL)

平衡二叉树,一般是用平衡因子差值决定并通过旋转来实现,左右子树树高差不超过1,那么和红黑树比较它是严格的平衡二叉树,平衡条件非常严格(树高差只有1),只要插入或删除不满足上面的条件就要通过旋转来保持平衡。由于旋转是非常耗费时间的。我们可以推出AVL树适合用于插入删除次数比较少,但查找多的情况。

应用相对其他数据结构比较少。windows对进程地址空间的管理用到了AVL树。
具体参考:平衡搜索树-AVLTree

<3> B-与B+树

1、B-树和B+树的定义:

B-树是一种多路搜索树(并不是二叉的),其定义如下:

1.定义任意非叶子结点最多只有M个儿子,且M>2;
2.根结点的儿子数为[2, M];
3.除根结点以外的非叶子结点的儿子数为[M/2, M];
4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
5.非叶子结点的关键字个数=指向儿子的指针个数-1;
6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的 子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
8.所有叶子结点位于同一层;
在这里插入图片描述
B+树是B-树的变体,也是一种多路搜索树:

1.其定义基本与B-树同,除了:
2.非叶子结点的子树指针与关键字个数相同;
3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
5.为所有叶子结点增加一个链指针;
6.所有关键字都在叶子结点出现;
在这里插入图片描述

2、B+树的应用

B树,B+树:它们特点是一样的,是多路查找树,一般用于数据库中做索引,因为它们分支多层数少,因为磁盘IO是非常耗时的,而像大量数据存储在磁盘中所以我们要有效的减少磁盘IO次数避免磁盘频繁的查找。
B+树是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文件系统而生的。

B+树相对B树磁盘读写代价更低:因为B+树非叶子结点只存储键值,单个节点占空间小,索引块能够存储更多的节点,从磁盘读索引时所需的索引块更少,所以索引查找时I/O次数较B-Tree索引少,效率更高。而且B+Tree在叶子节点存放的记录以链表的形式链接,范围查找或遍历效率更高。Mysql InnoDB用的就是B+Tree索引。

具体参考:
1、B+树图文详解
2、 B树、B-树、B+树与红黑树

<4> 单词查找树(Trie)

又名单词查找树,一种树形结构,常用来操作字符串。它是不同字符串的相同前缀只保存一份。

相对直接保存字符串肯定是节省空间的,但是它保存大量字符串时会很耗费内存(是内存)。
类似的有:前缀树(prefix tree),后缀树(suffix tree),radix tree(patricia tree, compactprefix tree),crit-bit tree(解决耗费内存问题),以及前面说的double array trie。

前缀树:字符串快速检索,字符串排序,最长公共前缀,自动匹配前缀显示后缀。
后缀树:查找字符串s1在s2中,字符串s1在s2中出现的次数,字符串s1,s2最长公共部分,最长回文串。

trie 树的一个典型应用是前缀匹配,比如下面这个很常见的场景,在我们输入时,搜索引擎会给予提示。还有比如IP选路,也是前缀匹配,一定程度会用到trie:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Albert_YuHan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值