查找的基本概念
关键码:可以标识一个记录的某个数据项
键值:关键码的值
主关键码:可以唯一地标识一个记录的关键码
次关键码:不能唯一地标识一个记录的关键码
查找 :在具有相同类型的记录构成的集合中找出满足给定条件的记录
查找的结果 :若在查找集合中找到了与给定值相匹配的记录,则称查找成功;否则,称查找失败
静态查找 :不涉及插入和删除操作的查找
动态查找 :涉及插入和删除操作的查找
静态查找适用:查找集合一经生成,便只对其进行查找,而不进行插入和删除操作,或经过一段时间的查找之后,集中地进行插入和删除等修改操作
动态查找适用:查找与插入和删除操作在同一个阶段进行,例如当查找成功时,要删除查找到的记录,当查找不成功时,要插入被查找的记录
查找结构 :面向查找操作的数据结构 ,即查找基于的数据结构
查找算法性能
查找算法时间性能通过关键码的比较次数来度量
与关键码的比较次数有关的因素:算法、问题规模、待查关键码在查找集合中的位置、查找频率(查找频率与算法无关,取决于具体应用)
同一查找集合、同一查找算法,与关键码的比较次数有关的因素:查找算法的时间复杂度是问题规模n和待查关键码在查找集合中的位置k的函数,记为T(n,k)
平均查找长度(Average search length ):将查找算法进行的关键码的比较次数的数学期望值定义为平均查找长度,计算公式为:
n:问题规模,查找集合中的记录个数
pi:查找第i个记录的概率
ci:查找第i个记录所需的关键码的比较次数
结论:ci取决于算法;pi与算法无关,取决于具体应用;如果pi是已知的,则平均查找长度只是问题规模的函数
线性表的查找技术
线性表:适用于静态查找,主要采用顺序查找技术、折半查找技术
1.顺序查找
基本思想:从线性表的一端向另一端逐个将关键码与给定值进行比较,若相等,则查找成功,给出该记录在表中的位置;若整个表检测完仍未找到与给定值相等的关键码,则查找失败,给出失败信息。
int SeqSearch1(int r[ ], int n, int k)
//数组r[1] ~ r[n]存放查找集合
{
i=n;
while (i>0 && r[i]!=k)
i--;
return i;
}
改进:设置“哨兵”。哨兵就是待查值,将它放在查找方向的尽头处,免去了在查找过程中每一次比较后都要判断查找位置是否越界,从而提高查找速度
int SeqSearch2(int r[ ], int n, int k)
//数组r[1] ~ r[n]存放查找集合
{
r[0]=k; i=n;
while (r[i]!=k)
i --;
return i;
}
顺序查找的缺点:
平均查找长度较大,特别是当待查找集合中元素较多时,查找效率较低
顺序查找的优点:
算法简单而且使用面广
对表中记录的存储没有任何要求,顺序存储和链接存储均可
2.折半查找
使用条件:线性表中的记录必须按关键码有序、必须采用顺序存储
基本思想:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键码相等,则查找成功;若给定值小于中间记录的关键码,则在中间记录的左半区继续查找;若给定值大于中间记录的关键码,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所查找的区域无记录,查找失败
非递归算法
int BinSearch1(int r[ ], int n, int k)
//数组r[1] ~ r[n]存放查找集合
{
low=1; high=n;
while (low<=high)
{
mid=(low+high)/2;
if (k<r[mid]) high=mid-1;
else if (k>r[mid]) low=mid+1;
else return mid;
}
return 0;
}
递归算法
int BinSearch2(int r[ ], int low, int high, int k)
//数组r[1] ~ r[n]存放查找集合
{
if (low>high) return 0;
else {
mid=(low+high)/2;
if (k<r[mid])
return BinSearch2(r, low, mid-1, k);
else if (k>r[mid])
return BinSearch2(r, mid+1, high, k);
else return mid;
}
}
折半查找判定树
判定树:折半查找的过程可以用二叉树来描述,树中的每个结点对应有序表中的一个记录,结点的值为该记录在表中的位置
通常称这个描述折半查找过程的二叉树为折半查找判定树,简称判定树
判定树的构造方法
⑴ 当n=0时,折半查找判定树为空
⑵ 当n>0时,折半查找判定树的根结点是有序表中序号为mid=(n+1)/2的记录,根结点的左子树是与有序表r[1] ~ r[mid-1]相对应的折半查找判定树,根结点的右子树是与r[mid+1] ~ r[n]相对应的折半查找判定树
折半查找性能分析
具有n个结点的折半查找判定树的深度为
查找成功:在表中查找任一记录的过程,即是折半查找判定树中从根结点到该记录结点的路径,和给定值的比较次数等于该记录结点在树中的层数
查找不成功:查找失败的过程就是走了一条从根结点到外部结点的路径,和给定值进行的关键码的比较次数等于该路径上内部结点的个数
树表的查找技术
树表:适用于动态查找,主要采用二叉排序树的查找技术
二叉排序树
定义
也称二叉查找树:或者是一棵空的二叉树,或者是具有下列性质的二叉树
⑴ 若它的左子树不空,则左子树上所有结点的值均小于根结点的值
⑵ 若它的右子树不空,则右子树上所有结点的值均大于根结点的值
⑶ 它的左右子树也都是二叉排序树
中序遍历二叉排序树可以得到一个按关键码有序的序列
构造
BiSortTree::BiSortTree(int r[ ], int n)
{
for (i=0; i<n; i++)
{
s=new BiNode<int>;
s->data=r[i];
s->lchild=s->rchild=NULL;
InsertBST(root, s);
}
}
插入
void BiSortTree::InsertBST(BiNode<int> *root, BiNode<int> *s)
{
if (root==NULL)
root=s;
else if (s->data<root->data)
InsertBST(root->lchild, s);
else InsertBST(root->rchild, s);
}
查找
BiNode *BiSortTree::SearchBST(BiNode<int> *root, int k)
{
if (root==NULL)
return NULL;
else if (root->data==k)
return root;
else if (k<root->data)
return SearchBST(root->lchild, k);
else return SearchBST(root->rchild, k);
}
删除
在二叉排序树上删除某个结点之后,仍然保持二叉排序树的特性
分三种情况讨论:
被删除的结点是叶子(操作:将双亲结点中相应指针域的值改为空)
被删除的结点是只有左子树或者只有右子树(操作:将双亲结点的相应指针域的值指向被删除结点的左子树(或右子树))
被删除的结点是既有左子树,也有右子树(操作:以其前驱(左子树中的最大值)替代之,然后再删除该前驱结点)
二叉排序树的查找性能分析
二叉排序树的查找性能取决于二叉排序树的形状,在O()和O(n)之间
平衡二叉树
定义:或者是一棵空的二叉排序树,或者是具有下列性质的二叉排序树:
⑴ 根结点的左子树和右子树的深度最多相差1
⑵ 根结点的左子树和右子树也都是平衡二叉树
平衡因子:结点的平衡因子是该结点的左子树的深度与右子树的深度之差
在平衡树中,结点的平衡因子可以是1,0,-1
最小不平衡子树:在平衡二叉树构造过程中,以距离插入结点最近的、且平衡因子的绝对值大于1的结点为根的子树
构造
基本思想:在构造二叉排序树的过程中,每插入一个结点时,首先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树,在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树
设结点A为最小不平衡子树的根结点,对该子树进行平衡调整归纳起来有以下四种情况:
1. LL型
2. RR型
3. LR型
4. RL型
散列表的查找技术
在存储位置和关键码之间建立一个确定的对应关系
散列表:静态查找和动态查找均适用,主要采用散列技术,采用散列技术将记录存储在一块连续的存储空间中
散列函数:将关键码映射为散列表中适当存储位置的函数
散列地址:由散列函数所得的存储位置址
散列的基本思想:在记录的存储地址和它的关键码之间建立一个确定的对应关系,既是一种查找技术,也是一种存储技术,是面向查找的存储结构
冲突:对于两个不同关键码ki≠kj,有H(ki)=H(kj),即两个不同的记录需要存放在同一个存储位置,ki和kj相对于H称做同义词
散列函数的设计:简单、均匀、存储利用率高的散列函数
原则:
⑴ 计算简单:散列函数不应该有很大的计算量,否则会降低查找效率
⑵ 函数值即散列地址分布均匀:函数值要尽量均匀散布在地址空间,这样才能保证存储空间的有效利用并减少冲突
冲突:对于两个不同关键码ki≠kj,有H(ki)=H(kj),即两个不同的记录需要存放在同一个存储位置,ki和kj相对于H称做同义词
1.直接定址法
事先知道关键码,关键码集合不是很大且连续性较好
散列函数:
2.除留余数法
除留余数法是一种最简单、也是最常用的构造散列函数的方法,并且不要求事先知道关键码的分布
散列函数:
一般情况下,选p为小于或等于表长(最好接近表长)的最小素数或不包含小于20质因子的合数。
3.数字分析法
能预先估计出全部关键码的每一位上各种数字出现的频度,不同的关键码集合需要重新分析
根据关键码在0各个位上的分布情况,选取分布比较均匀的若干位组成散列地址
4.平方取中法
事先不知道关键码的分布且关键码的位数不是很大
5.折叠法
将关键码从左到右分割成位数相等的几部分,将这几部分叠加求和,取后几位作为散列地址
关键码位数很多,事先不知道关键码的分布
6.开放定址法
由关键码得到的散列地址一旦产生了冲突,就去寻找下一个空的散列地址,并将记录存入
(1)线性探测法
当发生冲突时,从冲突位置的下一个位置起,依次寻找空的散列地址
用开放定址法处理冲突得到的散列表叫闭散列表
堆积:在处理冲突的过程中出现的非同义词之间对同一个散列地址争夺的现象
(2)二次探测法
(3)随机探测法
1.处理冲突的方法——拉链法(链地址法)
将所有散列地址相同的记录,即所有同义词的记录存储在一个单链表中(称为同义词子表),在散列表中存储的是所有同义词子表的头指针
用拉链法处理冲突构造的散列表叫做开散列表
设n个记录存储在长度为m的散列表中,则同义词子表的平均长度为n / m 2.处理冲突的方法——公共溢出区
散列表包含基本表和溢出表两部分(通常溢出表和基本表的大小相同),将发生冲突的记录存储在溢出表中
查找时,对给定值通过散列函数计算散列地址,先与基本表的相应单元进行比较,若相等,则查找成功;否则,再到溢出表中进行顺序查找
散列查找的性能分析
由于冲突的存在,产生冲突后的查找仍然是给定值与关键码进行比较的过程
在查找过程中,关键码的比较次数取决于产生冲突的概率。而影响冲突产生的因素有:
(1)散列函数是否均匀
(2)处理冲突的方法
(3)散列表的装载因子 α=表中填入的记录数 / 表的长度
开散列表(拉链表)与闭散列表(开放定址法)的比较
分块查找
块内无序,块间有序
块内选择最大值(图中第一行),块的起始地址为本块第一个数值地址(图中第二行)