1.13k likes | 1.3k Vues
第 9 章 查找. 9.1 检索的基本概念 9.2 线性表的检索 9.3 树表的检索 9.4 B 树 9.5 散列检索技术.
E N D
第9章 查找 • 9.1 检索的基本概念 • 9.2 线性表的检索 • 9.3 树表的检索 • 9.4 B树 • 9.5 散列检索技术
1、检索定义 (1)关键字 关键字(Key)是数据元素(或记录)中某个数据项的值,用它可以标识(或识别)一个数据元素(或记录)。在一个记录中,每个数据项都可作为该记录的关键字。 主关键字是指在一个记录众多的关键字中能唯一地标识一个数据元素(或记录)的关键字,其它的关键字称为从关键字,也叫辅助关键字。 (2)检索表 检索表是由同一类型的数据元素(或记录)组成的集合,是检索操作所依赖的数据结构。 9.1 检索的基本概念
(3)检索定义 检索,又叫查找,是根据给定的某个值,在一个检索表中确定一个其关键字等于给定值的记录或数据元素的操作运算。若检索表中存在这样的一条记录,则称检索是成功;若表中不存在这样的记录,则称检索是不成功的,是失败的,此时检索的结果在静态环境下可给出不存在此记录的提示信息;在动态环境下,可插入此关键字等于给定值的记录。
2、检索方法 • 依据数据的存储方式的不同,我们将检索分为线性表检索、树表检索和散列表检索等。3、平均检索长度 • 将“为检索到具有给定关键字值的数据元素或记录所需要关键字的比较次数的平均值”作为衡量检索算法好坏的依据,即通过平均检索长度来衡量。具体地说,即指为确定欲检索的记录在表中的位置,需与给定值进行比较的关键字个数的期望值,称为检索算法在检索成功时的平均检索长度(Average Search Length)。
其中,pi为检索第i个记录的概率且 。若不特别说明,均认为每个记录的检索概率相等(即为等概率情形),从而有p1 =p2=…=pn=1/n,ci为检索到第i个记录需要和给定关键值比较的关键字个数。一般情形下,ci依赖于所采用的检索方法。
9.2 线性表的检索 线性表的检索按照检索方法的不同可分为顺序检索、折半检索和分块检索。 9.2.1 顺序检索 1、基本思想 基本思想是:从表的第n个记录开始,用给定的值与表中各个记录的关键字逐个地进行比较,如果检索到某一个记录的关键字与给定值相等,则检索成功;若整个表中的记录均比较过,仍未检索到关键字等于给定值的记录,则检索失败。
2、类型定义与算法实现 typedef struct { keytype key ; /*关键字类型 */ elemtype other ; /*其他的域 */ } sqlist ; sqlist R[n+1] ; /* 顺序表 */ 顺序检索算法: int seqsrch (sqlist R[ ] , keytype k) { int i ; R[0].key = k ; /* 将R[0]作为监视哨 */ i = n ; /* 从第n个记录起向前扫描 */ while (R[i].key! =k) i-- ; if (i= =0) return(0) ; else return i ; } /*seqsrch *
3、顺序检索性能分析 在等概率情况下,顺序检索的平均检索长度为: 顺序检索成功时的平均比较次数约等于表长的一半。若所检索的给定值k不在表中,则需进行n+1次比较才可确定检索失败。其算法简单,且适用面广,对表的存储结构、记录是否按关键字有序等方面不作要求。
9.2.2 折半检索 • 1、基本思想 • 折半检索又称二分检索,其基本思想是:先将待检索的给定值和检索表的中间位置上的记录的关键字值进行比较,若相等,则检索成功,否则,若给定值比该中间位置上记录的关键字值大,则只要在右半部分中继续进行折半检索,否则,只要在左半部分中继续进行折半检索。这样,经过一次关键字比较就缩小一半的检索区间,…,如此进行下去,直到检索到关键字值为给定值的记录,或者未检索到,即检索失败。需注意的是,作为折半检索对象的表必须是顺序存储方式的有序表。
2、折半检索过程示例 • 已知一个含11个记录的有序表,其关键字序列为: • ( 08,10,12,19,25,31,39,42,47,52,57 ) • (1)检索k=12: 此时mid = 5,由于k = 12 < R[mid-1].key,只要在其左子表R[0]…R[4]中继续检索中继续进行折半检索。
此时mid = 2,由于 k =12 = R[mid].key,说明检索成功。 中间位置
(2)检索k=52的记录过程: 此时mid=5,由于k = 42 > R[mid].key,所以只要在其右子表中继续进行折半检索,即在R[6]…R[10]中继续检索。
此时mid = 8,由于k = 52 > R[mid].key,可缩小检索区间,即缩小为R[9]…R[10]。此时mid = 9 , 且有 k = 52 = R[mid].key,则检索成功。
(3)检索k=87的记录 此时,k = 87 > R[mid].key,则下一步为:
此时,k=87>R[mid].key,则下一步为: 此时,mid =9,k> R[mid].key ,则又缩小区间为R[10]…R[10],此时mid =10,由于此时k >R[mid].key,又缩小区间,但此时low =10+1>high,区间的下界大于上界,不能构成区间,说明检索失败。
3、折半检索算法 int binsrch( sqlist R[ ] , keytype k) {int low , high ,mid ;low = 0;high = n-1;while ( low <= high ) { mid =(low+high)/2 ; if (k = =R[mid].key) return (mid) ; else if (k<R[mid].key) high = mid-1 ; /*在左区间上检索*/ else low=mid+1 ; /*在右区间上检索*/ } return (0) ; } /* binsrch */
4、折半检索判定树 • 折半检索过程可以借助二叉树进行描述。把当前检索区间的中间位置上的记录或结点作为二叉树的根,左半区间和右半区间中的记录分别作为根的左子树和右子树,由此就可得到一棵二叉树,称为折半检索判定树。 • 如图8.1所示为具有11个记录的有序表的折半检索判定树表示。
5、折半检索的平均检索长度 • 折半检索的过程恰好是在判定树中走了一条从二叉树树根到被检索结点的路径,经历比较的关键字个数恰为该结点在树中的层数。折半检索成功时所进行的关键字比较的次数至多为 次。 • 在等概率情况下(即表中每个记录检索的概率相等,pi=1/n),检索成功时的折半检索的平均检索长度为(h 为判定树的深度,k为某层):
当n很大时,如n>100时,则可得近似公式:ASL≈log2(n+1)-1当n很大时,如n>100时,则可得近似公式:ASL≈log2(n+1)-1 折半检索的效率比顺序检索要高得多,但它只能适用于有序表,它一般只适用于那些有序表,且一旦建立后很少变动而又需要经常检索的线性表。
例如,一棵具有15个结点的判定树和其检索成功时的平均检索长度如图8.2所示。例如,一棵具有15个结点的判定树和其检索成功时的平均检索长度如图8.2所示。 ASL= (1+2×2+3×4+4×8)≈3.3
9.2.3 分块检索 1、定义 分块检索又称索引顺序检索,属于索引检索,是一种介于顺序检索与折半检索之间的检索方法。分块检索要求将表中数据元素分成很多子表(块)后,数据元素的关键字在块与块之间是有序,而在块内不一定有序。2、分块检索的基本思想 首先依据待检索的给定值在索引表中进行检索,由于索引表是一个有序表,所以可采用折半检索(也可采用顺序检索)以确定待检索记录属于哪一块;然后在已确定的那一块内进行顺序检索,如图8.3。
索引表有序 块内无序 图8.3 表及索引表
3、分块检索算法 (1)当顺序检索索引表时: #define M 99 typedef struct { keytype key ; int stadr ; int len ; } indexlist ; /*索引表的类型定义*/ indexlist ID[M] ; /*ID为具有indexlist类型的R上的一个索引表*/
int Blocksrch( R,ID,m,k ) sqlist R[ ] ; indexlist ID[ ] ; /* 索引表ID */ keytype k ; int m ; { int i, j i = 0 ; while((i <= m-1)&&(k > ID[i].key)) /* 顺序检索索引表 */ i + +; if ( i>m-1 ) retutn(0) /* 检索失败 */ else { j = R[i].stadr; /* 待检索块的第一个记录的下标 */ while((j < ID[i].stadr+ID[i].len)&&(k != R[j].key)) j ++ ; /* 第i块中顺序检索给定值为K的记录元素 */ if (j == ID[i].stadr + ID[i].len) retutn(0) /*块中检索失败 */ else return (j) ; /* 检索成功 */ } } /* Blocksrch */
(2)当折半检索索引表时: • indexlist ID[M] ; /*ID为具有indexlist类型的R上的一个索引表*/ int Blocksrch(sqlist R[ ],indexlist ID[ ],int m,keytype k ) • { int i,j,low1,low2,mid,high1,high2; • low1=0; high1=m-1; /*置折半检索区间的下、上界的初值*/ • while ( low1<=high1 ) /*在索引表中检索*/ { mid =(low1+high1)/2 ; /* 求当前区间的中间位置 */ if (k<=ID[mid].key) high1 = mid-1 ; /*在左区间上检索*/
else • low1=mid+1 ; /*在右区间上检索*/ } /*检索结束,low1为块号*/ if (low1<m) • {low2=ID[low1].stadr; /*块起始地址*/ • if (low1= =m-1) high2=n-1; /*块末地址*/ • else • high2=ID[low1+1].stadr-1; • for (i=low2;i<=high2;i++) /* 块内顺序检索*/ • if (R[i].key= =k) return (i); /* 检索成功 */ } • retutn(0) /* 检索失败 */ } /* Blocksrch */
4、分块检索的平均检索长度 (1)若用顺序检索确定所在的块时: ASL = Lb + Lw 其中,pbj = ,pwi = , Cbj = j ,Cwi = i;
(2)若用折半检索确定所在块时: 分块检索平均检索长度界于折半检索和顺序检索之间。 只要一个检索表的数据分布特性符合分块后有序,即可采用分块检索,且其对表的存储结构没有特殊要求。
9.3 树表的检索 • 在线性表的检索中,折半检索方法效率最高,但要求表以顺序存储方式存储,不能用链表作存储结构,而且表中元素按关键字有序。由于在顺序存储结构中,若要频繁地进行插入和删除结点运算时,必须移动大量的元素,而这会花费相当多的时间。利用二叉搜索树作为表的数据组织方式就可以既能把链式存储结构的优点和折半检索方法的高效率结合在一起,又不要求表为有序表。
9.3.1 二叉检索树 1.二叉检索树的定义与性质 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是一棵具有下列特性的非空二叉树: (l)若它的左子树非空,则左子树上所有结点的值均小于根结点的值; (2)若它的右子树非空,则右子树上所有结点的值均大于(若允许具有相同值的结点存在,则大于等于)根结点的值;且左、右子树本身又各是一棵二叉排序树。图8.4所示即为二棵二叉排序树。
当将一组数值大小无序的数据元素构造成一棵二叉检索树后,再对此二叉检索树进行中序遍历运算,所得到的中序遍历序列是一个非递减的有序序列。当将一组数值大小无序的数据元素构造成一棵二叉检索树后,再对此二叉检索树进行中序遍历运算,所得到的中序遍历序列是一个非递减的有序序列。
2、二叉检索树的结点结构:typedef struct node { keytype key ; /*关键字域*/ elemtype other ; /*其他数据域 */ struct node *lchild,*rchild ; } bilist /*二叉检索树的结点结构*/
3、二叉检索树的检索 • (1)检索过程 • 首先将一个无序序列的各个数据元素按照先后次序分放到一棵二叉检索树中,即构造一棵二叉检索树,然后根据二叉检索树的定义进行检索。其检索过程为:若二叉检索树为空,则表明检索失败,返回一个特定值,否则,若给定值k等于二叉检索树根结点的关键字值,则说明检索成功,返回当前指向根结点的指针;若给定值k小于根结点的关键字值,则说明待检索数据元素只可能在左子树中,继续在根的左子树中检索;若给定值大于根结点的关键字值,则说明待检索数据元素只可能在右子树
中,继续在根的右子树中检索。 • 在一棵二叉检索树上检索一个关键字值等于给定值的结点的过程,若检索成功,实际上是走了一条从根结点到该结点的路径。如图8.5中右边的实线所示为检索k为80时的检索成功的过程,左边的虚线所示为检索k为15时检索不成功的过程。
(2)检索算法 bilist *bstsrch (t,k) bilist *t ; keytype k; { if ((t = = NULL)||(t->key = =k)) return (t); else if (t->key < k) return (bstsrch(t->rchild,k)); /*检索右子树*/ else return (bstsrch(t->lchild,k)); /*检索左子树*/ } /*bstsrch */
4、二叉检索树的构造 构造过程: 可以从一棵初始为空的二叉检索树开始,依次输入数据元素,每当读入一个数据,就生成一个结点,然后把它们依次插入到二叉树的适当位置上。插入运算可以按以下算法实现: (1)若二叉检索树为空,则将新插入结点作为根结点而插入到空树中; (2)若二叉检索树非空,首先将待插入结点的关键字值和树根结点的关键字值进行比较,若待插入结点的关键字值大于根结点的关键字值,则将待插入结点插入到当前根结点的右子树中,否则插入到其左子树中;
若相等,则说明树中已有结点,不必插入。同样,在左、右子树中的插入过程与在树中的插入过程相同,这样不断进行,直到将待插入结点作为一个新的叶子结点插入到一棵二叉检索树中为止。若相等,则说明树中已有结点,不必插入。同样,在左、右子树中的插入过程与在树中的插入过程相同,这样不断进行,直到将待插入结点作为一个新的叶子结点插入到一棵二叉检索树中为止。
(2)算法实现 • 插入算法描述: • bilist *insert (t,s) • bilist *t ,*s ; {bilist *p , *q ; if (t = = NULL) return (s) ; /*若为空树,*s作为根结点*/ p = t ; while ( p!= NULL) { q = p ; • if (s->key = = p->key ) return (t) ;
else { if (s->key>p->key ) p = p->rchild ; else p =p->lchild ; } } if (s->key>q->key) q->rchild = s ; else q->lchild= s ; return (t) ; } /*insert*/
二叉检索树构造算法描述: bilist *bstcreat( ) { bilist *t,*s ;keytype k ;int i,n ; elemtype data ; t = NULL ; /* 设二叉检索树的初态为空树 */ scantf (“ %d ”,&n ) ; for (i =l; i<=n; i++) { scanf (“ %d”,&key ); s = (bilist* ) malloc(sizeof(bilist)); s->lchild = NULL ; s-> rchild = NULL; s-> key = key ; s->other = data; /*此处假设其它域data已读入 */ t = insert( t,s ) ; } } /*bstcreat */
5.二叉检索树的结点删除 (1)基本思想: ① 若要删除的是叶子结点 删除一个叶子结点(终端结点)只要将其双亲结点与它之间相链接的指针置空即可。例如,要在图8.6中删除结点100时,只要将结点100与其双亲相链接的指针去掉即可,删除后的二叉检索树如图8.7所示。 ② 若要删除的结点只有左子树或只有右子树 即单支结点,删除该结点时,要将其唯一的与后继结点相链接的指针链接它所在的链接位置上,即将被删除结点*p的左子树或右子树直接成为双亲*f的
左子树或右子树。如图8.6中,要删除45结点,只要将其的右孩子指针直接赋给其双亲40的右孩子指针即可,同理,可以删除90结点。删除后的二叉检索树如图8.8所示。左子树或右子树。如图8.6中,要删除45结点,只要将其的右孩子指针直接赋给其双亲40的右孩子指针即可,同理,可以删除90结点。删除后的二叉检索树如图8.8所示。
③ 若要删除的结点左、右子树均不空 常用的方法:先将被删除结点的中序前趋结点的值域赋给该结点的值域,然后再删除它的中序前趋结点,将中序前趋结点的左孩子指针链接到中序前趋结点所在的链接位置即可。如图8.6中,要删除结点40(设为A点),首先将该结点的中序前趋结点的值30赋给A点(即以30结点顶替40结点),然后删除30结点。删除后的二叉检索树如图8.9所示。 另外一种方法是将被删除结点的右子树链接到它的中序前趋结点的右孩子指针域,然后把被删结点的左子树直接链接到它的双亲的左孩子指针域上。如图8.6中,要删除结点40,首先可将该结点的右子树下
接到其中序前趋结点30的右孩子指针域上,然后将该结点的左子树上接到其双亲结点50上,作为它的左子树,此时就实现了删除操作,如图8.10所示。但这种删除方法往往会增加二叉树的深度,不经常采用。接到其中序前趋结点30的右孩子指针域上,然后将该结点的左子树上接到其双亲结点50上,作为它的左子树,此时就实现了删除操作,如图8.10所示。但这种删除方法往往会增加二叉树的深度,不经常采用。 其它删除方法:用待删除结点的中序后继结点替换待删除结点,或者把待删除结点的左子树链接到它的中序后继结点的左指针域,并把它的右子树直接链接在其双亲结点的相应链域中。