1. 查找基本概念
- 数据是如何组织的——查找表
- 查找表上如何查找——查找方法
1 对查找表的基本操作:增、删、查、改
- 查询某个数据元素是否在查找表中
- 检索某个数据元素的各种属性
- 在查找表中插入一个数据元素
- 从查找表中删去某个数据元素
2 查找表的分类
- 静态查找表:仅做查询和检索操作的查找表。
- 动态查找表:“查询”结果“不在查找表中”:数据元素插入到查找表中;“查询”结果“在查找表中”的数据元素:删除。
3 关键字
查找过程中,往往是依据数据元素的某个数据项进行查找,这个数据项通常是数据的关键字。
关键字:是数据元素中某个数据项的值,用以标识一个数据元素。
- 若关键字能标识唯一的一个数据元素,则称为主关键字。
- 若关键字能标识若干个数据元素,则称为次关键字。
4 查找算法的比较分析
- 时间复杂度
- 平均查找长度(ASL)
:查找第 个元素的概率。 :查找第 个元素需要的比较次数。
5 常见的查找方法
- 顺序查找
- 二分查找
- 索引查找
- 哈希查找
2. 顺序查找
1 顺序查找基本思想
- 从表中指定位置(一般为最后一个,第0个位置设为岗哨)的记录开始,沿某个方向将记录的关键字与给定值相比较,若某个记录的关键字和给定值相等,则查找成功。
- 反之,若找完整个顺序表,都没有与给定关键字值相等的记录,则此顺序表中没有满足查找条件的记录,查找失败。
2 顺序查找算法的实现
int seqsearch(DataType R[], KeyType key)
{
R[0].key = key; //把待查找的值放入下标为0的位置
int i = n; //从后往前比较
while(R[i].key != key)
i = i-1;
return i; //如果返回值非0则在查找表中找到了该元素,若为0则未找到
}
3 实例
4 性能分析
空间复杂度:O(1)
时间复杂度:
查找算法的基本运算是给定值与顺序表中记录关键字值的比较。
- 最好情况:O(1)
- 最坏情况:O(N)
- 平均情况:O(N)
平均查找长度:
对于顺序表:
- 等概率情况:
- 不等概率情况:若每个元素查找概率已知,把查找概率大的放后面,把查找概率小的放前面;若每个元素的查找概率未知,在每次查找结束后,把查找到的记录直接放在查找表尾部,因为最近查找的元素有可能最近再被查找。
3. 折半查找
1 有序表
有序表:如果顺序表中的记录按关键字值有序,即:R[i].key <= R[i+1].key(或 R[i].key >= R[i+1].key),i = 1,2,...,n-1,则称顺序表为有序表。
2 查找过程
将待查关键字与有序表中间位置的记录进行比较,若相等,查找成功,若小于,则只可能在有序表的前半部分,若大于则只能在有序表的后半部分。因此,进过一次比较,就将查找范围缩小一半,这样一直进行下去直到找到所需记录或记录不在查找表中。
3 实例
- low:指示查找区间的下界
- high:指示查找区间的上界
- mid:指示查找区间的中间位置。mid = (low+high)/2 (向下取整)
- 有序表的存储从下标 1 开始
4 折半查找算法的实现
int BinarySearch(DataType SL[], KeyType key, int n)
{
//在长度为n的有序表SL中折半查找其关键字等于key的记录
//查找成功返回其在有序表中的位置,查找失败返回0
int low = 1; //有序表从下标 1 开始存储
int high = n;
int mid;
while(low < high)
{
mid = (low + high)/2;
if(key == SL[mid].key)
return mid;
else if(key > SL[mid].key)
low = mid + 1;
else
high = mid - 1;
}
return 0; //没找到就返回0
}
5 折半查找性能分析
- (1,4,7,10的左边分支查找3次)
- 若 n>50 时,可近似结果:
6 折半查找的特点
- 折半查找的查找效率高。时间复杂度:
- 平均查找性能和最坏性能相当接近。
- 折半查找要求查找表为有序表。
- 折半查找只适用于顺序存储结构。
4. 索引查找
1 索引使用方法
- 先分析数据规律,建立索引
- 再根据索引进行快速定位
- 在定位的地方进行细致搜索
2 索引表的建立
1)分块:第块中所有关键字 < 块中所有关键字,(k = 1,2,...,L-1)
2)建立索引项:
- 关键字项:记载该块中最大关键字值。
- 指针项:记载该块第一个记录在表中的位置
3)所有索引项组成索引表。
3 索引表的查找
- 索引表的查找:索引表是有序的,可以采用顺序查找或折半查找。
- 查找表的查找
4 实例
- 如何表示块内查找结束?若查找的块内位置已经到了下一个块的起始地址则表明块内查找结束。所以索引表最后一项要额外增加一个索引项来指示整个记录第 n+1 的位置。
5 索引表的顺序查找算法实现
首先根据待查找关键字在索引表当中定位块。定位的方法是:只要 key > 索引块 i 的最大关键字值,则 i++,定位下一个索引项;直到定位到索引块(定位到比 key 关键字大的索引项),或者把索引项都定位完也没有比 key 关键字大的索引项。 如果定位到块,则在块内部进行顺序查找。
struct IndexType
{
KeyType key; //最大关键字值
int link; //块的起始地址
};
int IndexSequelSearch(IndexType Is[], DataType s[], int m, KeyType key)
{
//索引表为Is[0]-Is[m-1],顺序表为 s
int i = 0;
while(i < m && key >Is[i].key)
i++; //块间查找
if(i == m)
return -1; //查找失败(定位不到块)
else
{
int j = Is[i].link;
while(key != s[j].key && j < Is[i+1].link)
j++;
if(key == s[j].key)
return j; //查找成功
else
return -1; //查找失败
}
}
6 索引表顺序查找性能分析
ASL = ASL(索引表) + ASL(块内)
7 三种查找方法的比较
5 哈希查找
0次比较,时间复杂度:O(1)
1 哈希函数的定义
一般情况下,需在关键字与记录在表中的存储位置之间建立一个函数关系,以 H(key) 作为关键字为 key 的记录在表中的位置,通常称这个函数 H(key) 为哈希函数。
- 哈希查找需要做两方面事情:选择一个“好”的哈希函数;提供一种“处理冲突”的方法。
- 地址空间存储的数据集合称为哈希表。
一个好的哈希函数应该满足以下两个条件:
- 计算简单
- 冲突少
2 哈希函数的构造方法
- 直接哈希函数:取关键字本身或关键字的某个线性函数值作为哈希地址。即 H(key) = key 或 H(key) = a * key + b
- 数字分析法:设n个d位数的关键字,由r个不同的符号组成,此 r 个符号在关键字各位出现的频率不一定相同,可能在某些位上均匀分布,即每个符号出现的次数都接近于 n/r 此,而在另一些位上分布不均匀。则选择其中分布均匀的 s 位作为哈希地址,即 H(key) = "key 中数组均匀分布的 s 位”。
- 平方取中法:取关键字平方后的中间几位作为哈希地址,即哈希函数为:H(key) = "的中间几位",其中,所取的位数由哈希表的大小确定。
- 折叠法:关键字位数较长时,可将关键字分割成位数相等的几部分(最后一部分位数可以不同),取这几部分的叠加和(舍去高位的进位)作为哈希地址。位数由存储地址的位数确定。
叠加时有两种方法:
- 移位叠加法:即将每部分的最后一位对齐,然后相加。
- 边界叠加法:即将关键字看作一纸条,从一端向另一端沿边界逐次折叠,然后对齐相加。
此方法适合于:关键字的数字位数特别多。
- 除留余数法:取关键字被某个不大于哈希表长度 m 的数 p 除后的余数作为哈希地址,即:H(key) = key MOD p(p<=m)。其中 p 的选择很重要,如果选的不好会产生很多冲突。比如关键字都是10的倍数,而 p = 10
- 随机数法:选择一个随机函数,取关键字的随机函数值作为哈希地址,即:H(key) = random(key)。其中 random 为随机函数,且对于相同的关键字值,该函数的取值相同。
实际工作中需根据不同的情况采用不同的哈希函数。通常需要考虑的因素有:
- 计算哈希函数所需时间
- 关键字的长度
- 哈希表的大小
- 关键字的分布情况
- 记录的查找频率
3 字符串的哈希查找
哈希函数具有的特点:
- H(key) 必须能够计算任意关键字且冲突最少
- H(key) 应该均匀分布。比如对任意 x 和 i ,有如下概率 probability(H(x) = i) = 1/b 。这种函数叫(均匀分布哈希函数)
除留取余法:H(key) = key % TableSize (if key is an integer)
TableSize = prime number 。小于哈希表长度的最大质数。
字符串转换为整数:ASCII 码
4 冲突处理
- 冲突:指由关键字得到的 Hash 地址上已有其它记录。
- 冲突处理:为出现哈希地址冲突的关键字查找下一个哈希地址。
1) 开放地址法
为产生冲突的地址 H(key) 求得一个地址序列:
其中:
其中: 为第 次冲突的地址,
为 Hash 函数值
m 为Hash 表表长; 为增量序列。
对增量 有三种取法:
- 线性探测再散列: 最简单的情况
- 平方探测再散列: 或者
- 随机探测再散列: 是一组伪随机数列
实例:
- 若哈希表存储的内容已达到表长的一半,则将表长扩大为原来的两倍,避免数据堆积,造成大量冲突。
查找失败时注意:需要 mod ,循环。 只有找到没有元素的空位才停止。
2) 再哈希法
将 n 个不同哈希函数排成一个序列,当发生冲突时,由 RHi 确定第 i 次冲突的地址 Hi 。即:Hi = RHi(key) i = 1,2,...,n
其中: RHi 为不同哈希函数。
这种方法不会产生“聚类”,但会增加计算时间。
3) 链地址法
将所有哈希地址相同的记录都链接在同一链表中。
4) 公共溢出区法
假设某哈希表函数的值域[0,m-1],向量 HashTable[0,m-1] 为基本表,每个分量存放一个记录,另设一个向量 OverTable[0,v] 为溢出表。将与基本表中的关键字发生冲突的所有记录都填入溢出表中。
5 哈希表的查找
在哈希表上查找的过程和哈希表构造的过程基本一致。
Status SearchHash(HashTable H, KeyType key, int &p, int &c)
{
//在开放地址哈希表中查找关键字为key的数据
//用c记录发生冲突的次数,初值为0
p = Hash(key); //求哈希地址
while(H.data[p].key != NULL && H.data[p].key !=key) //该位置填有数据且与所查关键字不同
collision(p,++c); //求下一探查地址 p
if(H.data[p].key == key)
return success; //查找成功,p返回待查数据元素位置(p是引用类型)
else
return unsuccess; //查找不成功,p返回插入位置
}
哈希表插入算法:
Status InsertHash(HashTable &H, DataType e)
{
//查找不成功时在H中插入数据元素e,并返回success
//若冲突次数过大,则重建哈希表
int c = 0;
if(SearchHash(H, e.key, p, c))
return unsuccess; //数据已在哈希表中,不需插入
else if (c<hashsize[H.sizeindex]/2)
{
H.data[p] = e;
++H.count; //次数c还未达到上限,插入e
}
else
RecreatHashTable(H); //重建哈希表
}
哈希表查找与插入算法举例:
6 查找性能分析
一般情况下,可以认为选用的哈希函数是“均匀”的,则在讨论ASL时,可以不考虑哈希函数的因素。
实际上,哈希表的ASL是处理冲突方法和装载因子的函数。
查找成功时的平均查找长度:
查找失败时的平均查找长度: