目录
6. 查找
6.1 查找分类
- 静态表查找:查询某个元素是否在查找表中,不需要进行插入和删除操作(利用顺序表和散列表效率高)
- 动态表查找:查找时插入数据元素,查找时删除数据元素(利用二叉搜索树保存效率高)
6.2 顺序表查找
利用设置哨兵的方法,将第一个元素设置为要比较的值,这样可以减少几次判断
/* 暴力搜索的方法 */
int SequetialSearch(int *a, int n, int key)
{
int i;
a[0] = key;
i = n;
while (a[i] != key) {
i--;
}
return i;
}
6.3 有序表查找
如果事先再存储的过程中,就已经时有序的了,那么可以使用二分法查找Binary Search 二分法查找的判断次数,只需要完成 log(n)+1次的判断,就可以找到关键值。
#include <stdio.h>
/* 二分法查找 */
int BinarySearch(int *a, int len, int key)
{
int low, high, mid;
low = 0;
high = len - 1;
while (low <= high) {
mid = low + (high - low) / 2;
if (key < a[mid]) {
high = mid - 1;
} else if (key > a[mid]) {
low = mid + 1;
} else {
return mid;
}
}
return -1;
}
int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int key = 6;
printf("%d", BinarySearch(a, 10, key));
return 0;
}
二分法查找的改进版本可有插值法和斐波那契查找,可以应对要查找的值在很靠近两边的位置时,减少查询的次数
- 插值法 mid = low + (high - low) * (key - a[low]) / (a[high] - a[low])
6.4 线性索引查找
- 稠密索引:元素信息为关键码+指针,关键码时有序排列,指针指向各自数据地址
- 分块索引:元素信息包括最大关键码+块长+快首地址,关键码为顺序排列,可以使用二分查找到,然后根据块长和指针计算得到存储地址,块内部的关键码信息不要求顺序排列,否则时间成本太大
- 倒排索引(搜索引擎):建立出来单词表,元素信息包括英文单词+文章编号,这样就可以根据输入的单词信息,索引出包含该单词的文章编号。
6.5 散列表哈希表
根据散列函数,哈希函数,建立出关键码对应的存储位置,散列表是一块连续存储的空间,这块空间叫做散列表或者哈希表,关键码对应的地址叫做哈希地址。
6.5.1 散列函数构造方法
目的是为了让散列表分布的更均匀,设计合理的散列函数
- 直接定址法:如果关键码信息进行线性计算,那可以按照:
f(key) = a * key + b
但是需要实现知道关键字的分布情况,一般不这样使用
- 数字分析法:通过提取部分关键数字,进行反转,左移或者右移,或者前后两位相加等方式,适用于数字位数很多的时候
- 平方取中法:将关键码计算平方,取中间的三位作为关键码,该方法适用于不知道关键码分布,而且位数不是很大的情况下
- 折叠法:将关键码按照三位一组进行拆分,然后进行求和,如9876543210可以分为987|654|321|0四组,然后求和得到值为1962,再根据散列表的长度,取后三位或者后两位作为散列地址。该方法适用于事先不知道关键码分布,适合关键字较长
- 取余法:该方法为最常用的哈希函数,公式如下:
f(key) = key mod p (p <= m)
通常p的值选择为不大于表长度m的最大质数(素数)
- 基数转换法:将关键码看作是以r为基数的,将其转换为10进制或者2进制,然后再将得到的值用对叠法
6.5.2 散列冲突处理方法
分为内消解方法和外消解方法两种
- 内消解方法:还是再线性表的范围内找一块地址存入
- 线性探测法:在原哈希函数的位置上加上正数线性偏移:
f(key) = (f(key) + d) mod p (d = 1,2,3,…,m-1)
- 二次探测法:线性探查只能往后探测,如果冲突位置的前面有空余,也可以使用
f(key) = (f(key) + d) mod p (d = 12, -12, 22, -22, … q2, -q 2 q < m/2)
- 再哈希法:在冲突的位置再利用一个新的哈希函数
f(key) =RH(key) RH代表一个新的哈希方法,例如基数转换,折叠法,平方取中
- 外消解方法:还是在线性表的范围内找一块地址存入
- 桶散列(链地址法):在冲突的哈希地址处,存储一个头指针,指向所有存储在该位置的关键码链表,因此不会出现找不到地址的情况,但是会造成遍历链表的性能损耗
- 公共溢出区法:在散列表的外部单独申请一块存储区域,当哈希冲突时,将值顺序的存储在溢出区内,在冲突数据较少的情况下,性能还是非常高的。
6.5.3 散列表性能分析
当负载因子的值越大,冲突发生的可能性越高,因此探查就不是O(1)的时间复杂度了,当a < 0.7是,散列的查找,插入和删除可以看作是常量复杂度。
6.5.4 散列表实现
#include <stdio.h>
#define SUCCESS 0
#define FAIL 1
#define HASHSIZE 12
#define NULL -32768
typedef struct {
int *elem;
int count;
} HashTable;
int m = 0;
void InitHashTable(HashTable *H)
{
int i;
m = HASHSIZE;
H->count = m;
H->elem = (int *)malloc(m * sizeof(int));
for (i = 0; i < m; i++) {
H->elem[i] = NULL;
}
}
void Hash(int key)
{
return key % m;
}
void InsertHash(HashTable *H, int key)
{
int addr = Hash(key);
while (H->elem[addr] != NULL) {
addr = Hash(key+1) //线性探测
}
H->elem[addr] = key;
}
void SearchHash(HashTable *H, int key, int *addr)
{
*addr = Hash(key);
while (H->elem[*addr] != key) {
*addr = Hash(*addr+1);
if (H->elem[*addr] != NULL || *addr == Hash(key)) {
return FAIL;
}
}
return SUCCESS;
}