LeetCode刷题191130 --基础知识篇 二叉搜索树

  休息了两天,状态恢复了一下,补充点基础知识。

 

二叉搜索树

  搜索树数据结构支持许多动态集合操作,包括Search,minimum,maximum,predecessor(前驱),successor(后继),INSERT和DELETE等。因此我们使用一颗搜索树既可以作为一个字典又可以作为一个优先队列。且二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。二叉搜索树有两个很重要的变体,红黑树与B树,这个我们之后有机会再补一篇文章。

  顾名思义,一棵二叉搜索树是以一棵二叉树来组织的。如图所示,这样的一棵树可以用一个链表的数据结构来表示,其中每个结点就是一个对象。除了结点的key之外,每个结点还包含属性left,right,p。分别用于指向这个结点的左孩子,右孩子与父节点。如果不存在,则对应的属性值为null。根结点是树种唯一父指针为null的结点。

                                 

  关于二叉搜索树有这么一些特点。对任何结点x,其左子树中的关键字最大值不超过x.key,其右子树中的关键字的最小值不小于x.key。不同的二叉搜索树可以代表同一组值的集合。如上图所示。

 

中序遍历

  二叉搜索树的这种性质允许我们通过一个简单的递归算法来按序(x.key的顺序)输出二叉搜索树的所有关键字。这种算法被称为中序遍历(inorder tree walk)。这样命名的原因是输出的子树的根的关键字位于根的左子树与右子树的关键字值之间。类似地,先序遍历,后序遍历输出的根的关键字在其左右子树的关键字值的前/后。一个中序遍历的伪算法如下:

INORDER-TREE-WALK(x)
{
    if (x != Null)
    {
          INORDER-TREE-WALK(x.left);
          print x;
          INORDER-TREE-WALK(x.right);
    }
}  

  对于之前图中的两棵二叉搜索树,他们中序遍历得到的结果是一样的,2,5,5,6,7,8.

 

查询二叉搜索树

  前面有提到,二叉搜索树支持许多集合操作。我们来看看一下利用二叉搜索树完成这些操作的思路。

  查找

    假如我们要寻找关键字值为k的结点,如果这个结点存在就返回,否则就返回null。可以利用下面的方法来进行查找。

Search(x,k) //x为要进行查找的树的根结点,k为要查找的关键字值
{
    if(x == Null || x.key ==k)
    {
        return x;
    }

    if(k < x.key)
    {
        return Search(x.left, k);
    }
    else
    {
        return Search(x.right, k);
    }
}

  思路很简单,我们判断当前这个结点是否是我们要寻找的结点,如果不是则决定使用左子树或右子树来继续查找。但上面的方式其实还是一种递归,我们可以用一种while循环的方式来改写它。对于计算机来说,这种方式效率应该会更高一些。

Search(x, k) //同样x为树的根结点,k为要查找的关键字的值
{
    while(x != Null && x.key != k)  
    {
         if(k < x.key)
         {
             x = x.left;
         }
         else
        {
             x = x.right;
        }
    }

    return x;  
}

  

  最大值与最小值

    基于二叉搜索树的性质,对任何结点x,其左子树中的关键字最大值不超过x.key,其右子树中的关键字的最小值不小于x.key。这样我们去寻找一棵二叉搜索树种的最大值或最小值可以简化操作为不断的寻找右/左孩子,直到对应的右/左子孩子的值为null。以最大值举例,这样找到的结点值一定不小于它的父节点,而这个父节点也一定不小于它父节点,依次直到根结点。根结点又一定不小于左子树中的任意结点。

 

  后继与前驱

    我们先来看看后序与前驱的定义。给定一棵二叉搜索树种的一个结点,如果按照中序遍历的次序查找它的后继,如果所有的关键字互不相同,则一个结点x的后继是大于x的.Key的最小关键字的结点。同样,结点x的前驱是小于x.Key的最大关键字的结点。二叉搜索树的结构允许我们通过没有任何关键字的比较来确定一个结点的后继。如果后继存在,这样的过程将返回结点x的后继,否则就返回Null。

SUCCESSOR(x)
{
    if(x.right != null)
    {
        return TREE-MINIMUM(x.right); // 找到右子树中的最小值。
    }
    var y = x.p;
    
    while(y != null && x == y.right)
    {
        x = y;
        y = y.p;
    }
    
    return y;
}

  我们以上面寻找后继的伟算法为例,看看找后继结点的思路。前驱结点的寻找方式与之大同小异。根据后继的定义,它的值一定不小于x.key。因此,如果结点x的右子树存在,那么x.Right中的最小值就是x的后继。因为根据二叉搜索树的定义,这个最小值一定满足要求。如果x的右子树不存在,我们需要向x的祖先中寻找x的后继了。(x的左子树一定不满足要求,故舍去)。我们先找到x的父节点y,判读一下x是y的左孩子还是右孩子。如果x是左孩子,那x的父结点y就是x的后继。因为y.Key >= x.Key。如果x是y的右孩子,说明y的值仍小于x,此时我们应继续寻找。直到遇到根结点,或当期迭代的x是父节点的左孩子。

  另算法导论上有一个定理,即在一棵高度为h的二叉搜索树上,动态集合上的操作查找,最小值,最大值,寻找后继与前驱可以在O(h)的时间内完成。这里就不贴证明了。

 

  插入和删除

    插入和删除一个结点会引起由二叉搜索树表示的动态集合的变化。一定要修改数据结构来反应这个变化,但修改要保持二叉搜索树性质的成立。我们来看看插入一个结点的伪算法

TREE-INSERT(T,z) // T为要插入的树,z为要插入的新结点
{
    y = null;
    x = T.root;
    while(x != null)              // 这个while是为了确定要插入的结点z的父结点
    {
        y = x;
        if(z.key < x.key)
        {
            x = x.left;
        }
        else
        {
            x = x.right;
        }
    }
    
    z.p = y;                    
    
    if(y == null)              //这里判断是否新插入的z为根结点,或z应该为其父结点的左孩子还是右孩子
    {
        T.root = z;
    }
    else if( z.key < y.key)
    {
        y.left = z;
    }
    else
    {
        y.right = z;
    }
}

  插入的思路是这样的,首先我们找到树根结点x,然后用新插入的结点的值与x的值进行比较,来确定新插入的结点应该在树根的左子树还是右子树中。然后这样不断迭代下去最终确定新插入的结点z的父结点。然后第二步去修改其父结点的属性,确定该新插入的结点是否为根结点。或是父结点的左孩子还是右孩子。注意,插入与删除操作都会涉及到两部分的变化,一是被操作的结点本身,二是与该被操作的结点所关联的结点,如其父结点,孩子结点。

  删除

  从一棵二叉搜索树种删除结点可以分为下面三种情况:

  1. 如果要删除的结点z没有孩子结点,那么我们只要简单的将其删除,并修改z的父结点,用null来替换z。
  2. 如果要删除的结点z只有一个孩子结点,那么将这个孩子结点x替换到之前z结点的位置,并修改z结点的父节点,将之前指向z的孩子结点替换为x。
  3. 如果要删除的结点z有两个孩子,那么我们要先找到z的后继y(此时它一定在z的右子树中),然后用y替换z的位置。z原来的右子树应成为y的新的右子树,并且z的左子树成为y的新的左子树。这种情况有一些复杂,因为这与y(z的后继)是不是z的右孩子有关。
    • 如果y是z的右孩子,潜在的说明了这样一个条件。y没有左孩子。我们来回顾一下后继的定义就知道,z的后继一定是不小于z的最小值。假设y有左孩子,那么z的后继显然应该是y的左孩子而不是y,这与我们的条件矛盾。所以,这种情况下y只有右子树。因此,我们只需要用y来替换之前z在二叉搜索树种的位置,然后把z原来的左孩子赋给y即可。
    • 如果y不是z的右孩子,此时情况有些复杂。我们需要将z的右子树拿出来做一些调整,调整的结果为y为这棵新树的根结点。如果实现了这种调整,剩下的操作就与之前是一样了----只需要用y来替换之前z在二叉搜索树种的位置,然后把z原来的左孩子赋给y即可。

  为了实现删除操作,我们需要先定义这样一个子过程TRANSPLANT,它是用另一棵子树替换一棵子树并成为其双亲的孩子结点。当TRANSAPLANT用一棵以v为根的子树来替换一棵以u为根的子树时,结点u的双亲就变为结点v的双亲,并且最后v成为u的双亲的对应的孩子。

TRANSPLANT(T, u, v) //树T,原子树u,新子树v
{
    if(u.p == null)           //v成为u的双亲的对应的孩子。
    {
        T.root = v;
    }
    else if(u == p.left)
    {
        u.p.left = v;
    }
    else
    {
        u.p.right = v;
    }
    
    if(v != null)
    {
        v.p = u.p;      //结点u的双亲就变为结点v的双亲
    }
}

  有了这个子过程,我们再来看看在树T中删除结点z的伪算法:

 1 DELETE(T, z)
 2 {
 3     if(z.left = null)
 4     {
 5         TRANSPLANT(T, z, z.right);
 6     }
 7     else if (z.right == null)
 8     {
 9         TRANSPLANT(T, z, z.left);
10     }
11     else
12     {
13         y = TREE-MINIMUM(z.right);              //这里要注意一下,这里实际上取得的是y的后继!因为前面的讨论,当z有两个孩子时候,z的后继必然在z的右子树中,简化为直接取右子树的最小值作为后继!
14         
15         if(y.p != z)                            //注意这里的if里的逻辑,代表y不是z的右孩子,此时我们先用y的右孩子来替换y,然后把z的右孩子赋给y。这里其实是新生成了一颗已y为根结点的子树!
16         {
17             TRANSPLANT(T, y, y.right);
18             y.right = z.right;
19             y.right.p = y;
20         }
21         
22         TRANSPLANT(T,z,y);
23         y.left = z.left;
24         y.left.p = y;
25     }
26     
27 }

  有定理可以证明一棵高度为h的二叉搜索树,实现动态集合操作INSERT和DELETE的运行时间均为O(h).

 

  二叉搜索树,完结撒花✿✿ヽ(°▽°)ノ✿。

posted @ 2019-11-30 19:27  DogTwo  阅读(390)  评论(0编辑  收藏  举报