二叉查找树

二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

3. 任意节点的左、右子树也分别为二叉查找树。

4. 没有键值相等的节点(no duplicate nodes)。

实现

在实现中,我们需要定义一个内部类Node,它包含两个分别指向左右节点的Node,一个用于排序的Key,以及该节点包含的值Value,还有一个记录该节点及所有子节点个数的值Number。
public class BinarySearchTreeSymbolTable<TKey, TValue> : SymbolTables<TKey, TValue> where TKey : IComparable<TKey>, IEquatable<TValue>
{
    private Node root;
    private class Node
    {
        public Node Left { get; set; }
        public Node Right { get; set; }
        public int Number { get; set; }
        public TKey Key { get; set; }
        public TValue Value { get; set; }

        public Node(TKey key, TValue value, int number)
        {
            this.Key = key;
            this.Value = value;
            this.Number = number;
        }
    }
...
}

查找

查找操作和二分查找类似,将key和节点的key比较,如果小于,那么就在Left Node节点查找,如果大于,则在Right Node节点查找,如果相等,直接返回Value。

该方法实现有迭代和递归两种。
递归的方式实现如下:
public override TValue Get(TKey key)
{
    TValue result = default(TValue);
    Node node = root;
    while (node != null)
    {

        if (key.CompareTo(node.Key) > 0)
        {
            node = node.Right;
        }
        else if (key.CompareTo(node.Key) < 0)
        {
            node = node.Left;
        }
        else
        {
            result = node.Value;
            break;
        }
    }
    return result;
}
迭代方式如下:
public TValue Get(TKey key)
{
    return GetValue(root, key);
}

private TValue GetValue(Node root, TKey key)
{
    if (root == null) return default(TValue);
    int cmp = key.CompareTo(root.Key);
    if (cmp > 0) return GetValue(root.Right, key);
    else if (cmp < 0) return GetValue(root.Left, key);
    else return root.Value;
}

插入

插入和查找类似,首先查找有没有和key相同的,如果有,更新;如果没有找到,那么创建新的节点。并更新每个节点的Number值,代码实现如下:
public override void Put(TKey key, TValue value)
{
    root = Put(root, key, value);
}

private Node Put(Node x, TKey key, TValue value)
{
    //如果节点为空,则创建新的节点,并返回
    //否则比较根据大小判断是左节点还是右节点,然后继续查找左子树还是右子树
    //同时更新节点的Number的值
    if (x == null) return new Node(key, value, 1);
    int cmp = key.CompareTo(x.Key);
    if (cmp < 0) x.Left = Put(x.Left, key, value);
    else if (cmp > 0) x.Right = Put(x.Right, key, value);
    else x.Value = value;
    x.Number = Size(x.Left) + Size(x.Right) + 1;
    return x;
}

private int Size(Node node)
{
    if (node == null) return 0;
    else return node.Number;
}


最大最小值


迭代:
public override TKey GetMax()
{
    TKey maxItem = default(TKey);
    Node s = root;
    while (s.Right != null)
    {
        s = s.Right;
    }
    maxItem = s.Key;
    return maxItem;
}

public override TKey GetMin()
{
    TKey minItem = default(TKey);
    Node s = root;
    while (s.Left != null)
    {
        s = s.Left;
    }
    minItem = s.Key;
    return minItem;
}
递归:
public TKey GetMaxRecursive()
{
    return GetMaxRecursive(root);
}

private TKey GetMaxRecursive(Node root)
{
    if (root.Right == null) return root.Key;
    return GetMaxRecursive(root.Right);
}

public TKey GetMinRecursive()
{
    return GetMinRecursive(root);
}

private TKey GetMinRecursive(Node root)
{
    if (root.Left == null) return root.Key;
    return GetMinRecursive(root.Left);
}

Floor和Ceiling

查找Floor(key)的值就是所有<=key的最大值,相反查找Ceiling的值就是所有>=key的最小值,下图是Floor函数的查找示意图:

查找Floor为例,我们首先将key和root元素比较,如果key比root的key小,则floor值一定在左子树上;如果比root的key大,则有可能在右子树上,当且仅当其右子树有一个节点的key值要小于等于该key;如果和root的key相等,则floor值就是key。根据以上分析,Floor方法的代码如下,Ceiling方法的代码类似,只需要把符号换一下即可:
public TKey Floor(TKey key)
{
    Node x = Floor(root, key);
    if (x != null) return x.Key;
    else return default(TKey);
}

private Node Floor(Node x, TKey key)
{
    if (x == null) return null;
    int cmp = key.CompareTo(x.Key);
    if (cmp == 0) return x;
    if (cmp < 0) return Floor(x.Left, key);
    else
    {
        Node right = Floor(x.Right, key);
        if (right == null) return x;
        else return right;
    }
}

删除

删除元素操作在二叉树的操作中应该是比较复杂的。首先来看下比较简单的删除最大最小值得方法。

以删除最小值为例,我们首先找到最小值,及最左边左子树为空的节点,然后返回其右子树作为新的左子树。操作示意图如下:

public void DelMin()
{
    root = DelMin(root);
}

private Node DelMin(Node root)
{
    if (root.Left == null) return root.Right;
    root.Left = DelMin(root.Left);
    root.Number = Size(root.Left) + Size(root.Right) + 1;
    return root;
}

删除最大值也是类似。

现在来分析一般情况,假定我们要删除指定key的某一个节点。这个问题的难点在于:删除最大最小值的操作,删除的节点只有1个子节点或者没有子节点,这样比较简单。但是如果删除任意节点,就有可能出现删除的节点有0个,1 个,2个子节点的情况,现在来逐一分析。

当删除的节点没有子节点时,直接将该父节点指向该节点的link设置为null。


当删除的节点只有1个子节点时,将该自己点替换为要删除的节点即可。

当删除的节点有2个子节点时,问题就变复杂了。

假设我们删除的节点t具有两个子节点。因为t具有右子节点,所以我们需要找到其右子节点中的最小节点,替换t节点的位置。这里有四个步骤:

1. 保存带删除的节点到临时变量t

2. 将t的右节点的最小节点min(t.right)保存到临时节点x

3. 将x的右节点设置为deleteMin(t.right),该右节点是删除后,所有比x.key最大的节点。

4. 将x的做节点设置为t的左节点。

整个过程如下图:

public void Delete(TKey key)
{
    root =Delete(root, key);
        
}

private Node Delete(Node x, TKey key)
{
    int cmp = key.CompareTo(x.Key);
    if (cmp > 0) x.Right = Delete(x.Right, key);
    else if (cmp < 0) x.Left = Delete(x.Left, key);
    else
    {
        if (x.Left == null) return x.Right;
        else if (x.Right == null) return x.Left;
        else
        {
            Node t = x;
            x = GetMinNode(t.Right);
            x.Right = DelMin(t.Right);
            x.Left = t.Left;
        }
    }
    x.Number = Size(x.Left) + Size(x.Right) + 1;
    return x;
}

private Node GetMinNode(Node x)
{
    if (x.Left == null) return x;
    else return GetMinNode(x.Left); 
}

分析

二叉查找树的运行时间和树的形状有关,树的形状又和插入元素的顺序有关。在最好的情况下,节点完全平衡,从根节点到最底层叶子节点只有lgN个节点。在最差的情况下,根节点到最底层叶子节点会有N各节点。在一般情况下,树的形状和最好的情况接近。

在分析二叉查找树的时候,我们通常会假设插入的元素顺序是随机的。对BST的分析类似与快速排序中的查找:

BST中位于顶部的元素就是快速排序中的第一个划分的元素,该元素左边的元素全部小于该元素,右边的元素均大于该元素。

对于N个不同元素,随机插入的二叉查找树来说,其平均查找/插入的时间复杂度大约为2lnN,这个和快速排序的分析一样,具体的证明方法不再赘述,参照快速排序。

总结

它和二分查找一样,插入和查找的时间复杂度均为lgN,但是在最坏的情况下仍然会有N的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是后面要讲的平衡查找树的内容了。下文首先讲解平衡查找树的最简单的一种:2-3查找树。

红黑树

红黑树是一个平衡的二叉树,但不是一个完美的平衡二叉树。
我们希望一个所有查找都能在~lgN次比较内结束,但是这样在动态插入中保持树的完美平衡代价太高,所以,我们稍微放松一下限制,希望找到一个能在对数时间内完成查找的数据结构。这个时候,红黑树站了出来。
红黑树需要满足的五条性质: 
性质一:节点是红色或者是黑色; 
在树里面的节点不是红色的就是黑色的,没有其他颜色,要不怎么叫红黑树呢,是吧。 
性质二:根节点是黑色; 
根节点总是黑色的。它不能为红。 
性质三:每个叶节点(NIL或空节点)是黑色; 
性质四:每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点); 
就是连续的两个节点不能是连续的红色,连续的两个节点的意思就是父节点与子节点不能是连续的红色。
性质五:从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点;


节点的插入:
  1. 当插入的节点是根节点时,直接涂黑即可
  2. 当要插入的节点的父结点是黑色的时候,这时候插入一个红色节点没有对五个性质产生破坏,所以直接插入不用进行调整
  3. 如果要插入的节点的父节点是红色且父节点是祖父节点的左支的时候。 
    这个要分两种情况,一种是叔叔节点为黑的情况,一种是叔叔节点为红的情况。 
    当叔叔为黑时,也分为两种情况,一种是要插入的节点是父节点的左支,另一种是要插入的节点是父亲的右支。 
  4. 如果要插入的节点的父节点是红色且父节点是祖父节点的右支的时候; 
    这个时候的情况跟情况3所表述的情况是一个镜像,将情况3的左和右互换一下就可以了。 
  5. 如果要插入的节点的父节点是红色并且叔叔节点也为红色

删除操作:

  1.  如果删除的是叶节点,可以直接删除; 
    2.如果被删除的元素有一个子节点,可以将子节点直接移到被删除元素的位置; 
    3.如果有两个子节点,这时候就可以把被删除元素的右支的最小节点(被删除元素右支的最左边的节点)和被删除元素互换,我们把被删除元素右支的最左边的节点称之为后继节点(后继元素),然后在根据情况1或者情况2进行操作。
  2. 红黑树删除的规则: 
    1.当被删除元素为红时,对五条性质没有什么影响,直接删除。 
    2.当被删除元素为黑且为根节点时,直接删除。 
    3.当被删除元素为黑,且有一个右子节点为红时,将右子节点涂黑放到被删除元素的位置

  3. 1.红黑树的实现其实是一个2、3、4树,只是将双节点或者三节点用红色进行了标示,如果你将红色节点放到和它父元素相同的高度,并把它和父元素看做是一个元素,你就会发现,变成了一个高度为lgN的二叉树,这个2.3.4树对红黑树很有启发意义。 
  4. 2.上面的步骤其实可以不用死记硬背,是可以推导出来的,因为我们是把一个平衡但通过插入或者删除破坏了平衡的红黑树再次平衡,同过旋转让位,改变红黑颜色,使之符合那五条基本性质。比如遇到删除操作情况四的时候,我们可以把那个删除元素去除,发现左边比右边少一个黑元素,这个时候,怎么办,我们发现兄弟节点的子元素有一个红元素,操作这个不会影响那五条性质,所以我们通过变换颜色,旋转,即可让左右两边的的黑色数目一样。 
    3.旋转操作的目的是出让一个元素到另外的地方并且符合二叉树左小右大的性质,交换颜色的目的是为了保持红黑树的那五条性质。 
    4.要时刻记得 ,一切的操作都是为了保持那五条性质。

    最后的最后,其实还有一种更为简单的红黑二叉树,这个简单的红黑二叉树实际上是一个2.3树,他只允许左节点为红节点,但是性能上肯定是不如这个红黑树。这个简单的红黑二叉树在《算法》第四版有介绍,掌握完之后再看这个简单的红黑二叉树,就会觉着简单 easy。