第16章 外部搜索

来源:互联网 发布:怎么才能做淘宝客服 编辑:程序博客网 时间:2024/06/13 02:51

16章 外部搜索

适用于访问庞大文件中项的搜索算法具有极其重要的实际意义。搜索是庞大数据集的基本运算,在许多计算机环境中也必定会耗用大量的资源。

16.1游戏规则

我们做出基本假定:数据的顺序访问比非顺序访问开销少得到。我们的模型是,将用于实现符号表的任何内存工具都看为分成多页:即能被磁盘硬件有效访问的连续信息块。每个页容纳许多项;我们的任务是组织页内的项,只通过少数几页就可以访问任何项。

页(page指连续的数据块。探测(probe)是对页的首次访问。

16.2 索引顺序访问

构建一个索引的简单方法是,使用一个具有键和项引用的数组(按键序),再利用二分搜索来实现搜索。

使用索引顺序访问的缺点是,修改目录的运算开销昂贵。例如,添加单个键可能需要几乎重新构建整个数据库,将许多键放到新位置,并刷新索引值。

16.3 B

16.3.1 B树的定义

一棵M级别的B树是或为空,或包含k-节点的树,具有k-1个键和k个链接,分别表示键分隔的k个区间,并且具有以下结构性质:在根部,k必须在2M之间;在其他每个节点处,k必须在M / 2M之间;所有空树的链接必须与根同距离。


/*

B树中的每个节点包含一个数组和一个数组中活动项数的计数。在内部节点中,每个数组项是一个键以及某节点的一个链接;在外部节点中,数组项是一个键和一个数据项。通过C union结构可以再单个声明中定义这些项。

将新节点初始化为空(计数域设置为0),在数组项中有一个标记键。一棵空B树是一个指向空节点的链接。同时,使用变量来跟踪树中的数据项数量以及树高,它们都被初始化为0

*/

typedef struct STnode* link;

typedef struct

{

Key key;

union{

link next;

Item item;

} ref;

}entry;

struct STnode{entry b[M]; int m;};

static link head;

static int H, N;

link NEW()

{

link x = (link)malloc(sizeof(*x));

x->m = 0;

return x;

}

void STinit()

{

head = NEW();

H = 0;

N = 0;

}

16.3.2 B树的插入运算

像在插入排序中一样,将较大项右移一个位置来插入新项。如果插入使节点溢出,则调用split将节点分解成两半,返回新节点的链接。在递归中上进一层,额外的链接就导致父内部节点中一次相似的插入,它也可能会分解,可以将插入一直深入到根部。

link insertR(link h, Item item, int H)

{

int i, j;

Key v = key(item);

entry x;

link u;

x.key = v;

x.ref.item = item;

if (H == 0)

for (j = 0; j < h->m; j++)

if (less(v, h->b[j].key)) break;

if (H != 0)

for (j = 0; j < h->m; j++)

if ((j + 1 == h->m) || less(v, h->b[j + 1].key))

{

u = insertR(h->b[j++].ref.next, v, H - 1);

if (u == NULL) return NULL;

x.key = u->b[0].key;

x.ref.next = u;

break;

}

for (i = ++(h->m); (i > j) && (i != M); i--)

h->b[i] = h->b[i - 1];

h->b[j] = x;

if (h->m < M) return NULL;

else return split(h);

}

void STinsert(Item item)

{

link t, u = insertR(head, item, H);

if (u == NULL) return;

t = NEW();

t->m = 2;

t->b[0].key = head->b[0].key;

t->b[0].ref.next = head;

t->b[1].key = u->b[0].key;

t->b[1].ref.next = u;

head = t;

H ++;

}

16.3.3 B树节点的分解

如果分解B树中的节点,先创建一个新节点,将键中较大的一半移到新节点,然后调整计数,并在两个节点的中间设置标记值。这段代码假定M为偶树,而且在每个节点中为导致分解的项使用一个额外的位置。也就是说,在某节点中,最大的键数为– 1, 当节点有M个键时,就将它分解成两个节点,每个节点具有M / 2 个键。

link split(link h)

{

int j;

link t = NEW();

for (j = 0; j < M / 2; j ++)

t->b[j] = h->b[M / 2 + j];

h->m = M / 2;

t->m = M / 2;

return t;

}

16.3.3 B树的查找运算

B树这个搜索实现与往常一样基于递归函数。对于内部节点(正高度值),通过扫描找到大于搜索键的第一个键,在对前一个链接引用的子树进行递归调用。对于外部节点(高度为0),通过扫描检查是否存在键与搜索键相等的项。

Item searchR(link h, Key v, int H)

{

int j;

if (0 == H)

for (j = 0; j < h->m; j++)

if (eq(v, h->b[j].key))

return h->b[j].ref.item;

if (H != 0)

for (j = 0; j < h->m; j++)

if ((j + 1 == h->m) || less(v, h->b[j + 1].key))

return searchR(h->b[j].ref.next, v, H - 1);

return NULLitem;

}

Item STsearch(Key v)

{

return searchR(head, v, H);

}

16.3.4 B树的性质

跟红黑树一样,B树也是由2-3-4树推广得来的。B树的特点:

l 推广 2-3-4树得到具有M / 2 ~ M个节点之间的树;

l 通过项和链接的数组表示多路节点;

l 实现一个索引,而不是实现一个包含项的搜索结构;

l 从自底向上分离;

l 分离索引与项。

根据N个随机项构建M阶约有1.44N/M个页。

具有N个项、MB树中,一次搜索或插入需要logMN ~ logN / 2N 次探测。在实际应用中,可当作一个常数。

一般对某个项的访问只需要2次探测。

16.4可扩展哈希法

可扩展哈希法兼具哈希方法、多路trie算法以及顺序访问方法的特点。它是一种随机性算法,与哈希方法一样,可扩展哈希法是一种随机性算法,第一步定义将键转换成整数的哈希函数,为了简单起见,我们只考虑键为随机定长位串(bitstring)的情况。与多路trie一样,可扩展哈希法开始搜索时,使用键的前导位来索引大小为2的幂的表。与B树算法一样,可扩展哈希法在页中保存项,当页充满时分解成多页。与索引顺序访问方法一样,可扩展哈希法使用一个目录来告诉我们在哪里查找包含匹配搜索键的项的页。

这种方法在实际中应用不是很广泛,所以就不细致总结了。

xt-inde,�40p� �<� rgin-bottom:0pt; margin-top:0pt; " > if (less(key(h->r->item),v))

{

h->r->r = splay(h->r->r,item);

h = rotL(h);

} else {

h->r->l = splay(h->r->l,item);

h ->r = rotR(h->r);

}

return rotL(h);

}

}

void STinsertAmortize(Item item)

{

head = splay(head, item);

}

13.3.2 分裂BST查找操作

 

13.3.3 分裂BST性质

当使用分裂插入法在一颗BST中插入一个节点时,不仅将该节点带到了根,而且将其他在搜索路径上碰到的节点带到离根更近的位置。准确的说,我们执行的旋转将从根到碰到的任何节点之间的路径减少一半。如果我们实现搜索运算时,让它在搜索中执行分裂转换,这个性质同样成立。树中的一些路径会变得更长:如果我们不访问这些路径上的节点,其影响对我们来说就不重要。如果我们访问一条长路径上的节点,在完成后,它将变成一半长。因此没有一条路径可以带来高的开销。

当插入序列为有序时,分裂BST生成的树为最坏情况,有N-1层。但是通过少量的几次分裂搜索就可以实质性的改进这棵树的平衡性。

13.4 自顶向下2-3-4

尽管通过随机BST和分裂BST可以得到性能保证,但都仍然存在特定的搜索运算时间为线性的可能性。因此,这两种类型的BST不是如下平衡树的基本问题的答案:是否存在某种类型的BST,能够保证每次插入和搜索运算都与树的大小成对数关系?

13.4.1  2-3-4树的定义

2-3-4搜索树是一棵或为空或包含3种类型节点的树:2-节点,它具有1个键、具有较小键的树的左链接以及具有较大键的树的右链接;3-节点,它具有2个键、具有较小键的树的左链接、具有节点键之间键值的树的中间链接以及具有较大键的树的右链接。4-节点,它具有3个键,具有由节点的键对应的区间定义的键值的树的4个链接。

13.4.2  2-3-4树的操作

2-3-4树中对树的操作主要为插入节点以及对4-节点的删除。

为了在一颗2-3-4树种插入新节点,可以像在BST中一样,先进行一次失败搜索,再插入该节点。如果搜索终止的节点是一个2-节点,只要将该节点变成一个3-节点。相似地,如果搜索在一个3-节点终止,只要将该节点变成一个4-节点。但如果搜索在一个4-节点终止,就需要对4-节点进行分解。在保持树平衡的同时,为新键腾出空间,首先将4-节点分成22-节点,将中间键上传到节点的父亲。如果其父亲也是4-节点,则继续分解,直到找到一个非4节点的父节点。

13.4.3  2-3-4树的性质

1. 平衡2-3-4搜索树是这样一棵2-3-4搜索树,它的所有空树的链接都与根距离相同。

2. 只有根节点的分解会使树增高,内部节点的分解不会影响树的高度。所以2-3-4树是自低向上生长的,而标准BST是自顶向下生长的。

3. 搜索N-节点2-3-4树时,最多访问lgN + 1个节点。

4. 向N-节点2-3-4树进行插入时,在最坏情况下,需要分解的节点数少于lgN + 1;平均来讲,需要分解的节点数小于1

 

13.5 红黑树

其基本思想是将2-3-4树表示为标准BST(仅有2-节点),但为每个节点添加一个额外的信息位,来为3-节点和4-节点编码。我们将链接看作有两种不同的类型:红链接(red link),它将包含3-节点和4-节点的小二叉树捆绑在一起,还有黑链接(black link),它将2-3-4树捆绑在一起。

我们用红链接连接的32-节点来表示4-节点,用红链接连接的22-节点来表示3-节点。3-节点中的红链接可能是一个左链接或者一个右链接,因此,表示每个3-节点有两种方式。

13.5.1 红黑树定义

1. 红黑树中的节点要么为红节点要么为黑节点。

2. 根节点为黑节点。

3. 外部节点为黑节点。

4. 红节点的孩子为黑节点。

5. 从外部链接到根的所有路径都具有相同的黑节点数。

13.5.2 红黑树的操作

13.5.2.1 红黑树的插入操作

/*

这个函数使用红-黑表达方式实现2-3-4树中的插入。给类型STnode添加颜色位red(并相应扩展NEW),用1表示节点为红色,用0表示节点为黑色。空树是标记节点z的链接---一个具有指向自身的链接的黑色节点。

在沿树而下的路径中(在递归调用之前),检查4-节点,交换所有3个节点的颜色位来分解它们。当我们到达底部时,为被插入的项创建一个新红色节点,并返回它的一个指针。

在沿树而上的路径中(在递归调用之后),设置下向链接,它指向返回的链接值,然后检查是否需要旋转。如果搜索路径有两个具有相同方向的红色链接,则从顶部节点进行单个旋转,然后交换颜色位得到正确的4-节点。如果搜索路径有两个具有不同方向的红色链接,则从底部节点进行单个旋转,逐渐还原到其他情形。

*/

link RBinsert (link h, Item item, int sw)

{

Key v = key(item);

if (h == z) return NEW(item, z, z, 1, 1);

if ((h->l->red) && (h->r->red))

{

h->red = 1;

h->l->red = 0;

h->r->red = 0;

}

if (less (v, key(h->item)))

{

h->l = RBinsert(h->l, item, 0);

if (h->red && h->l->red && sw) h = rotR(h);

if (h->l->red && h->l->l->red)

{

h = rotR(h);

h->red = 0;

h->r->red = 1;

}

} else {

h->r = RBinsert(h->r, item, 1);

if (h->red && h->r->red && !sw) h = rotL(h);

if (h->r->red && h->r->r->red)

{

h = rotL(h);

h -> red = 0;

h ->l->red = 1;

}

}

fixN (h);

return h;

}

void STinsertRB(Item item)

{

head = RBinsert (head, item, 0);

head ->red = 0;

}

新插入的节点为红节点,插入红节点之后可能会违反性质4(此时没有一个2-3-4树的表示与其对应,所以需要调整)

红黑树在插入操作时需要对4-节点进行分解操作,各种情况下对4节点的分解方法如下图所示。

 

 

13.5.2.2 红黑树的删除操作(未实现)

 

13.5.3 红黑树的性质

红黑树能保证对于所有的搜索和插入,所化的步数为对数关系。这是具有此性质的少数符号表实现之一,而且它也适用于库实现,其中被处理的键序列不能准确特征化。

根据随机键生成具有N个节点的红-黑树,在此树中的搜索平均约适用1.002lgN次比较。

13.6 跳表

这一节介绍的跳表,它初看之下完全与我们所讨论的基于树的方法不同,但实际上,它们紧密关联。它基于一种随机数据结构,几乎对于我们考虑的所有符合表ADT基本运算都能提供接近最优的性能。搜索过程中,在跳跃式通过表的大部分时,它使用了链表的节点中额外的链接。

13.6.1 跳表的定义

跳表是一种有序链表,其中的每个节点包含数量可变的链接,并且节点中的第i个链接单独实现链接,它跳过少于i个链接的节点。

13.6.2 跳表的操作

跳表中的节点有一个链接数组,因此,NEW需要分配该数组,并将所有的链接设置为标记z。常数lgNmax为表中允许的最多层数,对于小型表,它可以设置为5,对于大型表,可以设置为30 。与往常一样,变量N为表中的项数,lgN为层数。空表为具有lgNmax个链接的一个头节点,所有链接均设置为zNlgN设置为0

typedef struct STnode* link;

struct STnode{Item item; link *next; int sz;};

static link head,z;

static int N, lgN;

link NEW(Item item, int k)

{

int i;

   link x = malloc(sizeof *x);

x->next = malloc (k * sizeof (link));

x->item = item;

x->sz = k;

for (i = 0; i < k; i++) x->next[i] = z;

return x;

void STinit(int max)

{

N = 0;

lgN = 0;

z = NEW(NULLitem,0);

head = NEW(NULLitem,lgNmax);

}

13.6.2.1 跳表的插入

要在跳表中插入一个项,先生成一个新的j个链接的节点(概率为1/2j),然后,沿着链接进行搜索,当下移到底部j层的每一层时链接新节点。

int randX()

{

int i, j, t =rand();

for (i = 1, j = 2; i < lgNmax; i++, j+=j)

if (t > RAND_MAX/j) break;

if (i > lgN) lgN = i;

return i;

}

void insert(link t, link x, int k)

{

Key v = key(x->item);

if (less(v, key(t->next[k]->item)))

{

if (k < x->sz)

{

x->next[k] = t->next[k];

t->next[k] = x;

}

if (k == 0) return;

insertR(t, x, k - 1);

return;

}

insertR(t->next[k], x, k );

}

void STinsert(Key v)

{

insertR(head,NEW(v, randX(), lgN));

N++;

}

13.6.2.2 跳表的搜索

k等于0,此代码就等价于单链表中的搜索。对于一般k值,如果它的键小于搜索键,则将表中的下一个节点移到k层,如果大于搜索键,则下移到k-1层。为了简化代码,假设所有的表以一个标记节点z终止,z为带有maxKeyNULLitem

Item search(link t, Key v, int k)

{

if (eq(v, key(t->item))) return t->item;

if (less(v, key(t->next[k]->item)))

{

if (k == 0) return NULLitem;

return searchR(t, v, k - 1);

}

return searchR(t->next[k], v, k);

}

Item STsearch(Key v)

{

return searchR(head, v, lgN);

}

13.6.2.3 跳表的删除

要从跳表中删除具有已知节点键的节点,在发现它的链接的每一层取消其链接,然后再到达底层时释放它。

void deleteR(link t, Key v, int k)

{

link x = t -> next[k];

if (!less(key(x->item),v))

{

if (eq(v, key(x->item)))

{t->next[k] = x->next[k];}

if (k == 0) {free(x); return;}

deleteR(t,v,t - 1);

return;

}

deleteR(t->next[k], v, k);

}

void STdelete(Key v)

{

delete(head, v, lgN);

N--;
}

13.7 基于BST的内部搜索算法性能比较

本章所讨论的所有方法为一般的应用都提供了优秀的性能,而且对有兴趣开发高性能符号表实现的人来说,每个方法都有各自的长处。分裂BST将提供与自组织搜索方法同样优秀的性能,特别是在频繁访问小型键集的典型模式下;随机BST对于全功能的符号表BST来说,可能更快,也更易于实现;跳表易于理解,与其他方法相比,它以比较少的空间开销提供了对数关系的搜索时间,而对于符号表的库实现,红-BST最具吸引力,因为它提供了最坏情况下的性能保障,而且对于随机数据具备最快的搜索和插入算法。

原创粉丝点击