第八章查找——内容简介
在非数值运算问题中,数据存储量一般很大,为了在大量信息中找到某些值,需要用到查找技术,为了提高查找效率,需要对一些数据进行排序。因而查找和排序是重要的处理技术。
【本章要点】
l 查找基本概念
l 基于线性表的查找
l 基于树表的查找
l 计算式查找
第1讲、查找的基本概念——内容简介
【本讲要点】
l 查找的基本概念
l 查找的方法
第 1 讲、查找的基本概念——教学讲义
一、查找有关的基本概念
列表:由同一类型的数据元素(或记录)构成的集合,可利用任意数据结构实现。
关键字:数据元素的某个数据项的值,用它可以标识列表中的一个或一组数据元素。
如果一个关键字可以惟一标识列表中的一个数据元素,则称其为主关键字,否则为次关键字。
当数据元素仅有一个数据项时,数据元素的值就是关键字。
查找:根据给定的关键字值,在特定的列表中确定一个其关键字与给定值相同的数据元素,并返回该数据元素在列表中的位置。
若找到相应的数据元素,则称查找是成功的,否则称查找是失败的,此时应返回空地址及失败信息,并可根据要求插入这个不存在的数据元素。
对于表的查找,一般有两种情况,一种是静态查找,指在查找过程中只是对数据元素进行查找;
另一种是动态查找,指在实施查找的同时,插入找不到的元素,或从查找表中删除已查到的某个元素,即允许表中元素变化。
查找算法中会涉及到三类参量:
- ①查找对象 K(找什么);
- ②查找范围 L(在哪找);
- ③K在 L 中的位置(查找的结果)。
其中①、②为输入参量 ,③为输出参量,在函数中,输入参量必不可少,输出参量也可用函数返回值表示。
平均查找长度:为确定数据元素在列表中的位置,需和给定值进行比较的关键字个数的期望值,
称为查找算法在查找成功时的平均查找长度。
对于长度为 n 的列表,查找成功时的平均查找长度为:
其中 Pi 为查找列表中第 i 个数据元素的概率,Ci 为找到列表中第 i 个数据元素时,已经进行过的关键字比较次数。
由于查找算法的基本运算是关键字之间的比较操作,所以可用平均查找长度来衡量查找算法的性能。
二、查找的基本方法
查找的基本方法可以分为两大类,即比较式查找法和计算式查找法。
其中比较式查找法又可以分为基于线性表的查找法和基于树的查找法,
而计算式查找法也称为 HASH(哈希)查找法。
习题巩固
采用顺序查找法查找长度为n的线性表时,平均查找长度为 。
-
A.n
-
B.
-
C.
-
D.
通常将 作为衡量一个查找算法效率优劣的标准。
-
A.平均查找长度
-
B.比较次数
-
C.WPL
-
D.ASL
顺序查找方法只能在顺序存储结构上进行。( )
-
A.ture
-
B.false
顺序查找含n个元素的顺序表,若查找成功,则比较关键字的次数最多为 n 次。
顺序查找含n个元素的顺序表,若查找不成功,则比较关键字的次数为 n+1 次。
第2讲 基于线性表的查找法——内容简介
【本节要点】
l 顺序查找法
n 顺序查找算法
n 性能分析
l 折半查找法
n 折半查找算法
n 性能分析
l 分块查找法
n 分块查找算法
n 性能分析
第 2 讲 基于线性表的查找法——教学讲义
基于线性表的查找法具体可分为顺序查找法、折半查找法以及分块查找法。
一、顺序查找法
顺序查找法的特点是,用所给关键字与线性表中各元素的关键字逐个比较,直到成功或失败。存储结构通常为顺序结构,也可为链式结构。
顺序结构数据类型的定义:
#define LIST_SIZE 20
typedef struct {
KeyType key;
OtherType other_data;
} RecordType;
typedef struct {
RecordType r[LIST_SIZE + 1]; /* r[0]为工作单元 */
int length;
} RecordList;
[算法思想]:
在表的一端设置一个称为“监视哨”的附加单元,存放要查找元素的关键字。
从表的另一端开始查找,如果在“监视哨”找到要查找元素的关键字,返回失败信息,否则返回相应下标。
[算法描述] 【算法 1 设置监视哨的顺序查找法】
设置监视哨的顺序查找法
int SeqSearch(RecordList l, KeyType k)
/*在顺序表l中顺序查找其关键字等于k的元素,若找到,则函数值为该元素在表中的位置,否则为0*/
{
int i;
l.r[0].key=k;
i=l.length;
while (l.r[i].key!=k) i--;
return(i);
}
[算法分析]:
用平均查找长度(ASL)分析顺序查找算法的性能。
假设列表长度为 n,那么查找第 i个数据元素时需进行 n-i+1 次比较,即 Ci=n-i+1。
又假设查找每个数据元素的概率相等,即Pi=1/n,则顺序查找算法的平均查找长度为:
为便于比较,下面给出不用“监视哨”的算法。
【算法 2 不设置监视哨的顺序查找法】
int SeqSearch(RecordList l, KeyType k)
/*不用"监视哨"法,在顺序表中查找关键字等于k的元素*/
{
int i;
i=l.length;
while (i>=1&&l.r[i].key!=k) i--;
if (i>=1)
return(i);
else
return (0);
}
算法 2 与算法 1 相比,循环控制条件中增加了 i>=1,用以判断查找过程是否越界。加上“监视哨”可省去这个条件,从而提高查找效率。
二、折半查找法
折半查找法又称为二分查找法,这种方法对待查找的列表有两个要求:(1)必须采用顺序存储结构;(2)必须按关键字大小有序排列。
[算法思想]:
首先,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;
否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
下图给出了用折半查找法查找 12、50 的具体过程,其中 mid=(low+high)/2,当 high<low时,表示不存在这样的子表空间,查找失败。
[算法描述]:【算法 :折半查找法】
int BinSrch(RecordList l, KeyType k)
/*在有序表l中折半查找其关键字等于k的元素,若找到,则函数值为该元素在表中的
位置*/
{
int low,high,mid;
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);
}
[算法分析]:
用平均查找长度(ASL)分析折半查找算法的性能。
折半查找过程可用二叉判定树的描述,判定树中每一结点对应表中一个记录,但结点值不是记录的关键字,而是记录在表中的位置序号。
根结点对应当前区间的中间记录,左子树对应前一子表,右子树对应后一子表。
显然,找到有序表中任一记录的过程,对应判定树中从根结点到与该记录相应的结点的路径,而所做比较的次数恰为该结点在判定树上的层次数。
因此,折半查找成功时,关键字比较次数最多不超过判定树的深度。
由于判定树的叶结点所在层次之差最多为 1,故 n 个结点的判定树的深度与 n 个结点的完全二叉树的深度相等,
均为 。这样,折半查找成功时,关键字比较次数最多不超过。
相应地,折半查找失败时,对应判定树中从根结点到某个含空指针的结点的路径,因此,折半查找成功时,关键字比较次数最多也不超过判定树的深度。
为便于讨论,假定表的长度 n=2h-1,则相应判定树必为深度是 h的满二叉树,。
又假设每个记录的查找概率相等,则折半查找成功时的平均查找长度为:
折半查找方法的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表
例如对上述含 11 个记录的有序表,其折半查找过程可如上图的二叉判定树表示。
二叉树中结点内的数值表示有序表中记录的序号,如二叉树的根结点表示有序表中第 6 个记录,
图中的两条虚线分别表示上述查找关键字等于 12 的记录的过程,虚线经过的结点正是查找过程中和给定值比较过的记录,
因此,记录在判定树上的“层次”恰为找到此记录时所需进行的比较次数。
例如在长度为 11 的表中查找第 2 个记录时需要的比较次数为 4,因为该记录在判定树上位于第 4 层,
查找过程中给定值先后和表中第 6、第 3、第 1 和第 2 个记录的关键字相比较。
假设每个记录的查找概率相同,则从上图所示判定树可知,对长度为 11 的有序表进行折半查找的平均查找长度为
三、 分块查找法
分块查找法要求将列表组织成以下索引顺序结构:
l 首先将列表分成若干个块(子表)。一般情况下,块的长度均匀,最后一块可以不满。每块中元素任意排列,即块内无序,但块与块之间有序。
l 构造一个索引表
其中每个索引项对应一个块并记录每块的起始位置,以及每块中的最大关键字(或最小关键字)。
索引表按关键字有序排列。
下图所示为一个索引顺序表。
其中包括三个块,第一个块的起始地址为 0,块内最大关键字为 25;第二个块的起始地址为 5,块内最大关键字为 58;第三个块的起始地址为 10,块内最大关键字为 88。
分块查找的基本过程如下:
⑴ 首先,将待查关键字 K 与索引表中的关键字进行比较,以确定待查记录所在的块。具体的可用顺序查找法或折半查找法进行。
⑵ 进一步用顺序查找法,在相应块内查找关键字为 K 的元素。
例如,在上述索引顺序表中查找 36。首先,将 36 与索引表中的关键字进行比较,因为25<36≤58,所以 36 在第二个块中,进一步在第二个块中顺序查找,最后在 8 号单元中找到 36。
分块查找的平均查找长度由两部分构成,即查找索引表时的平均查找长度为 LB,以及在相应块内进行顺序查找的平均查找长度 LW。ASLbs=LB+LW
假定将长度为 n 的表分成 b 块,且每块含 s 个元素,则 b=n/s。又假定表中每个元素的查找概率相等,则每个索引项的查找概率为 1/b,块中每个元素的查找概率为 1/s。若用顺序查找法确定待查元素所在的块,则有
习题巩固
对列表进行折半查找时,要求列表必须 。
-
A.顺序存储
-
B.链式存储
-
C.顺序存储且元素按关键字有序存储
-
D.链式存储且元素按关键字有序存储
当采用分块查找时,数据的组织方式要求 。
-
A.数据分成若干块,每块内元素有序
-
B.数据分成若干块,每块内元素不必有序,但块间必须有序,且每块内最大(或最小)的数据组成索引块;
-
C.数据分成若干块
-
D.数据分成若干块,每块(除最后一块外)中元素个数相等。
有一个有序表{1,3,9,12,32,41,45,62,75,77,82,95,99}当采用折半查找法查找关键字为82的元素时,需经过 次比较后查找成功。
-
A.1
-
B.2
-
C.4
-
D.8
折半查找可以在有序的双向链表上进行。( )
-
A.ture
-
B.false
第3讲 基于树表的查找——内容简介
【本讲要点】
基于树的查找法是将待查表组织成特定树的形式并在树结构上实现查找的方法,故又称为树表式查找法。主要包括:
l 二叉排序树
l 平衡二叉树
l B_树
第3 讲 基于树表的查找——教学讲义
基于树的查找法是将待查表组织成特定树的形式并在树结构上实现查找的方法,故又称为树表式查找法,主要包括二叉排序树、平衡二叉树和 B_树等。
一、 二叉排序树
1、 二叉排序树定义与描述
二叉排序树又称为二叉查找树,它是一种特殊的二叉树。
其定义为:二叉树排序树或者是一棵空树,或者是具有如下性质的二叉树:
(1)若它的左子树非空,则左子树上所有结点的值均小于根结点的值;
(2)若它的右子树非空,则右子树上所有结点的值均大于(或大于等于)根结点的值;
(3)它的左右子树也分别为二叉排序树。
这是一个递归定义。注意只要结点之间具有可比性即可,如下图 (a)中的数字值间的比较,图 (b)中是用单词字符的 ASCII 码间的比较
二叉排序树的存储结构同二叉树,使用二叉链表作为存储结构。
其结点结构描述说明如下:
typedef struct node
{
KeyType key; /*关键字的值*/
struct node* lchild, * rchild;/*左右指针*/
}BSTNode, * BSTree;
2.二叉排序树的创建
假若给定一个元素序列,我们可以利用逐个插入结点算法创建一棵二叉排序树。因此,实现建立二叉排序树包括创建树与插入结点两个算法。
[算法思想]:
首先,将二叉树序树初始化为一棵空树,然后逐个读入元素,每读入一个元素,就建立一个新的结点,并插入到当前已生成的二叉排序树中,即通过多次调用二叉排序树的插入新结点的算法实现,注意插入时比较结点的顺序始终是从二叉排序树的根结点开始。
①二叉排序树的插入
已知一个关键字值为 key 的结点 s,若将其插入到二叉排序树中,只要保证插入后仍符合二叉排序树的定义即可。
[算法思想]:
1) 若二叉排序树是空树,则 key 成为二叉排序树的根;
2) 若二叉排序树非空,则将 key 与二叉排序树的根进行比较:
a) 如果 key 的值等于根结点的值,则停止插入;
b) 如果 key 的值小于根结点的值,则将 key 插入左子树;
c) 如果 key 的值大于根结点的值,则将 key 插入右子树。
例如,设关键字的输入顺序为:45,24 ,53,12,28,90,按上述算法生成的二叉排序树的过程如下图所示。
[算法描述]:
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插入右子树*/
}
可以看出,二叉排序树的插入,即插入每一个结点都是作为一个叶子结点,将其插到二叉排序树的合适位置,插入时不需要移动元素,不涉及树的整体改动。
[算法分析]:二叉排序树的插入算法的时间复杂度仍为 O (log 2 n) ,具体的分析可留作练习。
②创建二叉排序树
[算法描述]:
void CreateBST(BSTree* bst)
/*从键盘输入元素的值,创建相应的二叉排序树*/
{
KeyType key;
*bst = NULL;
scanf("%d", &key);
while (key != ENDKEY) /*EDNKEY 为自定义常量*/
{
InsertBST(bst, key); /*在二叉排序树 bst 中插入结点 key*/
scanf("%d", &key);
}
}
[算法分析]:
假设共有 n 个元素,要插入 n 个结点需要 n 次插入操作,而插入一个结点的算法时间复杂度为 O(log2n),因此创建二叉排序树的算法时间复杂度为 O(nlog2n)。
如果输入顺序为 24,53,90,12,28,45,则生成的二叉排序树如下图所示:
注意:这个输入序列的值与前一个输入序列具有同样元素值,只是输入顺序不同,所创建的二叉排序树的形态不同。
3.二叉排序树的查找
二叉排序树的特性:根据二叉排序树的定义(左子树小于根结点,右子树大于根结点),
根据二叉树中序遍历的定义(先中序遍历左子树,访问根结点,再中序遍历右子树),可以得出二叉排序树的一个重要性质,
中序遍历一个二叉排序树,可以得到一个递增有序序列。
图 8.4 所示的二叉树就是一棵二叉排序树,若中序遍历下图的二叉排序树,则可得到一个递增有序序列为:1,2,3,4,5,6,7,8,9。
二叉排序树查找的实现方法:
因为二叉排序树可看作是一个有序表,所以在二叉排序树上进行查找,和折半查找类似,也是一个逐步缩小查找范围的过程。
[算法思想]:
首先将待查关键字 k 与根结点关键字 t 进行比较,如果:
1) k= t:则返回根结点地址;
2) k<t:则进一步查左子树;
3) k>t:则进一步查右子树。
[实现算法]:
根据二叉排序树的定义,在二叉排序树结构上查找可以用递归与非递归两种实现算法。
1.二叉排序树查找的递归算法
BSTree SearchBST(BSTree bst, KeyType key)
/ *在根指针bst所指二叉排序树中,递归查找某关键字等于key的元素,若查找成功,返回指向该元素结点指针,否则返回空指针* /
{
if (!bst)
return NULL;
else
if (bst->key == key)
return bst;/ *查找成功* /
else
if (bst->key > key)
return SearchBST(bst->lchild, key);/ *在左子树继续查找* /
else
return SearchBST(bst->rchild, key);/ *在右子树继续查找* /
}
2.二叉排序树查找的非递归算法
根据二叉排序树定义,其查找也可以用循环方式直接实现。
二叉排序树的非递归查找过程如下:
BSTree SearchBST(BSTree bst, KeyType key)
/*在根指针 bst 所指二叉排序树 bst 上,查找关键字等于 key 的结点,若查找成功,返回指
向该元素结点指针,否则返回空指针*/
{
BSTree q;
q = bst;
while (q)
{
if (q->key == key) return q; /*查找成功*/
if (q->key > key) q = q->lchild; /*在左子树中查找*/
else q = q->rchild; /*在右子树中查找*/
}
return NULL; /*查找失败*/
}/*SearchBST*/
[算法分析]:
显然,在二叉排序树上进行查找,若查找成功,则是从根结点出发走了一条从根结点到待查结点的路径。
若查找不成功,则是从根结点出发走了一条从根到某个叶子结点的路径。
因此,二叉排序树的查找与折半查找过程类似,在二叉排序树中查找一个记录时,其比较次数不超过树的深度。
但是,对长度为 n 的有序表而言,折半查找对应的判定树是唯一的,
而含有 n 个结点的二叉排序树却是不唯一的,因为对于同一个关键字集合,关键字插入的先后次序不同,所构成的二叉排序树的形态和深度也不同。
而二叉排序树的平均查找长度 ASL与二叉排序树的形态有关,二叉排序树的各分支越均衡,树的深度浅,其平均查找长度 ASL越小。
例如,下图为两棵二叉排序树,它们对应同一元素集合,但排列顺序不同,分别是:
(45,24,53,12,37,93)和(12,24,37,45,53,93)。
假设每个元素的查找概率相等,则它们的平均查找长度分别是:
ASL=1/6 (1+2+2+3+3+3)=14/6
ASL=1/6 (1+2+3+4+5+6)=21/6
由此可见,在二叉排序树上进行查找时的平均查找长度和二叉排序树的形态有关。
在最坏情况下,二叉排序树是通过把一个有序表的 n 个结点一次插入生成的,
由此得到二叉排序树蜕化为一棵深度为 n 的单支树,
它的平均查找长度和单链表上的顺序查找相同,也是(n+1)/2。
在最好情况下,二叉排序树在生成过程中,树的形态比较均匀,
最终得到的是一棵形态与折半查找的判定树相似的二叉排序树,此时它的平均查找长度大约是 log2 n 。
若考虑把 n个结点,按各种可能的次序插入到二叉排序树中,则有 n!棵二叉排序树(其中有的形态相同),
可以证明,对这些二叉排序树的查找长度进行平均,得到的平均查找长度仍然是O(log 2 n) 。
就平均性能而言,二叉排序树上的查找和折半查找相差不大,并且二叉排序树上的插入和删除结点十分方便,无需移动大量结点。
因此,对于需要经常做插入、删除、查找运算的表,宜采用二叉排序树结构。
人们也常常将二叉排序树称为二叉查找树
二、平衡二叉排序树
平衡二叉排序树又称为 AVL 树。
一棵平衡二叉排序树或者是空树,或者是具有下列性质的二叉排序树:
(1)左子树与右子树的高度之差的绝对值小于等于 1
(2)左子树和右子树也是平衡二叉排序树。
引入平衡二叉排序树的目的,是为了提高查找效率,其平均查找长度为 O(log2 n) 。
在下面的描述中,需要用到结点的平衡因子( balance factor )这一概念,其定义为:结点的左子树深度与右子树深度之差。
显然,对一棵平衡二叉排序树而言,其所有结点的平衡因子只能是-1、 0、或 1。
当我们在一个平衡二叉排序树上插入一个结点时,有可能导致失衡,即出现绝对值大于 1 的平衡因子,
如图 8.9 中给出了一棵平衡二叉排序树和一棵失去平衡的二叉排序树
下面通过几个实例,直观说明失衡情况以及相应的调整方法。
例 1 一棵平衡二叉排序树如下图(a)所示。在 A 的左子树的左子树上插入 15 后,导致失衡,如下图(b)所示。
为恢复平衡并保持二叉排序树特性,可将 A 改为 B 的右子,B 原来的右子,改为 A 的左子,如下图(c)所示。这相当于以 B 为轴,对 A 做了一次顺时针旋转
例 2 已知一棵平衡二叉排序树如下图(a)所示。在 A 的右子树 B 的右子树上插入 70 后,导致失衡,如下图(b)所示。
为恢复平衡并保持二叉排序树特性,可将 A 改为 B 的左子,B 原来的左子,改为 A 的右子,如下图(c)所示。这相当于以 B 为轴,对 A 做了一次逆时针旋转。
例 3 已知一棵平衡二叉排序树如下图(a)所示。在 A 的左子树 B 的右子树上插入 45 后,导致失衡,如下图(b)所示。
为恢复平衡并保持二叉排序树特性,可首先将 B 改为 C 的左子,而 C 原来的左子,改为 B 的右子;
然后将 A 改为 C 的右子, C 原来的右子,改为 A 的左子,如下图(c)所示。这相当于对 B 做了一次逆时针旋转,对 A 做了一次顺时针旋转。
例 4 已知一棵平衡二叉排序树如下图(a)所示。在 A 的右子树的左子树上插入 55 后,导致失衡,如下图(b)所示。
为恢复平衡并保持二叉排序树特性,可首先将 B 改为 C 的右子,而 C 原来的右子,改为 B 的左子;
然后将 A 改为 C 的左子, C 原来的左子,改为 A 的右子,如下图(c)所示。
这相当于对 B 做了一次顺时针旋转,对 A 做了一次逆时针旋转。
一般情况下,只有新插入结点的祖先结点的平衡因子受影响,即以这些祖先结点为根的子树有可能失衡。
下层的祖先结点恢复平衡,将使上层的祖先结点恢复平衡,因此应该调整最下面的失衡子树。
因为平衡因子为 0 的祖先不可能失衡,所以从新插入结点开始向上,遇到的第一个其平衡因子不等于 0 的祖先结点为第一个可能失衡的结点,
如果失衡,则应调整以该结点为根的子树。失衡的情况不同,调整的方法也不同。
三、B_树
1. m 路查找树
与二叉排序树类似,可以定义一种“m 叉排序树”,通常称为 m 路查找树。
一棵 m 路查找树,或者是一棵空树,或者是满足如下性质的树:
1) 结点最多有 m 棵子树,m—1 个关键字,其结构如下:
其中 n 为关键字个数,Pi(0≤i≤n)为指向子树根结点的指针,Ki(1≤i≤n)为关键字,
2) Ki<K i+1,1≤i≤n-1
3) 子树 Pi 中的所有关键字均大于 Ki、小于 Ki+1,1≤i≤n-1
4) 子树 P0 中的关键字均小于 K1,而子树 Pn 中的所有关键字均大于 Kn
5) 子树 Pi 也是 m 路查找树,0≤i≤n。
从上述定义可以看出,对任一关键字 Ki 而言,Pi-1 相当于其“左子树”,P i 相当于其“右子树”,1≤i≤n 。
下图所示为一棵 3 路查找树,其查找过程与二叉排序树的查找过程类似。
如果要查找 35,首先找到根结点 A,因为 35 介于 20 和 40 之间,因而找到结点 C,又因为 35大于 30,所以找到结点 E,最后在 E 中找到 35。
显然,如果 m 路查找树为平衡树时,其查找性能会更好。下面要讨论的 B_ 树便是一种 平衡的 m 路查找树。
2. B_树
一棵 B_树是一棵平衡的 m 路查找树,它或者是空树,或者是满足如下性质的树:
(1)树中每个结点最多有 m 棵子树;
(2)根结点至少有两棵子树;
(3)除根结点之外的所有非叶结点至少有 棵子树;
(4)所有叶结点出现在同一层上,并且不含信息,通常称为失败结点。失败结点为虚结点,在 B_树中并不存在,指向它们的指针为空指针。引入失败结点是为了便于分析 B_树的查找性能。
下图所示为一棵 4 阶 B_树,其查找过程与 m 路查找树相同。
首先由根指针 mbt 找到根结点 A,因为 58>37,所以找到结点 C,又因为 40<58<85,所以找到结点 G,最后在结点 G 中找到 58。
如果要查找 32,首先由根指针 mbt 找到根结点 A,因为 32<37,所以找到结点 B,又因为 32>25, 所以找到结点 E,因为 30<32<35,
所以最后找到失败结点 f ,表示 32 不存在,查找失败。
在具体实现时,采用如下结点结构:其中 n 、K i、 P i 的含义以及使用方法与前面 m 路查找树相同,parent 为指向双亲结点的指针。
#define m <阶数>
typedef int Boolean;
typedef struct Mbtnode
{
struct Mbtnode* parent;
int keynum;
KeyType key[m + 1];
struct Mbtnode* ptr[m + 1];
} Mbtnode, * Mbtree;
【在 B_树中查找关键字为 k 的元素】
Boolean srch_mbtree (Mbtree mbt, KeyType k, Mbtree *np, int *pos)
/*在根为mbt的B_树中查找关键字k,如果查找成功,则将所在结点地址放入np,将结点内位置序号放入pos,并返回true;否则,将k应被插入的结点地址放入np,将结点内应插位置序号放入pos,并返回false*/
{
Mbtree p,fp;
Boolean found;
int i;
p = mbt;
fp = NULL;
found = false;
i = 0;
while (p != NULL && !found)
{
i = search (p, k);
if (i>0 && p->key[i] == k)
found = true;
else
{
fp = p;
p = p->ptr[i];
}
}
if (found)
{
*np = p;
*pos = i;
return true;
}
else
{
*np = fp;
*pos = i;
return false;
}
}
【寻找小于等于关键字 k 的关键字序号】
int search (Mbtree mbt, KeyType key )
/*在mbt指向的结点中,寻找小于等于key的最大关键字序号*/
{
int n,i;
n = mbt->keynum ;
i = 1 ;
while (i <= n && mbt->key[i] <= key ) i ++;
return (i - 1); /* 返回小于等于key的最大关键字序号 ,为0 时表示应到
最左分支找,越界时表示应到最右分支找 */
}
习题巩固
如图所示的二叉排序树,起查找成功时的平均查找长度是 。
-
A.
-
B.
-
C.
-
D.
在一棵平衡二叉树中,每个结点的平衡因子的取值范围是 。
-
A.-1——1
-
B.-2——2
-
C.1——2
-
D.0——1
查找效率最高的二叉排序树是平衡二叉排序树。( )
-
A.true
-
B.false
在二叉排序树中新插入的结点总是作为叶子结点来插入的。( )
-
A.true
-
B.false
在二叉排序树中新插入的结点总是处于最底层。( )
-
A.true
-
B.false
每个结点的关键字都比左孩子关键字大,比右孩子关键字小,这样的二叉树都是二叉排序树。( )
-
A.true
-
B.false
第4讲 计算式查找(哈希表的构造)——内容简介
【本讲要点】
l 哈希函数的构造方法
l 地址冲突的处理方法
l 哈希表的构造
第 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) 如何处理冲突。
一、哈希函数的构造方法
构造哈希函数的原则是:①函数本身便于计算;②计算出来的地址分布均匀,即对任一
关键字 k,H (k) 对应不同地址的概率相等,目的是尽可能减少冲突。
下面介绍构造哈希函数常用的五种方法。
1. 数字分析法
如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以
从关键字中选出分布较均匀的若干位,构成哈希地址。例如,有 80 个记录,关键字为 8 位
十进制整数 d1d2d3…d7d8,如哈希表长度取为 100,则哈希表的地址空间为:0~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 位作为该关键字哈希地址,如下图所示。
3. 分段叠加法
这种方法是按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),
然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。具体方法有折叠法
与移位法。移位法是将分割后的每部分低位对齐相加,折叠法是从一端向另一端沿分割界来
回折叠(奇数段为正序,偶数段为倒序),然后将各段相加。例如:key=12360324711202065,
哈希表长度为 1000,则应把关键字分成 3 位一段,在此舍去最低的两位 65,分别进行移位
叠加和折叠叠加,求得哈希地址为 105 和 907,如下图所示。
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
此时没有冲突,如下图所示。
5. 伪随机数法
采用一个伪随机函数做哈希函数,即 H(key)=random(key)。
在实际应用中,应根据具体情况,灵活采用不同的方法,并用实际数据测试它的性能,
以便做出正确判定。通常应考虑以下五个因素 :
l 计算哈希函数所需的时间。
l 关键字的长度。
l 哈希表的大小。
l 关键字分布的情况。
l 记录查找的频率
8.4.2 处理冲突的方法
通过构造性能良好的哈希函数,可以减少冲突,但一般不可能完全避免冲突,因此解决
冲突是哈希法的另一个关键问题。创建哈希表和查找哈希表都会遇到冲突,两种情况下解决
冲突的方法应该一致。下面以创建哈希表为例,说明解决冲突的方法。常用的解决冲突方法
有以下四种:
1. 开放定址法
这种方法也称再散列法,其基本思想是:当关键字 key 的初始哈希地址 h0= H(key)
出现冲突时,以 h0 为基础,产生另一个地址 h1,如果 h1 仍然冲突,再以 h0 为基础,产生另
一个哈希地址 h2,…,直到找出一个不冲突的地址 hi ,将相应元素存入其中。这种方法有
一个通用的再散列函数形式:
hi=(H(key)+di)% m i=1,2,…,n
或:
hi=(h0 + di)% m i=1,2,…,n
其中 H(key)为哈希函数,h0=H(key),m 为表长,di 称为增量序列。增量序列的取
值方式不同,相应的再散列方式也不同。主要有以下三种:
l 线性探测再散列
di=1,2,3,…,m-1
这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍
全表。
l 二次探测再散列
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
l 伪随机探测再散列
di=伪随机数序列。
具体实现时,应建立一个伪随机数发生器,(如 i=(i+p) % m),并给定一个随机数做起点。
例如,已知哈希表长度 m=12,哈希函数为:H(key)= key % 11,则 H(47)=3,H
(26)=4,H(60)=5,假设下一个关键字为 69,则 H(69)=3,与 47 冲突。如果用线性
探测再散列处理冲突,下一个哈希地址为 h1=(3 + 1)% 12 = 4,仍然冲突,再找下一个哈希
地址为 h2=(3 + 2)% 12 = 5,还是冲突,继续找下一个哈希地址为 h3=(3 + 3)% 12 = 6,此
时不再冲突,将 69 填入 6 号单元,参下图(a)。如果用二次探测再散列处理冲突,下一个哈
希地址为 h1=(3 + 12)% 12 = 4,仍然冲突,再找下一个哈希地址为 h2=(3 - 12)% 12 = 2,
此时不再冲突,将 69 填入 2 号单元,参下图(b)。如果用伪随机探测再散列处理冲突,且伪
随机数序列为:2,5,9,……..,则下一个哈希地址为 h1=(3 + 2)% 12 = 5,仍然冲突,再
找下一个哈希地址为 h2=(3 + 5)% 12 = 8,此时不再冲突,将 69 填入 8 号单元,如下图(c)。
从上述例子可以看出,线性探测再散列容易产生“二次聚集”,即在处理同义词的冲突
时又导致非同义词的冲突。例如,当表中 i, i+1 ,i+2 三个单元已满时,下一个哈希地址为 i, 或
i+1 ,或 i+2,或 i+3 的元素,都将填入 i+3 这同一个单元,而这四个元素并非同义词。线性探
测再散列的优点是:只要哈希表不满,就一定能找到一个不冲突的哈希地址,而二次探测再
散列和伪随机探测再散列则不一定。
如果要在上述哈希表中删除一个记录,则需要在该记录的位置上填入一个特殊记录,否
则将无法找到在其后填入的同义词记录。
2. 再哈希法
这种方法是同时构造多个不同的哈希函数:
Hi=RHi(key) i=1,2,…,k
当哈希地址 H1=RH1(key)发生冲突时,再计算 H2=RH2(key)……,直到冲突不再产
生。这种方法不易产生聚集,但增加了计算时间。
3. 链地址法
这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并
将单链表的头指针存在哈希表的第 i 个单元中,因而查找、插入和删除主要在同义词链中进
行。链地址法适用于经常进行插入和删除的情况。
例:已知一组关键字(32,40,36,53,16,46,71,27,42,24,49,64),哈希表
长度为 13,哈希函数为:H(key)= key % 13,给出用链地址法处理冲突的结果,计算平均
查找长度。如下图所示:
平均查找长度 ASL=(1*7+2*4+3*1)/12=1.5
4. 建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素一律填入溢出表。
习题巩固
将10个元素散列到10000000个单元的哈希表中,则 产生冲突。
-
A.一定会
-
B.一定不会
-
C.仍可能会
-
D.以上都不对
在哈希查找中,可用 来处理冲突。
-
A.除留余数法
-
B.数字分析法
-
C.线性探测散列法
-
D.数字分析法
设哈希表长度m=12,哈希函数为H(key)=key mod 11.表中已经有4个结点分别为H(15)=4,H(38)=5, H(61)=6,H(84)=7,其余地址为空。如果用二次探测再散列处理冲突,则关键字为49的结点地址为 。
-
A.8
-
B.3
-
C.5
-
D.9
设哈希表长度m=14,哈希函数H(key)=key mod p,则p最好取 。
第5讲 计算式查找(哈希查找及性能分析)——内容简介
【本讲要点】
l 哈希查找算法
l 哈希查找法性能分析
n 查找成功时的平均查找长度
n 查找不成功的平均查找长度
习题巩固
若采用链地址法构造哈希表并处理冲突,哈希函数为H(key)=key mod 17,则需要 个链表。
-
A.17
-
B.16
-
C.13
-
D.不确定
假设有k个关键字互为同义词,若用线性探测再散列法将这k个关键字存入哈希表中,至少要进行 次定址。
-
A.k-1
-
B.k
-
C.k+1
-
D.k(k+1)/2
哈希表的查找性能 。
-
A.与处理冲突的方法有关而与表的长度无关
-
B.与处理冲突的方法无关而与表的长度有关
-
C.与处理冲突的方法无关而与装填因子有关
-
D.与处理冲突的方法有关,与装填因子有关
总结与提高——内容简介
【本讲要点】
u 主要知识点
l 查找表的检索机制
l 平均查找长度
l 折半查找
l 二叉排序树
l 平衡二叉排序树
l B_树
l 哈希法
u 典型题解