- 检索(Search ) 在一组记录集合中找到关键码值等于给定值的某个记录,或者找到关键码符合某种条件的一些记录。
- 检索效率很重要,需要对数据进行特殊的存储处理。
- 特殊处理方法:预排序、建立索引、散列技术、B树方法
- 检索核心操作:关键码的比较
- 平均检索长度(Average Search Length ):检索过程中对关键码的平均比较次数ASL
ASL=∑npiCi
其中 n 是可能检索的所有关键码,Ci 是检索第 i 个关键码的次数,pi 是检索第 i 个的概率。 - 评估检索的算法:ASL,算法所需存储量,算法的复杂性
基于线性表的检索
顺序检索
对线性表中的所有记录,逐个把它们的关键码和给定值进行比较。
//Item 类(非完全)
template<class Type>
Class Item{
private:
Type key;
public:
Item(Type value):key(value){}
Type getKey() {return key;}
void setKey(Type value){key = value};
};
vector<Item<Type>*> dataList;
//顺序检索算法
template<class T>
int SeqSearch(vector<Item<Type>*>& dataList,int length,Type k){
int i = length;
dataList[0] = k; //将第一个元素设置为给定值作为监视哨
while(dataList[i] != k)
i --;
return i; //找到第一个关键码符合的记录,返回
}
算法分析:
- 最好情况:1次
- 最坏情况:n+1次
- 检索成功时,假设检索每个关键码的值相等,平均次数为
ASL′=∑i=1n1n⋅i=n+12 - 设检索成功的概率为 p
ASL=p⋅n+12+(1−p)⋅(n+1)=(n+1)(1−p2) - ASL在 n+12 和 n+1 之间
二分检索
- 要求线性表有序
- 每一次比较缩小一半的范围
template<class Type>
int BinSearch(vector<Item<Type>*>& dataList,int length,Type k){
int low = 1; high = length;mid;
while(low < high) //循环
{
mid = (low + high)/2; //二分
if(dataList[mid] == k)
return mid;
else if(dataList[mid] < k)
low = mid;
else if(dataList[mid] > k)
high = mid;
}
return 0; //检索失败返回0
}
算法分析
最大检索长度: [log2[n+1]]
ASL=∑i=1[log2(n+1)]1n⋅i2i−1=1n[(log2(n+1)−1)(n+1)+1]≈log2(n+1)−1优点:检索速度快。
- 缺点:需要排序、不易增删
分块检索
- 顺序与二分法的折衷:兼顾速度与灵活性
- 分块的条件:不需要均匀,块内不一定要有序,只要保证前一块中的最大关键码小于后一块中的最小关键码。
- 建立索引表,每个块用一个结点,记录块的最大关键码、起始位置、块的长度(可能不满)。
- 索引表是递增的有序表。
性能分析
- 设
n
个元素分成
b 块 - 两级检索:检索块 ASLb +块内 ASLw , ASL=ASLb+ASLw
- 如果两级都用顺序检索
- ASLb=b+12,ASLw=s+12⇒ASL=b+12+s+12=b+s2+1=n+s22s+1
- 当 s=n√ 时,ASL取最小值, ASL≈n√ 。
- 速度比顺序检索快,比二分检索慢。
- 如果数据块放在外存,还会受到页块大小的制约。
- 如果采用二分检索确定记录所在子表
- ASL=log2(b+1)−1+s+12≈log2(1+ns)+s2
- 优点:插入删除容易、没有大量移动数据
- 缺点:增加辅助存储空间、初始线性表分块排序、结点分布不均时速度下降
集合的检索
用位向量表示集合
- 适用于密集型集合(数据范围小,集合中有效元素较多),比如查找“奇素数”
散列
检索是直接面向用户的操作。当问题规模很大时,前述基于关键码检索的时间效率可能使用户无法忍受。最理想的情况就是根据关键码直接找到记录的存储地址。
数组按下标读取就是 O(1) 的操作,和数组的规模无关。由此产生了散列方法。
基本思想
- 一个确定的函数 h
- 以结点的关键码
K 为自变量 - 函数值 h(K) 作为结点的存储地址
- 检索的时候根据这个函数计算存储位置
- 散列表的存储空间通常是一个一维数组
- 散列地址是数组下标
- 负载因子 α=nm=散列空间大小表中的结点数
- 冲突 不相同的关键码得到相同的散列地址
- 同义词 发生冲突的两个关键码
核心问题
- 如何构造散列函数?
Address=Hash(Key)
- 运算尽可能简单
- 值域必须在表长范围内
- 尽可能使避免冲突
- 综合考虑:关键码长度、散列表大小、关键码分布情况、记录的检索频率
- 如何解决冲突?
- 开散列方法(open hash ),也叫拉链法(separate chaining ),把发生冲突的关键码存在散列表主表之外。
- 闭散列方法(closed hashing ),也叫开地址法(open addressing),把发生冲突的关键码存在表中另一个槽内。
常用散列函数
- 除余法
- 用散列长度模关键码 h(x)≡x(modM)
-
M
一般取质数,增大分布均匀的可能性:函数值依赖于关键码
x 所有位 - 不用偶数、幂
- 缺点:连续的关键码映射成连续的函数值,可能导致散列性能降低。
- 乘余取整法
- 先让关键码乘一个数
A(0<A<1)
,取小数部分,乘以散列长度
n
,向下取整。
h(x)=[(x×A−[x×A])×n] - 优点:对
n
的选择无关紧要:地址空间有
k 位,就取 n=2k - Knuth认为A可以取任何值,但是取黄金分割最理想
- 先让关键码乘一个数
A(0<A<1)
,取小数部分,乘以散列长度
n
,向下取整。
- 平方取中法
通过平方扩大关键码之间的差别,再取其中几位或组合作为散列地址 数字分析法
- 以
n
个
d 位数为例 - 每一位可能有 r 个可能的符号
- 这
r 种不同的符号在各位上出现的频率不一定相同 - 根据散列表的大小,选取各符号分布均匀的若干位作为散列地址
- 计算各个位上符号分布的均匀度 λk=∑ri=1(αki−n/r)2
- 其中
αi
表示第
i
个符号在第
k 位上出现的次数。 - λ 越小,说明在 k 位上分布越均匀
- 仅适用于事先明确知道表中所有关键码在每一位上的分布情况,完全依赖于关键码的集合
- 如果换一个关键码的集合,就要重新进行分析。
- 以
n
个
基数转换法
- 把关键码看成另一进制上的数
- 把它转换成原来进制上的数
- 取其中若干位作为散列地址
- 一般取大于原来基数的数作为转换的基数,并且两个基数要互素
- 折叠法
- 将关键码分割成位数相同的几部分(最后一部分位数不一定相同),取叠加和作为散列地址。
- 分为移位叠加和分界叠加。
- ELFhash字符串散列函数
开散列方法
- 拉链法
- 插入同义词时,可以对同义词链排序插入。
- 性能分析:给定一个大小为
M 存储 n 个记录的表,理想情况下,散列把记录在表中M个位置平均放置,使得每个链表中有nM 个记录。如果 M>n ,散列方法的平均代价就 Θ(1) - 不适用与外存检索
2.桶式散列
- 适合于存储在磁盘的散列表
- 一个文件分成多个存储桶,每一个存储桶包含一个和多个页块。每个页块包含若干记录,页块之间用指针连接。
- 散列函数 h(K) 表示关键码是 K 的记录所在的存储桶序号。
闭散列方法
- 基地址位置:
d0=h(K) - 当冲突发生时,使用某种探查方法为关键码
K
生成一个散列地址序列
d1,d2,… ,所有的 di 是后继散列地址 - 当插入 K 时,如果基地址上的结点已经被其他数据元素占据,按这种探查方法后继地址,找到第一个空闲位置。如果满了,报告溢出。
- 检索的策略(探查方法)必须和插入相同。
探查方法
- 线性(顺序)探查
- 如果记录的基位置被占用,顺序地查找表中第一个空闲地址。
p(K,i)=i - 缺点:会发生聚集,导致很长的探查序列。
- 改进:每次跳 c 个位置,但是还是可能导致聚集(clustering)。
p(K,i)=i∗c
- 二次探查
探查的增量是p(K,i)={12,−12,22,−22,…} - 伪随机数序列探查
p(K,i)=perm[i−1]
- 优点:二次探查和伪随机数序列探查都可以消除聚集。
- 缺点:二次探查和伪随机数序列探查都不能消除二次聚集,即如果两个关键码散列到同一个基地址,得到的探查序列还是相同的,因而发生聚集。
- 双散列探查
- 探查序列不仅仅是基地址的函数,还是是原来关键码的函数
- 使用两个散列函数 h1,h2
- 利用第二个散列函数作为常数,每次跳过常数项,做线性检查
-
p(K,i)=i∗h2(key)
- h2(key) 必须与 M 互素。
- 不易产生聚集,但是计算量增大。
- 如果记录的基位置被占用,顺序地查找表中第一个空闲地址。
- 线性(顺序)探查
算法实现
- 字典(dictionary),一种特殊的集合,元素是(关键码,属性值)二元组。
- 同一个字典内关键码必须是不同的。
主要操作:依据关键码来存储和析取值
insert(key,value),lookup(key)
template<class Key,class Elem,class KEComp,class EEComp>class hashdict{ private: Elem * HT; //散列表 int M; //散列表大小 int current; //现有元素数目 Elem EMPTY; //空槽 int p(Key K,int i); //探查函数 int h(int x) const; //散列函数 int h(char * x) const; //字符串散列函数 public: hashdict(int sz,Elem e){ M = sz; EMPTY = e; current = 0; HT = new Elem[sz]; for(int i = 0;i < M;i++) HT[i] = EMPTY; } ~hashdict(){delete [] HT;} bool hashSearch(const Key&,Elem &) const; bool hashInsert(const Key& K); Elem hasDelete(const Key& K); int size() {return current;} };
插入算法:基地址未被占用则直接插入,如果非空闲且已经是
K 则报告已经插入,否则按照探查方法找到下一个地址并循环,直到插入成功。//插入算法 template<class Key,class Elem,class KEComp,class EEComp> bool hashdict<Key,Elem,KEComp,EEComp>::hashInsert(const Elem& e){ int home = h(getkey(e)); //找到新结点基地址 int i = 0; int pos = home; while(!EEComp::eq(EMPTY,HT[pos])){ if(EEComp::eq(e,HT[pos])) return false; i++; pos = (home + p(getkey(e),i)) % M; //探查 } HT[pos] = e; return true; }
检索算法:查找 K 对应的基地址,如果空闲则报错,如果非空闲且等于
K 则检索成功,否则则按照探查方法找到下一个地址并循环,知道检索成功。//检索算法 template<class Key,class Elem,class KEComp,class EEComp> bool hashdict<Key,Elem,KEComp,EEComp>::hashSearch(const Key& K,Elem e){ int i = 0; pos = home = h(K); // 初始位置 while(!EEComp::eq(HT[pos],EMPTY){ if(KEComp(HT[pos],K)){ //KE、EE的定义是什么? e = HT[pos]; //这是说e=K?检索的结果不应该是地址吗? return true; } i ++; pos = (home + p(K,i)) % M; } return false; }
删除算法:删除不能影响后面的检索,空出的位置应该可以放新的插入。只有开散列方法才可以真正实现删除,闭散列方法只能标记墓碑,不能真正删除。
//带墓碑的删除算法 template<class Key,class Elem,class KEComp,class EEComp> bool hashdict<Key,Elem,KEComp,EEComp>::hashDelete(const Key& K,Elem e){ int i = 0; pos = home = h(K); //初始位置 while(!EEComp::eq(EMPTY,HT[pos])){ if(EEComp::eq(K,HT[pos])){ Key temp = HT[pos]; HT[pos] = TOMB; return temp; } i++; pos = (home + p(K,i)) % M; } return EMPTY; } //带墓碑的插入算法 template<class Key,class Elem,class KEComp,class EEComp> bool hashdict<Key,Elem,KEComp,EEComp>::hashInsert(const Elem& e){ int insplace; int i = 0; int pos = home = h(getkey(e)); bool tomb_pos = false; while(!EEComp::eq(EMPTY,HT[pos])){ if(EEComp::eq(e,HT[pos])) return false; if(EEComp::eq(TOMB,HT[pos]) && !tomb_pos) { insplace = pos; tomb_pos = true; } i++; pos = (home + p(getkey(e),i)) % M; //探查 } if(!tomb_pos) insplace = pos; //如果没有墓碑,则insplace位于空槽位置 HT[implace] = e; return true; }
本文主要参考Wang Tengjiao课件