Rancher

导航

 

摘要

本文主要讨论的内容包括:BST的性质以及基本操作分析。

作为最基本的数据结构,二叉查找树(后文记为BST)本身不仅易于理解,代码精简,而且通过添加不同的特性,可以实现许多高级的数据结构,例如:添加颜色信息,升级为红黑树;添加高度和平衡信息,升级为AVL树;更改节点数量,成为2-3-4树等更为复杂的数据结构。而正如其名,BST及其变种在搜索领域有不可替代的作用。

二叉查找树概览

二叉查找树的特点及解释

在BST上执行的基本操作与树的高度成正比。对于一棵含n个节点的完全二叉树(perfect binary tree),这些操作的最坏运行时间为O(lgn)。但是,如果树是含n个节点的线性链,则这些操作的最坏运行时间为O(n)。

BST作为最基本的树结构(其实还有更基本的单纯的树,不过在这里没有讨论意义),并没有特殊性质去维护其平衡性,如下图,同样都是含有7个结点的二叉树,其高度却不尽相同。因而其基本操作时间也不尽相同。对于一棵含有n个节点的BST,其高度为lgn(完全二叉树)到n(线性链)。故操作时间也不同。后面我们将会看到,一颗随机构造的BST其期望高度为O(lgn),从而基本操作平均时间为O(lgn)。 

如图所示,BST是按二叉树结构组织而成。可以用链表结构表示。一般而言,一个BST的结点声明如下:

1 struct _BST_Node {
2     int key;                    // key域
3     DataType *data;             // 卫星数据
4     struct _BST_Node *parent;   // 父节点
5     struct _BST_Node *left;     // 左子结点
6     struct _BST_Node *right;    // 右子节点
7 }
8
9 typedef struct _BST_Node BST_Node;

如果某个指针对象不存在,则设为NULL。根节点是树中唯一父结点为NULL的结点。

二叉查找树的性质

BST中关键字总是按照如下方式存储来保持二叉搜索树的性质:设x是BST中一个结点,则

如果y是x左子树中的一个结点,则 y.key <= x.key;反之,如果y是x右子树中的一个结点,则 y.key >= x.key。

二叉查找树的遍历

根据BST的性质,我们可以用一个递归算法来依次输出书中所有关键字。根据顺序的不同,有三种遍历算法:

前序遍历(Preorder Traversal):中根-左子树-右子树;

中序遍历(Inorder Traversal):左子树-中根-右子树;

后序遍历(Postorder Traversal):左子树-右子树-中根;

此外,还有一种常用的遍历算法为广度优先搜索(Breadth-First Search, BFS),又称层序遍历,即在遍历中,每进入下一层之前,先将本层结点输出。

下面我们给出中序遍历的伪代码:

 1 void InorderTraversal(BST_Node *root) { // root为要输出的树的根节点
 2    
 3      if (root != NULL) {                // 当前根节点不为空时进入遍历
 4         InorderTraversal(root->left);   // 继续中序遍历左节点
 5         Print(root);            // 输出当前结点
 6         InorderTraversal(root->right);  // 继续中序遍历右结点
 7     }
 8     
 9     return;
10 }

于是我们可以很容易得出前序和后序遍历的代码,即改变代码4~6行中的次序即可。

接下来我们讨论BST的BFS实现方法

实现层序遍历需要用到队列结构。首先根节点进队,随后出队,出队前,需要将其左右两个子女依次进队。保持循环直到队列为空为止。下面给出伪代码:

 1 void BFS(BFS_Node *root) {                     // root为要输出的树的根节点
 2     
 3     if (root != NULL) {                        // 当根节点不为空时进入遍历
 4         
 5         Create queue;                          // 新建空队列
 6         Enqueue(queue, root);                  // 根节点进队
 7         
 8         while (!isEmpty(queue)) {              // 队列不为空时循环输出
 9             Enqueue(queue, queue.top->left);   // 当前节点左右子女进队
10             Enqueue(queue, queue.top->right);
11             Print(queue.top);                  // 输出队头
12             Dequeue(queue);                    // 队头出队
13         }
14   }
15 16 return; 17 }

事实上,广度优先搜索不仅在树中,在后面重要的数据结构图(graph)中也有着重要的作用。

定理:如果tree是一棵包含n个节点的树的根,则遍历树tree过程的时间为O(n)。

查询二叉查找树

对于二叉查找树,最常见的操作就是查找树中某个关键字。除了搜索任意关键字Search操作外,还应支持查找最小值(Min)、最大值(Max)、后继(Successor)、前驱(Predecessor)。

在一棵高度为h的树中,这些操作都可以在O(h)时间内完成。

查找

查找操作很容易实现,由于BST的性质,使得查找操作的实现类似于二分搜索。我们给出递归和非递归两个版本的搜索实现。

 1 BST_Node *Search(BST_Node *root, int key) {  // key为搜索关键字
 2     
 3     if (root == NULL || key == root->key)    // 查询到key则返回包含key的结点,否则返回NULL
 4         return root; 
 5     if (key < root->key)                     // 根据BST性质查找左子树或右子树
 6         return Search(root->left, key);
 7     else
 8         return Search(root->right, key);
 9 }
10 
11 // 搜索过程的非递归版本,一般而言速度会比递归版本快一点
12 BST_Node *Search(BST_Node *root, int key) {
13     
14     while (root != NULL && key != root->key) {
15         if (key < root->key)
16             root = root->left;
17         else
18             root = root->right;
19     }
20 
21     return root;
22 }

最大、最小关键字

根据BST的性质,我们可知一棵树的最小关键字结点一定是最左子节点,同理最大关键字节点一定是最右子节点。于是我们得到了如下代码:

 1 BST_Node *Min(BST_Node *root) {
 2     while (root->left != NULL)
 3         root = root->left;
 4     return root;
 5 }
 6 
 7 BST_Node *Max(BST_Node *root) {
 8     while (root->right != NULL)
 9         root = root->right;
10     return root;
11 }

前驱和后继

首先我们应当定义一个节点的前驱和后继分别代表什么。一般而言,前驱是指中序遍历时,在该节点前输出的那个结点,后继是在该节点后面输出的那个节点。换句话说,如果一棵树中所有关键字都不相同,则结点node的前驱为所有小于node关键字的最大关键字结点,后继为所有大于node关键字的最小关键字结点。在有了中序遍历算法的情况下,我们很容易得出搜索前驱和后继的算法,但如果每次查找前驱后继都需要遍历二叉树,未免显得太过复杂。事实上,根据BST的结构性质,不需要对关键字进行比较及遍历,即可找到某个节点的前驱或后继。

前驱:对于结点node,如果其有左子树,则后继为其左子树最右节点;如果左子树为空,则其后继为其最低祖先结点、且该节点右结点也为node的祖先。

后继:对于结点node,如果其有右子树,则后继为其右子树最左结点;如果右子树为空,则其后继为其最低祖先结点、且该节点左结点也为node的祖先。

这两个定义很绕口,我们需要看图来辅助理解,如图:

我们先找结点6的前驱。根据性质,结点6无左结点,所以我们延其祖先路径向上寻找。我们需要找到这样一个最低的祖先结点:其右结点也是结点6的祖先。结点10不是,它没有右结点。结点12也不是,它的右结点并不是结点6的祖先。结点5是,它的右结点12也是结点6的祖先,并且他是我们遇到的第一个满足该性质的结点。所以结点5是结点6的前驱。

我们再来找结点12的前驱,根据性质,结点12有左结点,因而其前驱是其左子树的最右结点,即最大结点。因而结点10是结点12的前驱。

我们再来找结点13的后继。根据性质,结点13无右结点,所以需要延其祖先路径想上寻找。我们需要找到这样一个最低的祖先结点:其左结点也是结点13的祖先。结点12不是,它的左结点不是结点13的祖先,结点5不是,他的左结点不是结点13的祖先。结点15,即根节点是,因为它的左结点5,也是结点13的祖先,并且它是我们遇到的第一个满足该性质的结点。所以结点15是结点13的后继。

最后我们找结点5的后继。根据性质,结点5有右结点,所以其后继是右子树的最左结点,即最小结点。因而结点6是结点5的后继。

下面我们给出伪代码实现:

 1 BST_Node *Predecessor(BST_Node *node) {
 2  
 3      if (node->left != NULL)
 4          return Max(root->left);
 5      
 6      BST_Node *predecessor = node->parent;
 7      while (predecessor != NULL && node == predecessor->left) {
 8          node = predecessor;
 9          predecessor = node->parent;
10      }
11      
12      return predecessor; 
13 }
14 
15 BST_Node *Successor(BST_Node *node) {
16  
17      if (node->right != NULL)              // 若结点node右子树不为空,则其后继为右子树中的最左结点,即右子树最小关键字结点
18          return Min(root->right);
19      
20      BST_Node *successor = node->parent;   // 若结点node右子树为空,则其后继为其最低祖先借点,且该节点做结点也为node的祖先
21      while (successor != NULL && node == successor->right) {
22          node = successor;
23          successor = node->parent;
24      }
25      
26      return successor; 
27 }

性质:如果BST中的某个结点有两个子女,则其后继没有左结点,前驱没有右结点。(CLRS 12.2-5)

证明:其实这个很容易理解。这里只证明后继的部分。如果结点x有两个子女,记为xLeft和xRight,则x的后继必定在xRight子树上,且xRight上所有结点关键字都大于x(假设没有重复关键字)。设结点y为x的后继,根据定义有y为大于x的最小节点。如果y有左结点,则该左结点比后继还小,与定义不符。

插入和删除

在插入和删除时,最关键的就在于要维护BST性质。在一棵高度为h的树中,这些操作都可以在O(h)时间内完成。

插入

插入过程还是很简单的,首先查找到插入位置,然后链接下新节点跟父节点就可以了。这里假设newnode已经设定好了,两个子女均为NULL。

 1 void Insert(BST_Node *root, BST_Node *newnode) {
 2     
 3     BST_Node *parent;
 4     BST_Node *temp = root;
 5     
 6     // 寻找到合适的插入位置,并记录其父节点
 7     while (temp != NULL) {
 8         parent = temp;
 9         if (newnode->key < temp->key)
10             temp = temp->left;
11         else
12             temp = temp->right;
13     }
14     
15     // 如果所记录父节点为空,则插入到根节点,否则链接新结点与父节点
16     newnode->parent = parent;
17     if (parent == NULL)
18         root = newnode;
19     else {
20         if (newnode->key < parent->key)
21             parent->left = newnode;
22         else
23             parent->right = newnode;
24     }
25 }

删除

删除过程要考虑三种情况:设待删除节点为z

1) 如果z没有子结点,则修改其父节点z.parent指向NULL;

2) 如果z只有一个子结点,则修改其父节点z.parent指向z的唯一子节点;

3) 如果z有两个子节点,则用z的后继y来代替z,并删除y(已经证明z的后继没有左结点,但有可能有右结点,所以还需要处理y的子结点)。事实上,这里也可以用前驱,并无差别。

 1 void Remove(BST_Node *root, BST_Node *z) {
 2 
 3     BST_Node *x,y;
 4     // 确定处理输入结点z(至多一个结点),还是其后继(有两个节点)
 5     if (z->left == NULL || z->righ == NULL)
 6         y = z;
 7     else
 8         y = Successor(z);
 9     // x被置为y的非空子女
10     if (y->left != NULL) 
11         x = y->left;
12     else
13         x = y->right;
14     // 修改被删除节点的子节点的父节点指针
15     if (x != NULL)
16         x->parent = y->parent;
17     // 修改被删除节点的父节点的子节点指针
18     if (y->parent == NULL) {
19         root = x;
20     else {
21         if (y == y->parent->left)
22             y->parent->left = x;
23         else
24             y->parent->right = x;
25     }
26     // 用后继替换原结点内容
27     if (y != z) {
28         z->key = y->key;
29         z->data = y->data;
30     }
31
32     return;
33 }

 

posted on 2014-11-30 11:02  Rancher  阅读(518)  评论(0编辑  收藏  举报