在 STL 的容器中两大阵营 (vector deque list forward_list) 顺序容器 与 关联容器 (map set unorder_map unorder_set) 。
关联容器指的是以关键字匹配指定数据的一种关联数据结构的集合, 在关联容器中通过关键字去查找对应的数据是非常块的, 下面我们会彻底地去剖析其底层实现机制 : 红黑树。
介绍红黑树之前,容许我介绍树的概念。
1. 树(tree), 在计算机科学中,是十分基础的数据结构:
树的结构特点:
2. 二叉搜索树
我们提到二叉树的特点是: 任意节点仅允许有两个孩子。
二叉搜索树:
如图所示:我们找一个值,只需要从根节点出发,与根节点去比较,若大于根节点去右子树找,若小于去左子树中找(每次淘汰一半的子树),这样只需要 O(logn)就能找到你要找的元素。
插入: 从根节点出发,遇到键值大的就向左, 遇到键值小的就向右,一直到尾端,即为插入点。
删除:
删除 12 节点:
删除 10 节点:
也许因为输入值不够随机,或者因为经过某些插入或删除操作,二叉搜索树可能失去平衡,造成搜索效率低落的情况(退化成链表),如图:
”平衡“ 的大致意义是:没有任何一个节点过深(深度过大)。
为了保证树形的平衡,衍生出了各种解决该问题的特殊结构: AVL_tree, RB_tree 等等。
AVL tree 是一个”加上了额外平衡条件“ 的二叉搜索树,其平衡条件的建立是为了保证整棵树的深度为 O(logN)
接下来让我们了解一下额外平衡条件到底是怎么操作的:
如图所示 AVL tree:
插入了节点 11 后,灰色节点违反了 AVL tree 的平衡条件,由于只有 “插入点至根节点”路径上的各节点可能改变平衡状态,因此,只需要调整其中最深的那个节点,便可以使得整个树重新获取平衡。
此时我们看到 18 节点的两颗子树因为 11 的插入平衡被破坏,而 18 这个节点就是从插入点到根路径上的最深节点X, 因此我们可以轻而易举地将情况分为以下四种:
情况 左左,和右右,可以看作是 外侧插入,使用单旋转即可调整,
而 右左,左右 则称为内侧插入, 可以采用双旋转操作调整解决。
旋转: 将旋转点(x)以某个方向进行偏移一单位,若左旋,则其右子节点将变为其 x 的父节点, 由于 右子节点的左子树必须挂在其左侧,所以旋转后原右子节点的左子树必须挂在旋转点(x) 的右侧,才能继续维持二叉搜索树的性质。
单旋转的精华是:
左旋:将旋转的根节点的右孩子的左孩子变成根节点的左孩子的右子树,再将根节点向左旋转,令根节点的右孩子为新根,原根节点成为现根节点的左孩子。
右旋:将旋转的根节点的左孩子的右子树变成根节点的右孩子的左子树,再将根节点向右旋转,令根节点的左孩子为新根,原根节点成为现根节点的右孩子。
双旋转的精华是:
两次单旋转
颇具历史并且被广泛应用的自平衡二叉搜索树是 RB-tree (红黑树),平衡的概念指叶子节点间的最大深度不能超过1,但红黑树不是这样, 它以牺牲部分的平衡性换取了操作上/旋转次数的降低, 插入操作旋转次数不超过2,删除操作不超过3,它不仅仅是一个二叉搜索树,它必须满足以下规则:
1*该路径上的黑色节点数量 <= 任意路径长度 <= 2 * 该路径上的黑色节点数目(红色节点和黑色节点一样多)
从上述特点得知:
在插入新节点时, 以二叉查找树的方法增加节点并标记其为红色(设置黑色违反性质 5)
下面是否要进行调整和如何调整取决于其临近节点的颜色,同人类的家族树一样,我们将使用术语叔父节点来指一个节点的父节点的兄弟节点。
注意:
我们设定将要插入的节点标为 N, N 的父节点标为 P, N 的祖父节点为 G, N 的叔父节点为 U
通过下面操作,能找到一个节点的叔父节点和祖父节点
node *grandparent(node *n) {
return n->parent->parent;
}
node *uncle(node *n) {
if(n->parent == grandparent(n)->left)
return grandparent(n)->right;
else
return grandparent(n)->left;
}
情形 1: 新节点 N 位于树的根上,没有父节点,在这种情况下,我们将其绘制成黑色节点以满足性质2, 因为它在每个路径上对黑节点数目增加 1, 性质 5 也是符合的。
void insert_case1(node *n) {
if(n->parent == NULL)
n->color = BLACK;
else
insert_case2(n)
}
情形2: 新节点的父节点 P 是黑色, 所以性质 4 没有失效(新节点的确是红色的),这种情况下红黑树是有效的,性质 5 也没有受到威胁,尽管新节点 N 有两个黑色叶子子节点, 但由于新节点 N 是红色, 通过它的每个子节点的路径就都有通过它所取代的黑色的叶子的路径同样数目的黑色节点,所有依然满足这个性质。
void insert_case2(node *n) {
if(n->parent->color == BLACK)
return;
else
insert_case3(n);
}
注意: 在下列情形中,我们假定新节点的父节点为红色,所以其有祖父节点,如果父节点是根节点,那父节点就应当是黑色,所以新节点总有一个叔父节点,尽管在情形 4 和 5 它可能是叶子节点。
情形3: 父节点 P 和 叔父节点 U 都是红色(此时新插入节点N 作为P 的左子节点或右子节点都属于情形三这里我们演示 N 作为 P 左子节点的情形)则我们可以将它们两个重新绘制为黑色并重绘祖父节点 G 为红色(用来保持性质 5).现在我们的新节点 N 有了一个黑色的父节点P, 因为通过父节点P或叔父节点 U 的任何路径都必定通过祖父节点 G,在这些路径上的黑节点数目没有改变,
注意: 红色的祖父节点 G 的父节点也有可能是红色的, 这就违反了性质 4, 为了解决这个问题,我们在祖父节点 G 上递归地进行情形 1 的整个过程。(把 G 当成是新加入的节点进行各自情况的检查。)
void insert_case3(node *n) {
if(uncle(n) != NULL && uncle(n)->color == RED) {
n->parent->color = BLACK;
uncle(n)->color = BLACK;
grandparent(n)->color = RED;
insert_case1(grandparent(n));
}
else
insert_case4(n);
}
注意:在余下的情况下,我们假定父节点 P 是其父亲 G 的左子节点,如果是右子节点,情形 4 和情形 5的左和右应当对调。
情形4:父节点 P 是红色而叔父U是黑色或没有, 并且新节点 N 是其父节点 P 的右子节点而父节点 P 又是其父节点的左子节点,在这种情形下,我们进行一次左旋转调换新节点, 在这种情况下我们进行一次左旋转调换新节点和其父节点的角色,
接着, 我们按情形 5 处理以前的 父节点 P 以解决性质 4 失效的问题, 注意这个改变会导致某些路径通过它们以前不通过的新节点 N ,或不通过节点 P,但由于这两个节点都是红色,所以性质 5 仍然有效。
void insert_case4(node *n) {
if(n == n->parent