算法学习笔记----二叉搜索树

一、算法描述

顾名思义,一颗二叉搜索树是以一颗二叉树来组织数据的。这样一棵树可以使用一个链表数据结构来表示,每一个节点都是一个对象。除了key和卫星数据之外,每个节点还包含属性left、right和p,它们分别指向节点的左孩子、右孩子和双亲。二叉搜索树不是一颗普通的二叉树,是具有下列性质的二叉树:

1. 若它的左子树不空,则左子树上所有节点都不大于它的根节点的值

2. 若它的右子树不空,则右子树上所有节点都不小于它的根节点的值

3. 它的左、右子树也分别为二叉搜索树

二叉搜索树上的基本操作(SEARCH、INSERT等)所花费的时间与这颗树的高度成正比。对于有n个节点的一颗完全二叉搜索树来说,这些操作的最坏运行时间为Θ(lgn)。如果这棵树是一条n个节点组陈过的线性链,那么同样的操作就要花费Θ(n)的最坏运行时间。

二叉搜索树的性质允许我们通过树的中序遍历算法按序输出二叉搜索树中的所有关键字。我们将中序遍历的过程定义为INORDER-TREE-WALK,如果x是一颗有n个结点子树的根,那么执行INORDER-TREE-WALK(x)所属的时间为Θ(n)。证明如下(选自《算法导论》):

如果是一颗空树,INORDER-TREE-WALK需要耗费一个小的常熟时间(测试x≠NIL),因此对某个常熟c>0,有T(0)=c。

对n>0,假设调用INORDER-TREE-WALK作用在一个节点x上,x节点左子树有k个节点,则x节点右子树的节点数为n-k-1,则执行INORDER-TREE-WALK的时间有T(n)=T(k)+T(n-k+1)+d限界,其中常熟d>0.此式反映了执行INORDER-TREE-WALK(x)的一个时间上界,其中不包括递归调用所花的时间。

使用替换法,通过证明T(n)≤(c+d)n+c,可以证明T(n)=O(n)。对于n=0,有T(0)=c。对于n>0,有

T(n)≤T(k)+T(n-k+1)+d=((c+d)k+c)+((c+d)(n-k-1)+c)+d

=(c+d)n+c-(c+d)+c+d=(c+d)n+c

到此,完成了定理的证明。

二、基本操作

在介绍各个操作之前,先说明代码中用到的两个结构struct bst和struct bst_node,其中bst结构表示二叉搜索树,bst_node表示二叉搜索树的节点,定义如下:

struct bst_node {
    struct bst_node *left;
    struct bst_node *right;
    struct bst_node *parent;
    
    int key;
};

struct bst {
    struct bst_node *root;
};

1、查找

给定一个关键字k,查找关键字为k的节点是否存在,如果存在,则返回找到的节点,否则返回NULL。由于搜索树的特殊性,这个过程从树根开始查找,如果遇到的节点x的关键字大于k则开始从节点x的左子树中继续查找;如果节点x的关键字小于k,则开始从节点x的右子树中开始查找;如果不是上述两种情况,则表示x节点就是所找的节点。如果直到叶子节点还是没有找到,则表示关键字为k的节点不存在。

下面的代码是基于递归思想来实现的,如下所示:

static struct bst_node *bst_search_recursive(struct bst_node *x, int key)
{
    if (!x) {
        return NULL;
    }
    
    if (x->key == key) {
        return x;
    }
    
    if (x->key < key) {
        return bst_search_recursive(x->right, key);
    }
    
    return bst_search_recursive(x->left, key);
}

从树根开始递归期间遇到的节点就形成了一条向下的简单罗京,所以TREE-SEARCH的运行时间为O(h),其中h是这棵树的高度。

我们可以采用while循环来展开递归,用一种迭代方式重写这个过程,对于大多数计算机,迭代版本的效率要高得多,实现如下所示:

static __used struct bst_node *bst_search(struct bst *t, int key)
{
    struct bst_node *x = t->root;
    
    while (x) {
        if (x->key == key) {
            break;
        } else if (x->key > key) {
            x = x->left;
        } else {
            x = x->right;
        }
    }
    
    return x;
}

2、最大关键字节点和最小关键字节点

如果节点x没有左子树,那么由于x右子树中的每个关键字都至少大于或等于x.key,则以x为根的子树中的最小关键字是x.key;如果节点x有左子树,那么由于其右子树中没有关键字小于x.key,且在左子树中的每个关键字不大于x.key,则以x为根的子树中的最小关键字一定在以x.left为根的子树中。所以二叉搜索树的最小关键字节点要么为根节点,或者在根节点的左子树中。同理,二叉搜索树的最大关键字元素要么为根节点或者在根节点的右子树中。

查找最大关键字节点的实现如下:

static __used struct bst_node *bst_maximum(struct bst_node *x)
{
    if (!x) {
        return NULL;
    }
    
    while (x->right) {
        x = x->right;
    }
    
    return x;
}

查找最小关键字节点的实现如下:

static struct bst_node *bst_minimum(struct bst_node *x)
{
    if (!x) {
        return NULL;
    }
    
    while (x->left) {
        x = x->left;
    }
    
    return x;
}

这两个过程在一颗高度为h的树上均能在O(h)时间内执行完,因为和TREE-SEARCH一样,它们所遇到的结点均形成了一条从树根向下的简单路径。

3、后继和前驱

给定一颗二叉搜索树中的一个节点,有时候需要按中序遍历的次序查找它的后继。如果所有的关键字互不相同,则一个节点x的后继节点是大于x.key的节点中关键字最小的节点。如果节点x的右子树不为空,则节点x的后继恰是x右子树中最左节点;如果节点x的右子树为空并且有一个后继y,那么y就是x的有左孩子的最底层祖先,也就是说x在y的左子树中,并且从x到y没有其他节点的左子树中包含x。代码实现如下所示:

static struct bst_node *bst_successor(struct bst_node *x)
{
    struct bst_node *y;
    
    if (x->right) {
        return bst_minimum(x->right);
    }
    
    y = x->parent;
    while (y && (y->right == x)) {
        x = y;
        y = y->parent;
    }
    
    return y;
}

在一颗高度为h的树上,TREE-SUCCESSOR的运行时间为O(h),因此该过程或者遵从一条简单路径沿树向上或者遵从简单路径沿树向下。过程TREE-PREDECESSOR与TREE-SUCCESSOR是对称的,其运行时间为O(h),代码实现如下所示:

static struct bst_node *bst_predecessor(struct bst_node *x)
{
    struct bst_node *y;
    
    if (x->left) {
        return bst_maximum(x->left);
    }
    
    y = x->parent;
    while (y && (y->left == x)) {
        x = y;
        y = y->parent;
    }
    
    return y;
}

4、插入

要将一个新值插入到一颗二叉搜索树中,需要调用TREE-INSERT过程。从树根开始,用指针x来记录一条向下的简单路径,查找要插入的节点的问题。从树根开始,指针沿树向下移动,向左或向右移动取决于要插入的key值和x.key的比较,直到x变为NULL,这个NULL占据的位置就是要插入的节点要放置的位置。和二叉搜索树的其他操作一样,插入操作在一颗高度为h的树上的运行时间为O(h),代码实现如下:

static int bst_insert(struct bst *t, int key)
{
    struct bst_node *bn;
    struct bst_node *y, *x;
    
    if (!t) {
        return -EINVAL;
    }
    
    bn = bst_node_new(key);
    if (!bn) {
        return -ENOMEM;
    }
    
    y = NULL;
    x = t->root;
    while (x != NULL) {
        y = x;
        
        if (bn->key < x->key) {
            x = x->left;
        } else {
            x = x->right;
        }
    }
    
    bn->parent = y;
    if (!y) {
        t->root = bn;
    } else if (bn->key < y->key) {
        y->left = bn;
    } else {
        y->right = bn;
    }
    
    return 0;
}

5、删除

删除操作比较复杂,需要考虑多种情况。假设从一颗二叉搜索树中删除一个给定的节点z,可能的情况有:

1)如果z没有左孩子,那么用其右孩子来替换z,这个右孩子可以是NULL,也可以不是。当z的右孩子是NULL时,此时这种情况归为z没有孩子节点的情况,只需修改z的父节点,然后将z直接删除。如果z的右孩子为非NULL时,这种情况就是z仅有一个孩子节点的情况,该孩子是其右孩子。

2)如果z仅有一个孩子且为其左孩子,那么用其左孩子来替换z。

3)否则,z既有一个左孩子又有一个右孩子,我们要查z的后继y,这个后继位于z的右子树中并且没有左孩子(如果有左孩子的话,左孩子的值小于y,y就不是节点z的后继了,因此左孩子的值小于y)。现在需要将y移出原来的位置进行拼接,并替换树中的z。

4)如果y是z的右子树,那么用y替换z,并仅留下y的右孩子。

5)否则,y位于z的右子树中但并不是z的右孩子,(也就是说y和z没有直接相连)。在这种情况下,要先用y的右孩子替换y,然后再用y替换z。

代码实现如下所示:

static void bst_transplant(struct bst *t, struct bst_node *old, struct bst_node *new)
{
    if (!old) {
        return;
    }
    
    if (!old->parent) {
        t->root = new;
    } else if (old == old->parent->left) {
        old->parent->left = new;
    } else {
        old->parent->right = new;
    }
    
    if (new) {
        new->parent = old->parent;
    }
}

static void bst_delete(struct bst *t, struct bst_node *z)
{
    struct bst_node *y;
    
    if (z->left == NULL) {
        bst_transplant(t, z, z->right);
        return;
    } else if (z->right == NULL) {
        bst_transplant(t, z, z->left);
        return;
    }
    
    y = bst_minimum(z->right);
    if (y->parent != z) {
        bst_transplant(t, y, y->right);
        y->right = z->right;
        y->right->parent = y;
    }
    
    bst_transplant(t, z, y);
    y->left = z->left;
    y->left->parent = y;
}

其中bst_transplant()是用来实现用另一颗子树替换一颗子树并成为其双亲的孩子节点。

同样,在一颗高度为h的树上,TREE-DELETE的运行时间为O(h)。

你可能感兴趣的:(算法学习笔记----二叉搜索树)