《算法导论》第14章 数据结构的扩张 (1)动态顺序统计


《数据结构扩张》是《算法导论》第三部分的最后一章。在介绍学习了这么多种数据
结构之后,简要介绍了当这些基本数据结构不满足需求时,如何扩张它们来满足需求。
这才是学习算法的目的,能够根据需求选择合适的数据结构和算法,并在无法满足需求
时能够扩张它。这才是对算法的思想和本质的学习!

可以将本章看做深入学习的前奏吧,因为紧接着就要开始进入第四部分《高级设计和分析
技术》了。那么赶快来看看如何扩张数据结构,然后就进入高级部分的学习吧!


1.如何扩张数据结构?

1)选择基础数据结构
2)确定要在基础数据结构中添加哪些信息
3)验证可用基础数据结构上的基本操作来维护新添加的信息
4)设计新的操作

下面来看一个简单的数据扩张的例子 - 动态顺序统计树。《算法导论》中选用红黑树
作为基础数据结构,这里为了简单,用最简单的二叉查找树来实现。这样我们可以集中
注意力在数据结构扩张的方法上。


2.动态顺序统计树

在第九章《中位数和顺序统计》中,我们曾研究过如何求数组中的元素的顺序统计量。
如查找最大值、最小值、第 i 小的值等等。其中求第 i 小元素时采用了随机快速排序的
RANDOMIZED-PARTITION划分方法,从而达到了Θ(n)的期望运行时间。

本节要研究的是动态顺序统计量,与之前的有些类似,首先我们来看面对这个需求,
如何选择合适的数据结构,并在必要时进行简单的扩张。

1)选择基础数据结构

我们面对的需求是维护动态集合,即数据可能会频繁的插入和删除,固定长度的数组
显然不适合这种情形。所以我们可以选择二叉查找树作为基础数据结构,但基本的
二叉查找树实现顺序统计功能是很低效的,所以还要添加新的域来扩张它。

2)确定要在基础数据结构中添加哪些信息

我们选择在每个结点添加一个新的size域,来保存每棵子树的大小(包含的结点树,包括
此根结点自身)。更自然的想法是新加一个rank域,直接保存每个结点的顺序统计量,
但这样插入和删除时可能会麻烦一些,参见习题14.1-6。

3)验证可用基础数据结构上的基本操作来维护新添加的信息

如前所述,添加size而不是rank域是为了只对原有数据结构略作改动,就足以维护附加信息,
这是比较理想的情况。如果添加rank域,当插入一个最小元素时,所有结点的rank域都要变。

4)设计新的操作

新的操作就是我们需求中所需的,OS-SELECT求顺序统计量为 i 的结点和OS-RANK求一个
结点的顺序统计量。


3.实现源码

下面来看实现代码,这里只列出BST实现的改动部分,着重来看新加的操作和对BST的影响。

typedef struct _BSTNode {
     struct _BSTNode *left, *right, *parent;
     int key;
     char *value;
     int size;
} BSTNode;

void bst_insert(BSTNode **root, BSTNode *newNode)
{
     // Locate insert location
     BSTNode *pNode = NULL;
     BSTNode *node = *root;
     while (node != NULL) {
          pNode = node;
          node->size += 1;//维护size域
          if (newNode->key < node->key)
               node = node->left;
          else
               node = node->right;
     }
     
     // Link newNode to pNode
     newNode->parent = pNode;

     // Link pNode to newNode
     if (pNode == NULL)
          *root = newNode;
     else if (newNode->key < pNode->key)
          pNode->left = newNode;
     else
          pNode->right = newNode;          
}

BSTNode *bst_delete(BSTNode **root, BSTNode *delNode)
{
     BSTNode *pNode = delNode;//维护size域
     while ((pNode = pNode->parent) != NULL)
          pNode->size -= 1;

     // Real delete node: delNode or its successor 
     // (if delNode has both left and right child)
     BSTNode *realDelNode;
     if (delNode->left && delNode->right)
          realDelNode = bst_successor(delNode);
     else
          realDelNode = delNode;

     // Child of real delete node
     BSTNode *childNode;
     if (delNode->left)
          childNode = realDelNode->left;
     else
          childNode = realDelNode->right;

     // Link realDelNode child and parent
     if (childNode)
          childNode->parent = realDelNode->parent;

     if (realDelNode->parent == NULL)
          *root = childNode;
     else if (realDelNode == realDelNode->parent->left)
          realDelNode->parent->left = childNode;
     else
          realDelNode->parent->right = childNode;

     // Copy successor data to delNode (override)
     // if real delete node is not delNode but its successor          
     if (realDelNode != delNode) {
          delNode->key = realDelNode->key;
          delNode->value = realDelNode->value;
          delNode->size = realDelNode->size;//维护size域
     }

     return realDelNode;
}

BSTNode *bst_os_select(BSTNode *node, int i)
{
     if (node == NULL)
          return NULL;

     int r = 1;
     if (node->left != NULL)
          r = node->left->size + 1;

     if (i == r)
          return node;
     else if (i < r)
          return bst_os_select(node->left, i);
     else
          return bst_os_select(node->right, i - r); 
}

int bst_os_rank(BSTNode *root, BSTNode *node)
{
     int r = 1;
     if (node->left != NULL)
          r += node->left->size;

     while (node != root) {
          if (node == node->parent->right)
               r += node->parent->left->size + 1;
          node = node->parent;
     }
     return r;
}


4.运行情况详细分析

现在以查找第 i = 17小的元素来看OS-SELECT的执行过程。


    |                                                                                                                                                    |
    |                                                                                                                                                    |
[  3, 7,  10, 12, 14,14, 16, 17, 19,   20,21,      21, 26, 28,       30,35, 38, 39,41,        47]

1.从根结点26开始,其顺序统计量 r = 26左子结点size + 1 = 13 < i,则递归OS-SELECT(26右子结点, i - r),
即OS-SELECT(结点41,4)。

[  3, 7,  10, 12, 14,14, 16, 17, 19,   20,21,      21, 26, 28,       30,35, 38, 39,41,        47]

2.根结点变为41,i = 4,r = 5 + 1 = 6 > i,递归OS-SELECT(41的左子结点, i ),即OS-SELECT(结点30, 4)。

 3, 7,  10, 12, 14,14, 16, 17, 19,   20,21,      21, 26, 28,       30,35, 38, 39,41,        47]

3.根结点变为30,i = 4,r = 1 + 1 = 2 < i,递归OS-SELECT(结点38, 2)。

[  3, 7,  10, 12, 14,14, 16, 17, 19,   20,21,      21, 26, 28,       30,35, 38, 39,41,        47]

4.根结点变为38,i = 2,r = 1 + 1 = 2 == i,找到了!结点38即为顺序统计量17的元素。

 3, 7,  10, 12, 14,14, 16, 17, 19,   20,21,      21, 26, 28,       30,35, 38, 39,41,        47]

看到OS-SELECT的实现了吧,跟第九章的RANDOMIZED-SELECT多么相似。从根结点开始遍历,
首先计算根节点的顺序统计量设为r,若比 i 小则继续处理根节点的左子树,否则处理右子树。
我们可以将每次递归处理的根节点node看做RANDOMIZED-PARTITION返回的划分元素A[q],
若 i 比A[q]的顺序统计量小则继续处理较小的数组部分,否则处理大的那部分。如出一辙!



posted on 2012-04-08 17:57  毛小娃  阅读(163)  评论(0编辑  收藏  举报

导航