• 盘点常用的搜索树


    树的典型应用有很多,比如计算机的文件系统就是一棵树,根目录就是根节点。树的重要应用之一就是搜索树,搜索树通常分为二叉搜索树和多路搜索树。

    二叉搜索树

    二叉搜索树是一颗有序的树,每个结点不小于其左子树任意结点的值,不大于右子树任意结点的值。二叉搜索树还有一个有趣的特性,它的中序遍历得到的是有序数列。

    二叉搜索树能提高搜索的效率,搜索次数最多是树的深度次,最少能到log(n)。

    搜索树有搜索,插入,删除等操作。搜索即搜索整棵树,查找是否有匹配的节点;还有搜索最大值和最小值操作,最大值在树的最右侧,最小值在最左侧,因此都很好实现。

    treenode find(tree T,int val){   /*搜索操作*/
      if(T==NULL)  return NULL;
      if(T->data > val)
        return find(T->left,val);
      else if(T->data < val)
        return find(T->right,val);
      else
        return T;
    }

    插入,也是采用递归实现,插入的结点都放到了叶子节点。

    treenode insert(tree T,int val){
      if(T==NULL) {
        T=(treenode)malloc(sizeof(struct node));
        T->data=val;
        T->right=T->left=NULL;
      }
      else if(T->data >= val)
        T->left = insert(T->left,val);
      else
        T->right = insert(T->right,val);
      return T;
    }

    删除,这种操作比较复杂,因为可能会破坏BST的规则,需要进行调整。删除操作分几种情况:

    1)当删除的结点是叶子节点,直接删除即可,如删除下图中的7结点;

    2)当删除非叶节点,且该节点只有一个子节点时,我们可以删除该节点,并让其父节点连接到它的子节点即可;如删除8结点,直接让5结点连接到7即可;

    3)当非叶节点,且有两个子节点时,可以删除该节点,然后抓出左子树中最大的元素(或者右树中最小的元素),来补充被删元素产生的空缺。但抓出的元素可能也不是叶节点,所以它所产生的空缺需要它的子树某个结点补充…… 直到最后用来填充的是一个叶节点。上述过程可以递归实现。如删除2结点,可以用1替换或者用3替换。

    通常删除使用递归实现,如果非递归需要记录父节点,并且被删结点是根节点需要特殊处理,比较麻烦。

    AVL树

    AVL树是用发明它的人的名字命令的,AVL树的任意节点的左子树深度和右子树深度相差不超过1。

    因此AVL树是一个平衡的树,不会偏向一侧,保证了搜索的时间复杂度为O(logn)。AVL树的大部分操作和二叉搜索树相同,只有插入和删除操作大有不同,因为可能会破坏平衡,我们需要恢复平衡。树的平衡需要理解旋转操作。

    (网上很多关于AVL插入调整的博客,删除的博客较少,下面借鉴Vamei大神的博客,讲讲插入操作。)

    先看看失衡时如何调整为不失衡的例子:

    1)一次单旋调整平衡

    当插入5结点时,2结点失衡,左子树深度0,右子树为2。结点2的右边超重,因此我们进行左旋,旋转中心为结点4,旋转后4为根节点,如图,恢复了AVL性质。这样的旋转称为‘左单旋’。

    同样的,如果都是指向左边,那就是左边超重,进行‘右单旋’。

    2)两次单旋调整平衡

    如图如果插入的是结点3,那么旋转中心为结点3,先进行一次右单旋,再以3为中心进行一次左单旋即恢复平衡。这种旋转称为‘右左旋转’,同样还有‘左右旋转’。

    对某个结点进行单旋的结果可以一般化为下图:

     

    总结插入的一般流程:

    1.按照常规BST的插入,将节点插为叶节点;

    2.从插入节点开始回溯,寻找第一个出现失衡的结点;

    3.找到失衡结点(暂称为结点A)失衡的一侧,是左右哪一侧导致的失衡;

    4.失衡结点的失衡侧的根节点(暂称为结点B),判断B节点的左右子树哪个更深;

    5.如果步骤3和4的方向一致,均为左(右),则以B结点进行一次右(左)单旋;如果方向不一致,如步骤3为左侧失衡,步骤4为右侧子树更深,则以结点B的右结点进行两次单旋,先左单旋,再右单旋。

    #练习几个实例:

     具体代码要实现回溯和恢复AVL,因此节点结构需要额外记录父节点和结点深度。代码就不细看了,简单看看Vamei写的恢复AVL树的函数代码:

    /* tr是整个AVL树的根,np是插入节点的位置 */
    TREE recover_avl(TREE tr, position np)  {     
        int myDiff;     
        while (np != NULL) {         
            update_root_depth(np);
            myDiff = depth_diff(np);
                if (myDiff > 1 || myDiff < -1) {
                   if (myDiff > 1) {                 /* left rotate needed */ 
                       if(depth_diff(np->rchild) > 0) {
                            np = left_single_rotate(np);
                       }
                       else {
                            np = left_double_rotate(np);
                       }
                  }
                  if (myDiff < -1) {
                      if(depth_diff(np->lchild) < 0) {
                          np = right_single_rotate(np);
                      }
                      else {
                          np = right_double_rotate(np);
                      }
                  }
                  /* if rotation changes root node */
                  if (np->parent == NULL) tr = np;
                  break;
            }
            np = np->parent;/* backtracking */
        }
        
        return tr;
    }
                

    红黑树

    红黑树也是一种自平衡的二叉查找树,相比于AVL树,没有严格的深度差要求,但是结点多了颜色的特性。红黑树的时间复杂度为O(logn),红黑树应用广泛,Javad的TreeMap、C++STL的map、linux的CFS调度器都是基于红黑树实现的。

    红黑树的特征:

    1. 节点是红色或黑色。

    2. 根节点是黑色。

    3. 所有叶节点都是黑色。(红黑树的叶子节点指的是NULL节点)

    4. 每个红色节点的两个子节点都是黑色。(所有路径上不能有两个连续的红色节点)

    5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

    性质4、5保证了红黑树的深度差最大为1倍,也就是保证了树的平衡,不会倒向一侧。

    红黑树特点:

    · 同AVL树一样,由于有平衡条件限制,插入和删除可能需要调整平衡;

    · 红黑树应用广泛,实际很少用AVL树,因为AVL树对平衡要求过于苛刻,节点变动可能需要很多次旋转才能恢复平衡,而红黑树最多只需要3次旋转(插入最多两次,删除最多3次)就能恢复平衡;

    · 红黑树查询效率略低于AVL树,但是插入删除效率比AVL树高;

    · 红黑树是一种以2叉树表示的平衡2-3-4叉搜索树,只不过3叉和4叉的表示形式被做了限定。

    参考:(找了几篇讲得不错的红黑树博客)

    红黑树介绍(插入删除)

    红黑树-插入和删除

    红黑树揭秘-类比2-3-4叉树

    哈夫曼树

    哈夫曼树是一种节点带权的最优二叉树。最优的意思是该树的带权路径长度最小。

    所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度。

    哈夫曼树主要内容是建树,假设已有n个具有权值的结点,构造一个有n个叶节点的哈夫曼树步骤如下:

    1.将结点看成n棵树组成的森林;

    2.从森林中取出根节点权值最小的两棵树分别作为左右子树,形成新的父节点的权值为子节点权值之和,然后将新树放到森林中;

    3.重复步骤2,直到森林中只剩一棵树。

     

    # 哈夫曼编码

    哈夫曼树的由来,用于变长编码的。哈夫曼编码典型应用就是用于字符编码,计算机中对字符也是采用01编码,我们最容易想到的是每个字符都采用等长的01串进行编码,但哈夫曼的变长编码缩短了编码长度。哈夫曼的思想是出现频率高的字符用更短的编码表示。对每个字符串设定权值,构造一颗哈夫曼树,左侧的路径编号为0,右倾为1,那么字符的哈夫曼编码就等于从根到各个字符路径上01的组合。

    由于哈夫曼树的有效节点都是叶子节点,所以不会产生一个字符的编码等于另一个字符编码前缀的情况。解码方采用同样的哈夫曼树规则即可实现解码。

    哈夫曼编码常用于压缩文件。

    Treap树堆

    Treap = tree + heap,树堆本身是一颗二叉搜索树,每个结点附有不同的优先级,单看优先级形成一个堆,所以树堆其实是树和堆的组合。树堆不需要保证树的深度差,只保证优先级的堆成立。树结点变化时,通过旋转使得保持堆的性质。

    Splay伸展树

    关于数据查询有个90-10规律,就是90%的查找发生在10%的数据上。那如果经常查找的数据放得离根结点近就会提高效率,伸展树就是基于这样的想法实现的。

    伸展树也是一种自平衡的二叉查找树,不过它左右子树的深度差没有规定。只是规定了每次查找完后通过旋转将被查找结点移至根节点,旋转操作可以分为三类,zig,zig-zag,zig-zig操作。伸展树的平均时间复杂度为O(logn).

    最坏情况下,伸展树会恶化为一条单链;伸展树空间效率很高,不需要额外的标记信息,如深度,颜色等;总体效率在搜索树中也很高。

    B树

    B树是一种平衡多路查找树。每个结点可以有多个子节点。

    B树是为文件系统和数据库而生的,因为磁盘访问的IO时间相对于计算时间很长,而每次访问一个结点就要进行一次IO,因此树深度越小,IO代价越小。于是B树诞生了,它可以压缩树的深度到很小,以减少IO次数。

    数据库索引文件有可能很大,关系型数据存储了上亿条数据,索引文件大则上G,不可能全部放入内存中,而是需要的时候换入内存,方式是磁盘页。一般来说树的一个节点就是一个磁盘页。如果使用二叉查找树,那么每个节点存储一个元素,查找到指定元素,需要进行大量的磁盘IO,效率很低。

    而B树解决了这个问题,通过单一节点包含多个data,大大降低了树的高度,大大减少了磁盘IO次数。一个B树的结点通常和一个完整的磁盘页一样大。

    常用于文件系统和部分数据库索引。

    B+树

    和B树很像,不过B+树的数据节点都是叶子节点,内部节点不存储数据。

    常用于MySql数据库的索引。

     搜索树还有很多,如SBT结点平衡搜索树、替罪羊树、区间树、线段树……

    随着技术发展,不断有更多的搜索树被发明出来,还要一直学习呀!

  • 相关阅读:
    程序的局部性原理2
    程序的局部性原理
    ROM
    学习Spring Security OAuth认证(一)-授权码模式
    mybatis*中DefaultVFS的logger乱码问题
    maven生命周期绑定要点
    spring security antMatchers相关内容
    JSTL
    什么是CSS hack?
    Java中获得当前静态类的类名
  • 原文地址:https://www.cnblogs.com/cpcpp/p/13529829.html
Copyright © 2020-2023  润新知