二叉搜索树、AVL树、红黑树总结

一、二叉搜索树

1.1、二叉搜索树概念
    二叉搜索树又称二叉排序树,它或者是一颗空树,或者是一颗具有以下性质的二叉树:
  (1) 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
  (2) 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
  (3) 它的左右子树也分别为二叉搜索树。
在这里插入图片描述
1.2、二叉搜索树的操作
 (1) 二叉搜索树的查找
在这里插入图片描述
 (2) 二叉搜索树的插入
    插入的具体过程如下:
   a. 数为空,则直接插入
在这里插入图片描述
   b. 数不为空,按二叉搜索树性质查找插入位置,插入新节点
在这里插入图片描述
 (3) 二叉搜索树的删除
    首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
    a. 要删除的结点无孩子结点
    b. 要删除的结点只有左孩子结点
    c. 要删除的结点只有右孩子结点
    d. 要删除的结点有左、右孩子结点
    看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
      情况b: 删除该结点且使被删除节点的双亲结点(其实就是父节点,只是一个节点)指向被删除节点的左孩子结点。
      情况c: 删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点。
      情况d: 在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题。
1.3、二叉搜索树的性能
    插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
    对有n个结点的二叉搜索树,二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
    对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述

二叉搜索树代码:https://github.com/xiao-hao-hao/learning-C/blob/master/C%2B%2B/STL/bst_avl/bst_avl/test.cpp

二、AVL树

2.1、AVL树的概念
    二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
    一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树(两者都满足):
     (1) 它的左右子树都是AVL树;
     (2) 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。
在这里插入图片描述
2.2、AVL树的旋转
    如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。

AVL树的插入
    在向一颗本来是高度平衡的AVL树中插入一个新节点时,如果树中的某个节点的平衡因子的绝对值|bf|>1,则出现了不平衡,需要做平衡化处理。
    设新插入的节点为p,从节点p到根节点的路径上,每个节点为根的子树的高度都可能增加1,因此在每执行一次二叉搜索树的插入运算后,都需要从新插入的节点p开始,沿该节点的插入路径向根节点方向回溯,修改各节点的平衡因子,调整子树的高度,恢复被破坏的平衡性质。
    新节点p的平衡因子为0。现在考察它的父节点pr,若p是pr的右子女,则pr的平衡因子增加1,否则pr的平衡因子减少1,按照修改后pr的平衡因子值有三种情况:
    1、节点pr的平衡因子为0,说明刚才是在pr的较矮的子树上插入了新节点,节点pr处平衡,且高度没有增减,此时从pr到根的路径上各节点为根的子树高度不变,从而各个节点的平衡因子不变,可以结束重新平衡化的处理,返回主程序。
    2、节点pr的平衡因子绝对值bf=1,说明插入前pr的平衡因子是0,插入新节点后,以pr为根的子树没有失去平衡,不需要平衡化旋转,但该子树的高度增加,还需从节点pr向根的方向回溯,继续考察节点pr的父节点的平衡状态。
    3、节点pr的平衡因子的绝对值bf=2,说明新节点在较高的子树上插入,造成了不平衡,需要做平衡化旋转,分两种情况考虑:
      3.1 若节点pr的bf=2,说明右子树高,结合其右子女q的bf分别处理:
        1、若q的bf=1,执行左单旋转;
        2、若q的bf=-1, 执行先右后左双旋转 >
      3.2 若节点pr的bf=-2,说明左子树高,结合其左子女q的bf分别处理:
        1、若q的bf=-1,执行右单旋转; /
        2、若q的bf= 1, 执行先左后右双旋转 <

AVL树的删除
    若删除节点后破坏了AVL树的平衡,则需要进行平衡旋转。
    1和2与bst树的删除过程一样。
    1、如果被删除节点p有两个子女节点,首先搜索p在中序下的直接前驱q(也可以是直接后继,也就是左树的最大值或右树的最小值),再把节点q的内容传送给节点p,问题转移到删除节点q,把节点q当做被删除节点p,它没有子女节点。
    2、如果被删除节点p最多只有一个子女q,可以简单的把p的父节点pr中原来指向p的指针指到q,如果节点p没有子女,p的父节点pr的相应指针为NULL。
考察节点q的父节点pr,若q是pr的左子女,则pr的平衡因子增加1,否则减少1,根据修改后的pr的平衡因子,按三种情况分别进行处理:
      (1)pr的平衡因子原来为0,在它的左子树或右子树被缩短后,则它的平衡因子改为1或-1,由于以pr为根的子树高度没有改变,从pr到根节点的路径上所有节点都不需要调整,此时可结束本次删除的重新平衡过程。
      (2)节点pr的平衡因子原不为0,且较高的子树被缩短,则pr的平衡因子改为0,此时pr为根的子树平衡,但其高度减1,为此需要继续考察节点pr的父节点的平衡状态。
      (3)节点pr的平衡因子原不为0,且较矮的子树被缩短,则在节点pr发生不平衡,为此需要进行平衡化的旋转来恢复平衡,令pr的较高的子树的根为q(该子树是未被缩短的子树),根据q的平衡因子,有3中平衡化操作:
        1、如果q的平衡因子为0,执行一个单旋转来恢复节点pr的平衡(因为一个单旋转就可以把引起不平衡的两个结点的影响全部消除,双旋转会引起新的树不平衡),pr平衡因子变成-1,q平衡因子变成1,由于平衡旋转后以q为根的子树的高度没有发生改变,从q到跟的路径上所有结点的平衡因子不变,此时可以结束重新平衡过程。
        2、如果q的平衡因子与pr的平衡因子正负号相同,则执行一个单旋转来恢复平衡,节点pr和q的平衡因子均改为0,由于经过平衡旋转后节点q的子树高度降低1,故需要继续延插入路径上考查节点q的父节点的平衡状态,即将当前考查节点向跟节点方向上移。
        3、如果pr与q的平衡因子正负号相反,则执行一个双旋转来恢复平衡(必须用双旋转才能消除结点引起的不平衡影响)。新的根节点的平衡因子置为0,其他节点的平衡因子相应处理,同时由于经过平衡化处理后子树的高度降低1,还需要考查它的父节点,继续向上层进行平衡化工作。

2.3、AVL树的性能
    AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2(N) 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
AVL树代码:https://github.com/xiao-hao-hao/learning-C/blob/master/C%2B%2B/STL/AVL/AVL/test.cpp

三、红黑树

3.1、红黑树的概念
    红黑树是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
    红黑树的性质:一头一脚黑,黑同红不连:
      1、每个节点不是红色就是黑色;
      2、根节点和叶子节点都是黑的;
      3、对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点;
      4、两个红色节点不能直接相连;
      5、每个叶子结点都是黑色的(此处的叶子结点指的是空结点)。
    红黑树其最长路径中节点个数不会超过最短路径节点个数的两倍。 因为最长路径和最短路径黑色节点相同,但是即使最短路径没有红节点,最长路径红节点达到最大数量,则最长路径上的红节点也比起黑节点少一个,所以最长路径中节点个数不会超过最短路径节点个数的两倍。
    在节点的定义中,为什么要将节点的默认颜色给成红色的。 如果节点默认给成黑色,那么每次插入都会改变(增加)插入路径中黑色节点的个数,也就是说每次插入节点都需要我们对红黑树进行调整。如果默认节点是红色,那么插入新节点后如果父节点是黑色,我们不需要调整,否则才去调整数,所以默认红色可以减少调整的频率。
    红黑树可能产生不平衡状态(高度相差1以上),这倒没关系,因为RBTree的平衡性本来就比AVLTree弱。然而RBTree通常能够导致良好的平衡状态,是的,经验告诉我们,RBTree的搜索平均效率和AVLTree几乎相等。
在这里插入图片描述
3.2、红黑树的旋转

红黑树树的插入
    新插入的节点为X,X的父节点为P,P的父节点为G,P的兄弟节点为S,G的父节点为GG。我们新插入的节点X是红色,若P为黑色,则不用调整,若P为红色,则G必为黑色,根据X的插入位置及外围节点(S和GG)的颜色,有如下三种情况:
    状况1:S为黑且X为外侧插入,对此情况,我们先对P、G做一次单旋转,再更改P、G颜色,即可重新满足红黑树的规则。
    注意: 此时可能产生不平衡状态(高度相差1以上),这倒没关系,因为RBTree的平衡性本来就比AVLTree弱。然而RBTree通常能够导致良好的平衡状态,是的,经验告诉我们,RBTree的搜索平均效率和AVLTree几乎相等。
    状况2:S为黑且X为内侧插入,对此情况,我们先对P、X做一次单旋转,并更改G、X颜色,再将结果对G做一次单旋转,即可再次满足红黑树规则。
    状况3:S为红,对此情况,先改变P、G、S的颜色,此时如果GG为黑,一切搞定。如果GG为红,还得持续往上做,直到不再有父子连续为红的情况。

红黑树的删除
对可能出现的情形讨论:
    删除红黑树中一个结点,删除的结点是其子结点状态和颜色的组合。子结点的状态有三种:无子结点、只有一个子结点、有两个子结点。颜色有红色和黑色两种。所以共会有6种组合。
    组合1:被删结点无子结点,且被删结点为红色
    此时直接将结点删除即可,不破坏任何红黑树的性质.
    组合2:被删结点无子结点,且被删结点为黑色
    处理方法略微复杂,稍后再议。
    组合3:被删结点有一个子结点,且被删结点为红色
在这里插入图片描述
    这种组合是不存在的,如图假如被删结点node只有一个有值的子结点value,而以value为根结点的子树中,必然还存在null结点,如此不符合红黑树的性质5,对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
    组合4:被删结点有一个子结点,且被删结点为黑色
在这里插入图片描述
    这种组合下,被删结点node的另一个子结点value必然为红色,此时直接将node删掉,用value代替node的位置,并将value着黑即可。
    组合5&6:被删结点有两个子结点,且被删结点为黑色或红色
    当被删结点node有两个子结点时,先要找到这个被删结点的后继结点successor,然后用successor代替node的位置,此时相当于successor被删。
    因为node有两个子结点,所以successor必然在node的右子树中,必然是下图两种形态中的一种。
在这里插入图片描述
若是(a)的情形,用successor代替node后,相当于successor被删,若successor为红色,则变成了组合1;若successor为黑色,则变成了组合2或4。
若是(b)的情形,用successor代替node后,相当于successor被删,若successor为红色,则变成了组合1;若successor为黑色,则变成了组合2或4。
综上: 若被删结点是组合1或组合4的状态,很容易处理;被删结点不可能是组合3的状态;被删结点是组合5&6的状态,将变成组合1或组合2或组合4。
再议组合2:被删结点无子结点,且被删结点为黑色
    因为删除黑色结点会破坏红黑树的性质5,所以为了不破坏性质5,在替代结点上额外增加一个黑色,这样不违背性质5而只违背性质1,每个结点或是黑色或是红色。此时将额外的黑色移除,则完成删除操作。
在这里插入图片描述
然后再结合node原来的父结点father和其兄弟结点brother来分析。
情形一:
brother为黑色,且brother有一个与其方向一致的红色子结点son, 所谓方向一致,是指brother为father的左子结点,son也为brother的左子结点;或者brother为father的右子结点,son也为brother的右子结点。
在这里插入图片描述
图 ( c ) 中,白色代表随便是黑或是红,方形结点除了存储自身黑色外,还额外存储一个黑色。将brother和father旋转,并重新上色后,变成了图(d),方形结点额外存储的黑色转移到了father,且不违背任何红黑树的性质,删除操作完成。
图( c )中的情形颠倒过来,也是一样的操作。
情形二
brother为黑色,且brother有一个与其方向不一致的红色子结点son
在这里插入图片描述
图(e)中,将son和brother旋转,重新上色后,变成了图(f),情形一。
图(e)中的情形颠倒过来,也是一样的操作。
情形三
brother为黑色,且brother无红色子结点
此时若father为红,则重新着色即可,删除操作完成。如图下图(g)和(h)。
在这里插入图片描述
此时若father为黑,则重新着色,将额外的黑色存到father,将father作为新的结点进行情形判断(不删除),遇到情形一、情形二,则进行相应的调整,完成删除操作;如果没有,则结点一直上移,直到根结点存储额外的黑色,此时将该额外的黑色移除,即完成了删除操作。
在这里插入图片描述
情形四
brother为红色,则father必为黑色。
在这里插入图片描述

图(i)中,将brother和father旋转,重新上色后,变成了图(j),新的brother变成了黑色,这样就成了情形一、二、三中的一种。如果将son和brother旋转,无论怎么重新上色,都会破坏红黑树的性质4或5,例如图(k)。
图(i)中的情形颠倒过来,也是一样的操作。

四、树型结构的关联式容器(map、set、multimap、multiset)

    根据应用场景的不同,STL总共实现了两种不同结构的关联式容器:树形结构与哈希结构。树形结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用红黑树作为其底层结构,容器中的元素是一个有序的序列。
    vector、list、deque、forward_list(C++11)等这些容器统称为序列式容器,因为其底层的数据结构为线性序列,里面存储的是元素本身。
    关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。
    键值对用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词和与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该英文单词,在词典中就可以找到与其对应的中文含义。
4.1、set
    set是按照一定次序存储元素的容器,在set中,元素的value同时也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。在内部,set中的元素总是按照其内部比较对象所指示的特定排序准则进行排序。set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代,set在底层是用红黑树实现的。
注意:
    1. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。
    2. set中插入元素时,只需要插入value即可,不需要构造键值对。
    3. set中的元素不可以重复,因此可以使用set进行去重。
    4. 使用set的迭代器遍历set中的元素,可以得到有序序列
    5. set中的元素默认按照小于来比较
    6. set中查找某个元素,时间复杂度为log2(N)
    7. set中的元素不允许修改,修改之后不满足二叉搜索树性质了,调整起来麻烦
    8. set中的底层是红黑树

4.1、map
    map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair : typedef pair value_type;。在内部,map中的元素总是按照键值key进行比较排序的。map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。map底层结构是红黑树。
    map中的元素是键值对,map中的key是唯一的,并且不能修改,默认按照小于的方式对key进行比较。map中的元素如果用迭代器去遍历,可以得到一个有序的序列。map的底层为红黑树,查找效率比较高 O(log2(N))。map的[ ]实际是插入查找,如果没有找到会自动为其插入一个值T()。因为键值是唯一的,所以在插入时,如果该键值已经存在,则会插入失败(insert)。但是[ ]可以改一个键对应的键值。
4.2、multiset
    multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。在内部,multiset中的元素总是按照其内部比较规则所指示的特定排序准则进行排序。multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。multiset底层结构为红黑树。
注意:
    1. multiset中在底层中存储的是<value, value>的键值对
    2. multiset的插入接口中只需要插入即可
    3. 与set的区别是,multiset中的元素可以重复,set中value是唯一的
    4. 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列
    5. multiset中的元素不能修改
    6. 在multiset中找某个元素,时间复杂度为O(log2(N))
    7. multiset的作用:可以对元素进行排序

4.3、multimap
    multimap是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key, value>,其中多个键值对之间的key是可以重复的。在multimap中,通常按照key排序和唯一地标识元素。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,value_type是组合key和value的键值对:typedef pair<const Key, T> value_type;。 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定排序标准对key进行排序的。 multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于key有序的序列。multimap在底层用红黑树来实现
注意:
    1. multimap中的key是可以重复的。
    2. multimap中的元素默认将key按照小于来比较。
    3. multimap中没有重载operator[]操作。
    4. 使用时与map包含的头文件相同:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值