zoukankan      html  css  js  c++  java
  • 搜索树

    前面写过了向量和链表的结构,前者静态性能好而动态性能差,后者则相反。考虑改进树的结构,得到动态和静态性能都让人满意的数据结构,特别是在查找以及插入删除操作上具有优势。

    查找或搜索(search):从一组数据对象中找出符合条件者的操作。把数据对象,定义为词条的形式(Entry),词条的形式为关键码-值,即为key-value形式,前者作为比较的依据,后者是实际存储的数据。词条必须定义key相关的比较操作,词条的排序就是key的排序。因此,查找的过程和结果,仅仅取决于目标对象的关键码,这种方式称作循关键码访问(call-by-key)

    二叉搜索树

    考虑每个节点互不相等的情况,如果任一节点r的左子树中,所有节点都不大于r,右子树中,所有节点都不小于r,那么这棵树称为二叉搜索树(binary search tree)。

    回想二叉树的中序遍历,总体即为lc-r-rc的顺序。按照如上定义的二叉搜索树,其中序遍历必然是非减的,反过来依然成立,即一棵二叉树是二叉搜索树,当且仅当其中序遍历是非减的。

    下面,给出一个二叉搜索树的模板实现:

     1 template<typename T> class BST :public BinTree<T>
     2 {
     3 protected:
     4     BinNodePosi(T) _hot;//“命中”节点的父亲
     5     BinNodePosi(T) connect34(BinNodePosi(T), BinNodePosi(T), BinNodePosi(T),
     6         BinNodePosi(T), BinNodePosi(T), BinNodePosi(T), BinNodePosi(T));//联结3个节点和4颗子树
     7     BinNodePosi(T) rotateAt(BinNodePosi(T) x);//对x及其父亲、祖父做统一旋转调整
     8 public:
     9     virtual BinNodePosi(T)& search(const T& e);
    10     virtual BinNodePosi(T) insert(const T& e);
    11     virtual bool remove(const T& e);
    12 };

    继承二叉树的大部分操作,只需要重新定义查找、添加、删除操作即可。

    查找

    从二叉搜索树的定义即可发现,查找的过程,只需要从树根出发,根据大小转向左右子树即可。再回想二叉树中的定义,如果一棵树接近于一颗满二叉树,那么查找的操作,几乎与中序遍历顺序的数组二的分查找一模一样。当然,这是一种非常理想的状态,需要二叉树尽可能满。实际上,最坏的时候,二叉搜索树也会退化成链表,查找的效率仅为O(n)。这也是后面要介绍的几种搜索树的着手点:对树增加限制,使得查找更为高效。

    按照上面思路的代码如下:

     1 template<typename T> //在以v为根的BST子树中查找关键码e
     2 static BinNodePosi(T)& searchIn(BinNodePosi(T)& v, const T& e, BinNodePosi(T)& hot)
     3 {
     4     if (equal(v, e)) return v; hot=v;
     5     while (1)
     6     {
     7         BinNodePosi(T) &c = (e < hot->data) ? hot->lc : hot->rc;
     8         if (equal(c, e)) return c; hot = c;
     9     }
    10 }
    11 template<typename T> BinNodePosi(T)& BST<T>::search(const T& e)
    12 {
    13     return searchIn(_root, e, _hot = NULL);
    14 }

    这里同样需要解决二分查找时相同的问题,如果不命中元素应该如何处理。这里,以_hot作为命中节点的父亲,如果查找的结果为NULL,它的父亲也可以指明位置。

    如上面分析的,复杂度最坏可能为O(n),当二叉搜索树退化链表时。

    插入

    插入操作的基础是查找,如果一个关键码已经存在,直接返回即可。如果不存在,返回插入的位置,申请一个新的节点空间并把它加入到二叉搜索树中,实现代码如下:

    1 template<typename T> BinNodePosi(T) BST<T>::insert(const T& e)//将关键码e插入到BST树中
    2 {
    3     BinNodePosi(T)& x = search(e); if (x) return x;//如果e已经存在,直接返回,操作失败
    4     x = new BinNode<T>(e, _hot);//search必然返回要插入的位置
    5     _size++;
    6     updateHeightAbove(x);
    7     return x;
    8 }

    删除

    与插入操作类似,删除操作同样需要先查找。如果关键码不存在,直接返回。不同之处在于,如果删除的节点不是叶节点,那么需要分情况讨论:如果只有左子树或者右子树,直接接入即可;如果两个子树都存在,那么需要把以被删除节点为根节点的子树重新接入原树中。这里,包含三个步骤:把要删除的节点删除,选取合适的节点重新接入,释放被删除的节点。

    事实上,可以简化这个步骤:直接交换要删除的节点与替换的节点的value,然后删除掉交换完的节点并保留要删除的节点,再释放要删除的点。如下图所示。

    这里,确定交换节点的方式,是使用了二叉树的后继。使用后继交换也会造成问题,删除大量元素后,二叉树左侧会高于右侧,造成不平衡。可以考虑等概率用前驱和后继替换解决这个问题。

    template<typename T> bool BST<T>::remove(const T& e)//删除关键码e
    {
        BinNodePosi(T)& x = search(e); if (!x) return false;
        removeAt(x, _hot); _size--;
        updateHeightAbove(_hot);
        return true;
    }
    template<typename T> static BinNodePosi(T) removeAt(BinNodePosi(T)& x, BinNodePosi(T)& hot)
    {
        BinNodePosi(T) w = x;//实际被摘除的节点
        BinNodePosi(T) succ = NULL;
        if (!(x->lc))//如果左孩子为空
            succ = x = x->rc;//直接替换为右子树
        else if (!(x->rc))//如果右孩子为空
            succ = x = x->lc;
        else//左右孩子都存在,选择x的直接后继作为实际摘除的节点(可以画个图来理解)
        {
            w = w->succ();
            swap(x->data, w->data);//交换x与其直接后继的数值
            BinNodePosi(T) u = w->parent;
            ((u == x) ? u->rc : u->lc) = succ = w->rc;//隔离节点w,把w的孩子与原树连接起来
        }
        hot = w->parent;//记录被删除节点的父亲
        if (succ) succ->parent = hot;//将被删除节点的接替者与hot连接
        release(w->data); release(w); //释放被删除的节点
        return succ;//返回后继(接替)
    }

    删除操作的复杂度不超过树的高度。

    平衡二叉搜索树

    如果一棵二叉搜索树的高度小于log2N,那么称作理想平衡树。完全二叉树和满二叉树显然是理想平衡树。

    然而事实上,维护一个理想平衡树是很困难的,叶节点均出现在最低两层的要求过高,维护代价会非常大。因此,引入了适度平衡的概念,即在渐进意义下放松标准的平衡。

    将条件放松为高度渐进地不超过logn,则可以满足这个要求。AVL树,伸展树,红黑树,kd-树都是这样适度平衡的二叉搜索树。

    因此,插入和删除操作都可能局部地破坏这个平衡性,为此,引入等价二叉搜索树的概念,以及一些用来恢复平衡的操作。

    等价二叉树:如果经过某种变换后,二叉树的中序遍历不变,称为等价二叉树。变换可能会造成高度的改变,但是左右层次不会改变。

    事实上,各种平衡二叉搜索树的条件是精妙的,具有局部性,即动态操作会造成局部失衡,但可以在很短的时间内恢复。

    旋转

    旋转是非常重要的操作,分两种:顺时针旋转zig和逆时针旋转zag,如下图所示:

    可以发现,旋转操作后,树的中序遍历确实不会发生改变。因此,变换前后的树是等价的。此外,旋转后树的高度可能会发生变化,但是变化不超过1。

  • 相关阅读:
    div居中和table居中,jQuery获取下拉列表值
    Android利用canvas画各种图形(点、直线、弧、圆、椭圆、文字、矩形、多边形、曲线、圆角矩形)
    Android监听应用程序安装和卸载
    android开发图片分辨率问题解决方案
    [整理]Selector、shape详解
    android string.xml %问题
    Android Service学习之本地服务
    android selector中使用shape
    android AsyncTask 详解
    android bluetooth UUID蓝牙查询表
  • 原文地址:https://www.cnblogs.com/lustar/p/7219855.html
Copyright © 2011-2022 走看看