盘点常用的搜索树

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

二叉搜索树

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

二叉搜索树能提高搜索的效率,搜索次数最多是树的深度次,最少能到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结点平衡搜索树、替罪羊树、区间树、线段树……

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

原文地址:https://www.cnblogs.com/cpcpp/p/13529829.html