(学习的主要对象是《算法导论》上B-树章节)
应用环境:
辅存和主存的矛盾,主存只能维持有限的页数,其他页存于辅存上,使用时调入内存。
B树的定义:
是一个具有如下性质的有根树:
(1)每个结点x有以下域:
(a) n[x],存放结点x的关键字数;
(b) n[x]个关键字本身,以非降序存放;
(c) leaf[x],1代表x是叶子,0代表x是内结点。
(2)每个内结点包含n[x]+1各指向其子女的指针。叶结点对这个域没有定义。
(3)各关键字对其各子树关键字范围进行分隔。
(4)每个叶结点深度相同。
(5)每个结点包含的关键字有上界和下界,用t表示最小度数,则有
(a)每个非根结点至少t-1各关键字。每个非根的内结点至少t个子女。若树非空,则根至少有一个关键字。
(b)每个结点包含至多2t-1个关键字。因此一个内结点至多可以有2t个子女。
相关算法简述:
搜索算法:
类似于二叉查找树。
分裂算法:
对于关键字达到2t-1时的满结点(可能)将做的操作。简述为,将中间关键字提升至父结点,同时将原结点分裂成两个。
插入算法:
利用了分裂算法,在插入时,逐步下降到要查入的结点,沿途对满结点进行分裂,若不满,直接插入即可,用辅助算法insert_nonful实现。
删除算法:
比较复杂,逐步下降时,对于将来不满足关键字数大于t-1的结点做出调整。调整有多种形式:合并两个关键字为t-1的兄弟;下降父结点至一个关键字所在子树的子结点,同时上升它的一个关键字大于等于t的兄弟结点的关键字至父结点;对于逐步下降后含关键字的叶子,直接删除关键字。
这个简述看上去比较混乱,直接理解算法即可,或者参考算法导论上相关叙述。。
心得体会:
与红黑树相反,B-树的插入和删除在下降时进行调整,而前者是先操作,然后逐步向上调整使其满足性质。因此B-树不需要指向父亲的指针。
同时,B-树由于是在下降过程中调整,因此它不能直接对非根结点调用delete,这样会导致调整不完全。
而且对于B-树关键字和子结点指针操作时,下标比较容易引起混乱。有一个小技巧需要可以减少混乱:下标为i的关键字,其左边的孩子下标也是i。不过编写算法时最好画个示意图,比较清楚。
以下是自己编写的C语言版本的B-树,其中各个操作已经过验证和调整,暂未发现遗留的bug。如果想实现自己的测试过程,调整main函数里面的操作即可。
#include <stdio.h> #include <stdlib.h> #define BT_T 3//B树的度 struct bnode *bt_search(struct bnode *, char); struct bnode *bt_creat(); int bt_split_child(struct bnode *, int, struct bnode *); struct bnode *bt_insert(struct bnode *, char); int bt_insert_nonful(struct bnode *, char); int bt_delete(struct bnode*, char); struct bnode { int num; int leaf; //1 is leaf char value[2*BT_T -1]; struct bnode *child[2*BT_T]; }; struct bnode *bt_search(struct bnode *p, char k) { int i; i = 0; while ((i < p->num) && (k > p->value[i])) i++; if ((i<=p->num) && (k == p->value[i])) { printf("[search]%c's num is %d\n",k,i); return p; } if (p->leaf) { printf("not found.\n"); return NULL; } else { printf("DISK-READ: c%d\n", i); //DISK-READ() return bt_search(p->child[i],k); } } struct bnode *bt_creat() { struct bnode *p; p = (struct bnode *)malloc(sizeof(struct bnode)); p->leaf = 1; p->num = 0; printf("DISK-WRITE: root of new T\n"); return p; } int bt_split_child(struct bnode *x, int i, struct bnode *y) { int j; struct bnode *z; z = (struct bnode *)malloc(sizeof(struct bnode)); printf("in split\n"); z->leaf = y->leaf; z->num = BT_T -1 ; for (j=0;j<BT_T-1;j++) z->value[j] = y->value[j+BT_T]; if (!(y->leaf)) for(j=0;j<=BT_T-1;j++) //there was a bug. z->child[j] = y->child[j+BT_T]; y->num = BT_T -1; for(j=x->num +1;j>=i+1;j--)// x->child[j+1] = x->child[j]; x->child[i+1] = z; for(j=x->num -1;j>=i-1;j--) x->value[j+1] = x->value[j]; x->value[i] = y->value[BT_T-1]; x->num++; printf("DISK-WRITE(y):in split\n"); printf("DISK-WRITE(z):in split\n"); printf("DISK-WRITE(x):in split\n"); } struct bnode* bt_insert(struct bnode *x, char k) {//x is root struct bnode *s; if (x->num == 2*BT_T-1) { s = (struct bnode *)malloc(sizeof(struct bnode)); s->leaf = 0; s->num = 0; s->child[0] = x; bt_split_child(s,0,x); bt_insert_nonful(s,k); return s; } else { bt_insert_nonful(x,k); return x; } } int bt_insert_nonful(struct bnode *x, char k) { int i; struct bnode *p; i = x->num-1; if (x->leaf) { while((i>=0)&&(k<x->value[i])) { x->value[i+1] = x->value[i]; i--; } x->value[i+1] = k; printf("(!!!!)%c %d\n",x->value[i+1],i+1); x->num = x->num+1; printf("DISK-WRITE(x):in bt_insert_nonful\n"); } else { while((i>=0)&&(k<x->value[i-1])) i=i--; i++; printf("DISK-READ(c%d[x]):in insert_nonful\n",i); p = x->child[i]; if (p->num == (2*BT_T - 1)) { bt_split_child(x,i,p); if (k>x->value[i]) i++; } bt_insert_nonful(x->child[i],k);//there was a bug: bt_insert_nonful(p,k); } return 1; } int bt_delete(struct bnode* x, char k) { int i,j,lnum,rnum; struct bnode *p; i=0; while ((i<x->num) && (k>x->value[i])) i++; if ((i<=x->num) && (k == x->value[i])) { if(x->leaf) {//情况(1),不能对叶结点的指针直接调用bt_delete(),这样相当于跳过了情况(3) for(;i<x->num-1;i++) x->value[i] = x->value[i+1]; x->num --; return 1; } else { //情况2 lnum = x->child[i]->num; rnum = x->child[i+1]->num; if (lnum >= BT_T) { x->value[i] = x->child[i]->value[lnum-1]; bt_delete(x->child[i],x->value[i]); return 2; } else if (rnum >= BT_T) { x->value[i] = x->child[i+1]->value[0]; bt_delete(x->child[i+1],x->value[0]); return 2; } else { //合并k两个孩子结点,并把k下降到这个结点 x->child[i]->value[BT_T-1] = k; for(j=0;j<BT_T-1;j++) { x->child[i]->value[BT_T+j] = x->child[i+1]->value[j]; x->child[i]->child[BT_T+j] = x->child[i+1]->child[j]; } x->child[i]->num = 2*BT_T -1; //修改x,使其原k右边的孩子与x断开 for(j=i;j<x->num-1;j++) { x->value[j] = x->value[j+1]; x->child[j+1] = x->child[j+2]; } x->num--; //递归删除k bt_delete(x->child[i],k); return 2; } } } else { if(bt_search(x->child[i],k) == NULL) { printf("[delete]not found!"); return 0; } p = x->child[i]; if (x->child[i]->num >= BT_T) { bt_delete(x->child[i],k); return 3; } else if ((i>0)&&(x->child[i-1]->num >= BT_T)) {//情况3a其1,左兄弟可用 for(j=BT_T-2;j>=0;j--) p->value[j+1] = p->value[j]; p->value[0] = x->value[i-1]; for(j=BT_T;j>=1;j--) p->child[j] = p->child[j-1]; p->child[0] = x->child[i-1]->child[x->child[i-1]->num]; x->value[i-1] = x->child[i-1]->value[x->child[i-1]->num-1]; x->child[i-1]->num--; bt_delete(x->child[i],k); return 3; } else if ((i<x->num-1)&&(x->child[i+1]->num >= BT_T)) {//情况3b其2,右兄弟可用 p->num++; p->value[p->num-1] = x->value[i]; p->child[p->num] = x->child[i+1]->child[0]; x->value[i] = x->child[i+1]->value[0]; p=x->child[i+1];//为了便于编码 for(j=0;j<p->num-1;j--) p->value[j] = p->value[j+1]; for(j=0;j<p->num;j--) p->child[j] = p->child[j+1]; p->num--; bt_delete(x->child[i],k); return 3; } else if (i>0) {//情况3b其1,与左兄弟合并 x->child[i-1]->value[BT_T-1] = x->value[i-1]; for(j=0;j<BT_T-1;j++) x->child[i-1]->value[BT_T+j] = x->child[i]->value[j]; for(j=0;j<=BT_T-1;j++) x->child[i-1]->child[BT_T+j] = x->child[i]->child[j]; for(j=i-1;j<=x->num-2;j++) { x->value[j]=x->value[j+1]; x->child[j+1] = x->child[j+2]; } x->num--; x->child[i-1]->num = 2*BT_T -1; bt_delete(x->child[i-1],k); return 3; } else if (i<x->num-1) {//情况3b其2,与右兄弟合并 x->child[i]->value[BT_T-1] = x->value[i]; for(j=0;j<=BT_T-1;j++) x->child[i]->value[BT_T+j] = x->child[i+1]->value[j]; for(j=0;j<=BT_T;j++) x->child[i]->child[BT_T+j] = x->child[i+1]->child[j]; for(j=i;j<x->num-1;j++) { x->value[i] = x->value[i+1]; x->child[j+1] = x->child[j+2]; } x->num--; x->child[i]->num = 2*BT_T -1; bt_delete(x->child[i],k); return 3; } } } int main() { struct bnode *p,*root; char a; root = bt_creat(); for (a='A';a<='Z';a++) if ((a!='H')&&(a!='I')) root = bt_insert(root,a); bt_delete(root,'M'); bt_search(root,'N'); bt_search(root,'O'); return 1; }
近期课程的作业需要用到RPC编程,除了课堂上学到的知识,还得亲自动手。打算先写个简单的HelloWorld练习一下,顺便复习一下学到的知识。RPC意为远程过程调用协议(Remote Procedure Call Protocol)。编制好单机应用程序,然后划分为两个或多个程序片,加入通信协议使得每片可以在单独的计算机上运行。从一个程序片调用另一个程序片的过程称为远程过程调用,即RPC。它是一个C/S模型,调用程序称为rpc client,被调用程序片称为rpc server。对于RPC的编程过程可以简化如下:1.构建解决问题的常规应用程序;
2.选择一组过程形成远程程序,以便将远程程序转移到远程机器中,通过这种方法将程序分解;
3.为远程程序编写RPC界面(xxx.idl),包括远程的名字及其编号,还有对其参数的申明,选择远程程序号和版本号;
4.运行rpcgen检查该界面,如果合法,便生成四个源代码文件:xxx.h(类型说明文件)、xxx_XDR.c(XDR转换例程)、xxx_clnt.c(客户端的stub)以及xxx_svc.c(服务守护过程,服务端的stub) ,这些文件将在客户和服务器程序中使用;
5.为客户端和服务器端编写stub接口例程;
6.编译并链接客户程序。它由四个主要文件组成:去掉了远程过程的程序、客户端的stub(rpc生成)、客户端的stub接口以及XDR过程( rpc生成)。
7.编译并链接服务器程序。它由四个主要文件组成:远程过程组成的程序、服务器的stub(rpc生成)、服务器端的stub接口以及XDR过程( rpc生成)。
8.在远程机器上启动服务器,接着在本机上启动客户。简单来讲,需要做的主要工作就是编写应用程序并分片,编写接口(规格说明文件),编写stub接口例程,编写主函数。通信协议的实现和XDR数据格式的统一交给rpcgen完成,它会自动生成相关的代码。练习的代码的参考: http://wenku.baidu.com/view/58e19446b307e87101f696c6.html这个程序的目的是客户端接收一个参数(设置为HelloWorld),发送给服务器,然后服务器显示自己的提示信息和客户端发来的信息。调试不是很顺利,最开始时遇到了提示:Cannot register service: RPC: Unable to receive; errno = Connection refused。上网搜索得知,是因为服务器没有开启端口映射的功能,开启portmap就可以了。sudo /etc/init.d/portmap restart 当然,我的Linux虚拟机没有安装portmap,需要安装。执行下面的命令:sudo apt-get install portmap此时仍然报错:Cannot register service: RPC: Authentication error; why = Client credential too weak。这个看着百思不得其解,因为参数设置不是按照参考代码就是自动生成的(只有program编号是根据划分规则修改的),完全不知道错在哪里,查了很多资料也一头雾水。最后看到了一项搜索结果的预览( 点击查看),提到了Fedora7出现这种问题改成超级用户即可。虽然现在虚拟机里用的是Ubuntu11.10,不过还是在运行编译好的文件前加了个sudo。输完密码果然显示需要的结果,这个小练习可以告一段落了。