目录
6.多路查找树(B树)
7.散列表(哈希表)
7.1 散列表定义
7.2 散列表查找步骤
7.3 散列的缺点
7.4 散列函数的构造方法
7.5 散列冲突的解决方法
7.6 散列表查找算法实现
7.7 散列表查找性能分析
8.总结回顾
参考博客
多路查找树(multi-way search tree),其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。
是为了降低对硬盘等外部设备的访问次数所构建的新型数据结构。
2-3树
2-3-4树
B树
B+树
参考博客
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。
我们把这种对应关系f称为散列函数/哈希(Hash)函数。采用散列技术将记录存储在一块连续的存储空间中,这块连续空间称为散列表或哈希表(Hash Table),关键字对应的记录存储位置称为散列地址。
整个散列过程分为两步:
(1)在存储时,通过散列函数计算记录的散列地址,并按散列地址存储该记录;
(2)当查找记录时,通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。
所以说,散列技术既是一种存储方法,也是一种查找方法。与线性表、树和图不同,散列技术的记录之间无逻辑关系,只与关键字有关。因此,散列主要是面向查找的存储结构。
散列技术最适合的求解问题是查找与给定值相等的记录。
(1)散列技术不具备很多常规数据结构的能力。比如,同样的关键字对应很多记录的情况,同样,散列表也不适合范围查找,比如查找一个班级18-22岁的同学;
(2)另一个问题是冲突。两个关键字key1与key2不相等,但f(key1)=f(key2),这种现象称为冲突(collision),并把key1与key2称为这个散列函数的同义词(synonym)。如何处理冲突成了一个很重要的话题。
设计一个简单、均匀、存储利用率高的散列函数是散列技术中最关键的问题。
构造原则:
(1)计算简单。散列函数的计算时间不应超过其他查找技术与关键字比较的时间;
(2)散列地址分布均匀。尽量让散列地址均匀地分布在存储空间中,这样可以保证存储空间的有效利用,并减少为处理冲突而耗费的时间。
构造方法:
总结:
直接定址法需事先知道关键字分布情况,适合查找表小且连续的情况,因此在现实应用中并不常用;
数字分析法适合于事先知道关键字分布且关键字的若干位分布较均匀的情况;
平方取中法适合于不知道关键字的分布,而位数又不是很大的情况;
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况;
随机数法适合于关键字的长度不相等的情况;
除留取余法为最常用的构造散列函数法。
1.开放定址法
2.再散列函数法
3.链地址法
4.公共溢出区法
首先是需要定义一个散列表的结构以及一些相关的常数。其中,HashTable就是散列表结构。结构中的elem为一个动态数组。
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 //定义散列表长为数组的长度
#define NULLKEY -32768
typedef struct
{
int *elem; //数据元素存储基址,动态分配数组
int count; //当前数据元素个数
}HashTable;
int m = 0; //散列表表长,全局变量
有了结构的定义,我们可以对散列表进行初始化。
/*初始化散列表*/
Status 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] = NULLKEY;
return OK;
}
为了插入时计算地址,需要定义散列函数,散列函数可以根据不同情况更改算法。
/*散列函数*/
int Hash(int key)
{
return key%m; //除留余数法
}
初始化完成后,可以对散列表进行插入操作。假设插入的关键字集合是{12,67,56,16,25,37,22,29,15,47,48,34}。
/*插入关键字进散列表*/
void InsertHash(HashTable *H, int key)
{
int addr = Hash(key); //求散列地址
while (H->elem[addr] != NULLKEY) //如果不为空,则冲突
addr = (addr + 1) % m; //开放定址法的线性检测
H->elem[addr] = key; //直到有空位后插入关键字
}
代码中插入关键字时,首先算出散列地址,如果当前地址不为空关键字,则说明有冲突。此时我们应用开放定址法的线性探测进行重新寻址,此处也可更改为链地址法等其他解决冲突的办法。
散列表存在后,在需要时就可以通过散列表查找要的记录。
/*散列表查找关键字*/
Status SearchHash(HashTable H, int key, int *addr)
{
*addr = Hash(key); //求散列地址
while (H.elem[*addr] != key) //如果不为空,则冲突
{
addr = (addr + 1) % m; //开放定址法的线性检测
if (H->elem[addr] == NULLKEY || *addr = Hash(key))
{
//如果循环回到原点
return UNSUCCESS; //则说明关键字不存在
}
}
return SUCCESS;
}
如果没有冲突,散列查找是我们介绍的所有查找中效率最高的,因为它的时间复杂度为O(1),但没有冲突的散列只是一种理想,实际中,冲突不可避免。那么散列查找的平均查找长度取决于哪些元素?
1.散列函数是否均匀
散列函数的好坏直接影响出现冲突的频繁程度,不过,由于不同的散列函数对同一组随机的关键字,产生冲突的可能性相同,因此不考虑其对平均查找长度的影响。
2.处理冲突的方法
比如线性探测处理冲突可能会产生堆积,显然没有二次探测法好,而链地址处理冲突不会产生堆积,因而具有更佳的平均查找性能。
3.散列表的装填因子
所谓的装填因子a=填入表中的记录个数/散列表长度。a标志着散列表的装满的程度。当填入表中的记录越多,a就越大,产生冲突的可能性越大。通常,将散列表的空间设置的比查找空间大,虽然浪费一定空间,但换来的是查找效率的大大提升。
顺序表查找虽然简单,但是后面很多查找的基础,注意设置哨兵的技巧。
有序查找,介绍了折半查找、插值查找和斐波那契查找,三者各有优缺点。
线性索引查找,介绍了稠密索引、分块索引和倒排索引。索引技术被广泛的用于文件检索、数据库和搜索引擎等技术领域。
二叉排序树是动态查找最重要的数据结构,可以在兼顾查找性能的基础上,使插入和删除也变得效率很高。排序树最好保持平衡,因此介绍了AVL树。
B树这种数据结构是针对内存与外存之间的存取而专门设计的。另外介绍了B+树。
散列表是一种非常高效的查找数据结构,对于那种查找性能要求高,记录之间无要求的数据有非常好的适用性。注意散列函数的选择和处理冲突的方法。