740 likes | 948 Views
第 8 章 查 找. 8.1 查找的基本概念 8.2 基于线性表的查找法 8.3 基于树的查找法 8.4 计算式查找法 —— 哈希法. 8.1 查找的基本概念. 列表: 由同一类型的数据元素(或记录)构成的集合, 可利用任意数据结构实现。 关键字: 数据元素的某个数据项的值,用它可以标识列表中的一个或一组数据元素。如果一个关键字可以唯一标识列表中的一个数据元素, 则称其为主关键字,否则为次关键字。当数据元素仅有一个数据项时, 数据元素的值就是关键字。.
E N D
第8章 查 找 8.1 查找的基本概念 8.2 基于线性表的查找法 8.3 基于树的查找法 8.4 计算式查找法——哈希法
8.1 查找的基本概念 列表:由同一类型的数据元素(或记录)构成的集合, 可利用任意数据结构实现。 关键字:数据元素的某个数据项的值,用它可以标识列表中的一个或一组数据元素。如果一个关键字可以唯一标识列表中的一个数据元素, 则称其为主关键字,否则为次关键字。当数据元素仅有一个数据项时, 数据元素的值就是关键字。
查找: 根据给定的关键字值,在特定的列表中确定一个其关键字与给定值相同的数据元素,并返回该数据元素在列表中的位置。若找到相应的数据元素, 则称查找是成功的,否则称查找是失败的,此时应返回空地址及失败信息,并可根据要求插入这个不存在的数据元素。显然,查找算法中涉及到三类参量: ① 查找对象K(找什么); ② 查找范围L(在哪找); ③ K在L中的位置(查找的结果)。其中①、②为输入参量, ③为输出参量,在函数中,输入参量必不可少,输出参量也可用函数返回值表示。
平均查找长度:为确定数据元素在列表中的位置, 需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度。对于长度为n的列表, 查找成功时的平均查找长度为: 其中Pi为查找列表中第i个数据元素的概率,Ci为找到列表中第i个数据元素时,已经进行过的关键字比较次数。由于查找算法的基本运算是关键字之间的比较操作,所以可用平均查找长度来衡量查找算法的性能。
查找的基本方法可以分为两大类,即比较式查找法和计算式查找法。其中比较式查找法又可以分为基于线性表的查找法和基于树的查找法,而计算式查找法也称为HASH(哈希)查找法。
8.2 基于线性表的查找法 8.2.1 顺序查找法 顺序查找法的特点是,用所给关键字与线性表中各元素的关键字逐个比较,直到成功或失败。存储结构通常为顺序结构,也可为链式结构。 下面给出顺序结构有关数据类型的定义: #define LIST_SIZE 20 typedef struct { KeyType key;
OtherType other_data; } RecordType; typedef struct { RecordType r[LIST_SIZE+1]; /* r[0]为工作单元 */ int length; } RecordList;
基于顺序结构的算法如下: int SeqSearch(RecordList l, KeyType k) /*在顺序表l中顺序查找其关键字等于k的元素, 若找到, 则函数值为该元素在表中的位置,否则为0*/ { l.r[0].key=k; i=l.length; while (l.r[i].key![KG-*2]=k) i--; return(i); } 【算法8.1 设置监视哨的顺序查找法】
其中l.r[0]称为监视哨,可以起到防止越界的作用。不用监视哨的算法如下: int SeqSearch(RecordList l, KeyType k) /*不用监视哨法, 在顺序表中查找关键字等于k的元素*/ { l.r[0].key=k; i=l.length; while (i>=1&&l.r[i].key![KG-*2]=k) i--; if (i>=1) return(i) else return (0); } 【算法8.2 不设置监视哨的顺序查找法】
其中,循环条件 i>=1 判断查找是否越界。 利用监视哨可省去这个条件,从而提高查找效率。 下面用平均查找长度来分析一下顺序查找算法的性能。假设列表长度为n,那么查找第i个数据元素时需进行n-i+1次比较,即Ci=n-i+1。又假设查找每个数据元素的概率相等,即Pi=1/n, 则顺序查找算法的平均查找长度为:
8.2.2 折半查找法 折半查找法又称为二分法查找法,这种方法要求待查找的列表必须是按关键字大小有序排列的顺序表。其基本过程是:将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表, 如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。图8.1给出了用折半查找法查找12、50的具体过程, 其中mid=(low+high)/2,当high<low时,表示不存在这样的子表空间,查找失败。
折半查找的算法如下: int BinSrch (SqList l, KeyType k) /*在有序表l中折半查找其关键字等于k的元素, 若找到, 则函数值为该元素在表中的位置*/ { low=1 ; high=l.length; /*置区间初值*/ while ( low<=high)
{ mid=(low+high) / 2; if (k==l.r[mid].key) return(mid); /*找到待查元素*/ else if (k<l.r[mid]. key) high=mid-1; /*未找到, 则继续在前半区间进行查找*/ else low=mid+1; /*继续在后半区间进行查找*/ } return (0); } 【算法8.3 折半查找法】
折半查找过程可用一个称为判定树的二叉树描述,判定树中每一结点对应表中一个记录,但结点值不是记录的关键字,而是记录在表中的位置序号。根结点对应当前区间的中间记录, 左子树对应前一子表,右子树对应后一子表。显然,找到有序表中任一记录的过程,对应判定树中从根结点到与该记录相应的结点的路径,而所做比较的次数恰为该结点在判定树上的层次数。因此,折半查找成功时,关键字比较次数最多不超过判定树的深度。
8.2.3 分块查找法 分块查找法要求将列表组织成以下索引顺序结构: ·首先将列表分成若干个块(子表)。一般情况下,块的长度均匀, 最后一块可以不满。每块中元素任意排列,即块内无序,但块与块之间有序。 ·构造一个索引表。其中每个索引项对应一个块并记录每块的起始位置,以及每块中的最大关键字(或最小关键字)。索引表按关键字有序排列。 图8.2所示为一个索引顺序表。其中包括三个块,第一个块的起始地址为0,块内最大关键字为25;第二个块的起始地址为5, 块内最大关键字为58;第三个块的起始地址为10,块内最大关键字为88。
分块查找的基本过程如下: (1)首先,将待查关键字K与索引表中的关键字进行比较, 以确定待查记录所在的块。具体的可用顺序查找法或折半查找法进行。 (2)进一步用顺序查找法,在相应块内查找关键字为K的元素。 例如,在上述索引顺序表中查找36。首先,将36与索引表中的关键字进行比较,因为25<36≤58,所以36在第二个块中, 进一步在第二个块中顺序查找, 最后在8号单元中找到36。
分块查找的平均查找长度由两部分构成, 即查找索引表时的平均查找长度为LB,以及在相应块内进行顺序查找的平均查找长度为LW。 ASLbs=LB+LW 假定将长度为n的表分成b块,且每块含s个元素,则b=n/s。又假定表中每个元素的查找概率相等,则每个索引项的查找概率为1/b,块中每个元素的查找概率为1/s。若用顺序查找法确定待查元素所在的块,则有 :
将 代入, 得 若用折半查找法确定待查元素所在的块,则有
8.3 基于树的查找法 基于树的查找法又称为树表查找法,是将待查表组织成特定树的形式并在树结构上实现查找的方法,主要包括二叉排序树、 平衡二叉排序树和B树等。 8.3.1 二叉排序树 二叉树排序树或者是一棵空树, 或者是具有如下性质的二叉树: (1) 若它的左子树非空, 则左子树上所有结点的值均小于根结点的值; (2) 若它的右子树非空, 则右子树上所有结点的值均大于根结点的值; (3) 它的左右子树也分别为二叉排序树。
在下面讨论的二叉排序树的操作中,使用二叉链表作为存储结构,其结点结构说明如下: typedef struct node { KeyType key ; /*关键字的值*/ struct node *lchild, *rchild; /*左右指针*/ }BSTNode, *BSTree;
1. 二叉排序树的插入和生成 已知一个关键字值为key的结点s, 若将其插入到二叉排序树中,只要保证插入后仍符合二叉排序树的定义即可。插入可以用下面的方法进行: ① 若二叉排序树是空树,则key成为二叉排序树的根;② 若二叉排序树非空, 则将key与二叉排序树的根进行比较,如果key的值等于根结点的值,则停止插入;如果key的值小于根结点的值,则将key插入左子树;如果key的值大于根结点的值,则将key插入右子树。相应的递归算法如下:
void InsertBST(BSTree *bst, KeyType key) • /*若在二叉排序树中不存在关键字等于key的元素, 插入该元素 */ • { BSTree s; • if (*bst==NULL) /*递归结束条件*/ • {s=(BSTree)malloc(sizeof(BSTNode)); /*申请新的结点s*/ • s-> key=key; • s->lchild=NULL; s->rchild=NULL; • *bst=s; • } • else if (key < (*bst)->key) • InsertBST(&((*bst)->lchild), key); /*将s插入左子树*/ • else if (key > (*bst)->key) • InsertBST(&((*bst)->rchild), key); /*将s插入右子树*/ • } 【算法8.4 二叉排序树的插入】
假若给定一个元素序列, 我们可以利用上述算法创建一棵二叉排序树。首先,将二叉排序树初始化为一棵空树,然后逐个读入元素,每读入一个元素,就建立一个新的结点并插入到当前已生成的二叉排序树中,即调用上述二叉排序树的插入算法将新结点插入。生成二叉排序树的算法如下: void CreateBST(BSTree *bst) /*从键盘输入元素的值, 创建相应的二叉排序树*/ { KeyType key; *bst=NULL; scanf(″%d″, &key); while (key![KG-*2]=ENDKEY) /* ENDKEY为自定义常数 */ { InsertBST(bst, key); scanf(″%d″, &key); } }
2. 二叉排序树的删除 从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去, 只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变。也就是说,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点。 删除操作首先要查找,已确定被删结点是否在二叉排序树中。若不在 ,则不做任何操作;否则,假设要删除的结点为p,结点p的双亲结点为f,并假设结点p是结点f的左孩子(右孩子的情况类似)。
下面分三种情况讨论: (1) 若p为叶子结点,则可直接将其删除:f->lchild=NULL; free(p); (2) 若p结点只有左子树,或只有右子树,则可将p的左子树或右子树直接改为其双亲结点f的左子树,即:f->lchild=p->lchild(或f->lchild=p->rchild); free(p); (3) 若p既有左子树, 又有右子树, 如图8.6 (a) 所示。 此时有两种处理方法:
方法1:首先找到p结点在中序序列中的直接前驱s结点,如图8.6 (b) 所示,然后将p的左子树改为f的左子树,而将p的右子树改为s的右子树:f->lchild=p->lchild;s->rchild= p->rchild; free(p);结果如图8.6 (c) 所示。 方法2: 首先找到p结点在中序序列中的直接前驱s结点, 如图8.6 (b) 所示,然后用s结点的值替代p结点的值,再将s结点删除,原s结点的左子树改为s的双亲结点q的右子树:p->data=s->data;q->rchild= s->lchild;free(s);结果如图8.6 (d) 所示。
综上所述,可以得到下面的在二叉排序树中删去一个结点的算法。综上所述,可以得到下面的在二叉排序树中删去一个结点的算法。 BSTNode * DelBST(BSTree t, KeyType k) /*在二叉排序树t中删去关键字为k的结点*/ { BSTNode *p, *f, *s , *q; p=t; f=NULL; while(p) /*查找关键字为k的待删结点p*/ { if(p->key==k) break; /*找到, 则跳出查找循环*/ f=p; /*f指向p结点的双亲结点*/ if(p->key>k)p=p->lchild; else p=p->rchild; } if(p==NULL) return t; /*若找不到, 返回原来的二叉排序树*/ if(p->lchild==NULL) /*p无左子树*/ {if(f==NULL) t=p->rchild; /*p是原二叉排序树的根*/ else if(f->lchild==p) /*p是f的左孩子*/ f->lchild=p->rchild ; /*将p的右子树链到f的左链上*/
else /*p是f的右孩子*/ f->rchild=p->rchild ; /*将p的右子树链到f的右链上*/ free(p); /*释放被删除的结点p*/ } else /*p有左子树*/ { q=p; s=p->lchild; while(s->rchild) /*在p的左子树中查找最右下结点*/ {q=s; s=s->rchild; } if(q==p) q->lchild=s->lchild ; /*将s的左子树链到q上*/ else q->rchild=s->lchild; p->key=s->key; /*将s的值赋给p*/ free(s); } return t; } /*DelBST*/ 【算法8.6 在二叉排序树中删除结点】
3. 二叉排序树的查找 • 因为二叉排序树可看作是一个有序表,所以在二叉排序树上进行查找与折半查找类似,也是一个逐步缩小查找范围的过程。 根据二叉排序树的特点,首先将待查关键字k与根结点关键字t进行比较,如果: • (1) k=t, 则返回根结点地址; • (2) k<t, 则进一步查左子树; • (3) k>t, 则进一步查右子树。
显然, 这是一个递归过程。 可用如下递归算法实现: BSTree SearchBST(BSTree bst, KeyType key) /*在根指针bst所指二叉排序树中, 递归查找某关键字等于key的元素, 若查找成功,则返回指向该元素结点指针, 否则返回空指针 */ { if (!bst) return NULL; else if (bst-> key==key) return bst; /*查找成功*/ else if (key < bst-> key) return SearchBST(bst->lchild, key); /*在左子树中继续查找*/ else return SearchBST(bst->rchild, key); /*在右子树中继续查找*/ } 【算法8.7 二叉排序树查找的递归算法】
根据二叉排序树的定义,二叉排序树查找的递归算法可以用循环方式直接实现。二叉排序树的非递归查找过程如下: BSTree SearchBST(BSTree bst, KeyType key) /*在根指针bst所指二叉排序树bst上,查找关键字等于key的结点, 若查找成功, 则返回指向该元素结点指针, 否则返回空指针 */ { BSTree q; q=bst; while(q) {if (q->key==k) return q; /*查找成功*/
if (key < q->data.key) q=q->lchild; /*在左子树中查找*/ else q=q->rchild; /*在右子树中查找*/〖ZK)〗 } return NULL; /*查找失败*/ }/*SearchBST*/ 【算法8.8 二叉排序树非递归查找算法】
4. 二叉排序树的查找性能 图8.7 二叉排序树的不同形态
8.4 计算式查找法——哈希法 哈希法又称散列法、杂凑法或关键字地址计算法等,相应的表称为哈希表。 这种方法的基本思想是:首先在元素的关键字k和元素的存储位置p之间建立一个对应关系H,使得p=H(k),H称为哈希函数。创建哈希表时,把关键字为k的元素直接存入地址为H(k)的单元;以后当查找关键字为k的元素时,再利用哈希函数计算出该元素的存储位置p=H(k),从而达到按关键字直接存取元素的目的。
当关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上,即 k1≠k2,但 H(k1)=H(k2),这种现象称为冲突,此时称k1和k2为同义词。实际中,冲突是不可避免的, 只能通过改进哈希函数的性能来减少冲突。 综上所述, 哈希法主要包括以下两方面的内容: (1) 如何构造哈希函数; (2) 如何处理冲突。
8.4.1 哈希函数的构造方法 1. 数字分析法 如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位, 构成哈希地址。例如,有80个记录,关键字为8位十进制整数d1d2d3…d7d8,如哈希表长取100,则哈希表的地址空间为: 00~99。假设经过分析,各关键字中d4和d7的取值分布较均匀, 则哈希函数为:H(key)=H(d1d2d3…d7d8)=d4d7。例如, H(81346532)=43,H(81301367)=06。 相反, 假设经过分析, 各关键字中 d1和d8的取值分布极不均匀,d1都等于5,d8都等于2,此时,如果哈希函数为:H(key)=H(d1d2d3…d7d8)=d1d8, 则所有关键字的地址码都是52,显然不可取。
2. 平方取中法 当无法确定关键字中哪几位分布较均匀时, 可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。 例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、 “BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址, 如表8 - 1所示。
3. 分段叠加法 • 2 3 • 0 3 • 4 7 • 1 2 • 0 2 0 • 2 3 • 3 0 6 • 4 7 • 2 1 1 • 0 2 0 +) +) 1 1 0 5 9 0 7 (a) 移位叠加 (b) 折叠叠加 图8.23 由叠加法求哈希地址
4. 除留余数法 • 假设哈希表长为m, p为小于等于m的最大素数, 则哈希函数为 • H(k)=k%p, 其中%为模p取余运算。 • 例如,已知待散列元素为(18,75,60,43,54,90,46),表长m=10, p=7, 则有 • H(18)=18 % 7=4 H(75)=75 % 7=5 H(60)=60 % 7=4 • H(43)=43 % 7=1 H(54)=54 % 7=5 H(90)=90 % 7=6 • H(46)=46 % 7=4
此时冲突较多。 为减少冲突, 可取较大的m值和p值, 如m=p=13, 结果如下: H(18)=18 % 13=5 H(75)=75 % 13=10 H(60)=60 % 13=8 H(43)=43 % 13=4 H(54)=54 % 13=2 H(90)=90 % 13=12 H(46)=46 % 13=7 0 1 2 3 4 5 6 7 8 9 10 11 12 图8.24 除留余数法求哈希地址
5. 伪随机数法 • 采用一个伪随机函数作哈希函数,即H(key)=random(key)。 • 在实际应用中, 应根据具体情况, 灵活采用不同的方法, 并用实际数据测试它的性能,以便做出正确判定。 通常应考虑以下五个因素: • ·计算哈希函数所需的时间(简单)。 • ·关键字的长度。 • ·哈希表的大小。 • ·关键字的分布情况。 • · 记录查找的频率。