之所以会接触到散列,是因为在学习中遇到这么一个问题:“根据课堂中所介绍的散列索引原理,实现一个自增长散列索引的原型。”
那么就根据上面这个问题,来探讨下关于散列的一些基础知识。
一、什么是散列?散列“牛”在哪里?
1.1 什么是散列
这里说下传统的查找操作,设计如下场景:
角色:班主任、校长、学生
需求:找学生中的“张三”
那么场景1:校长要找“张三”,笨的一种方式,是获取学生名单,然后进行比对,发现“张三”,则找人成功。
场景2:还是校长找“张三”,通过班主任找,班主任可以直接找到自己的学生“张三”,则找人成功。
这里的场景1与场景2,则是传统的查找操作与“散列”的对比:
传统查找通过给定的key,通过比对获取相应的value,这个比对的过程是不可省略的。
散列查找,则是通过某种关系f,直接找到相应的key对应的value,其中没有比对的过程,因此效率非常高。
那么给出一个散列的小概念:存储位置 = f( key ),同时我们称这个关系f为散列函数,又称哈希函数。
1.2 散列牛在哪里
散列牛在:我们通过查找关键字,不需要比较就可以获得需要记录的存储位置。这个也称之为散列技术。
下面为散列技术与散列表做一个简单的定义:
"散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f ( key )"
查找时,根据这个确定的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。
按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(HashTable)。那么关键字对应的记录存储位置称为散列地址。
二、散列是如何查找的?
2.1 散列的过程
散列的过程分为两步:
1.在存储的时候,通过散列函数计算记录的散列地址,并按此散列地址存储该记录
2.当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。
通过这些分析,可以发现散列与通用的存储结构如线性表、树等区别是:通用数据结构之间元素与元素之间会存在某种逻辑关系,而散列的记录之间不存在逻辑关系,每条记录只与关键字有关。因此,散列主要是面向查找的。
2.2 散列表的优势与劣势
散列技术的特长在于:查找与给定key相符的记录。(简化了比较过程,效率提升)
散列的劣势则在于 :1th. 查找一对多的关系,如对班级学生做散列后,以性别为key,进行查找,是不合适的
2th. 范围查询、最大值、最小值这种具有比较性质的操作,散列也是无从计算的。
2.3 散列存在的“大坑”——哈希冲突
散列算法有一个严重的问题,就是当key1 ≠ key2,但却有 f ( key1 ) = f ( key2 );当这种情况发生时,我们称之为冲突(collision),并把key1和key2称为这个散列函数的同义词(synonym)
通常解决哈希冲突的办法有:1. 开放定址法(Open Addressing)
2. 拉链法(Chaining)
三、哈希冲突的解决办法
注:本节的例子与配图来自——“现代魔法学院”,附录中会注明该网站地址
3.1 开放定址法
开放定址法就是当发生冲突时,就寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
对于开放定址有如下几种方式:
- 线性探测法
- 二次探测法
- 随机探测法
3.1.1 线性探测法
线性探测法公式:fi ( key ) = ( f ( key ) + di ) MOD m ( di = 1, 2, 3, ......, m - 1 )
假设有关键字集合:{ 12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34 }
采用的散列函数 f ( key ) = key mod 12
下面进行散列操作:
f ( 12 ) = 12 mod 12 ----> 0
f ( 67 ) = 67 mod 12 ----> 7
f ( 56 ) = 56 mod 12 ----> 8
f ( 16 ) = 16 mod 12 ----> 4
f ( 25 ) = 25 mod 12 ----> 1
此时的存储状态为:
当继续进行计算 f ( 37 ) = 37 mod 12 ----> 1,此时与25所在的位置发生了冲突,于是应用上面的公式
f ( 37 ) = ( f ( 37 ) + 1 ) mod 12 = ( 1 + 1 ) mod 12 ----> 2。于是将37插入地址为2的位置:
随后的22, 29, 15, 47均正常插入
当插入48时,f ( 48 ) = 48 mod 12 ----> 0,与12的位置发生冲突,依旧采用上述公式,不过这次当di = 6时,才解决冲突 :
可以看出,解决冲突时,会出现48和37这种不是同义词却会争夺同一个地址的情况,称这种情况为堆积。堆积的出现,会使我们不断的处理冲突,无论是插入还是查找效率都会变低。
3.1.2 二次探测法
二次探测法公式:fi ( key ) = ( f ( key ) + di ) MOD m ( di = 1^2, -1^2, 2^2, -2^2, ......, q^2, -q^2 q <= m / 2 )
二次探测法思路:以双向的形式,寻找可能的空位置;另外增加平方运算是为了不让关键字都聚集在某一区域
3.1.3 随机探测法
随机探测法公式:fi ( key ) = ( f ( key ) + di ) MOD m ( di是一个伪随机数列 )
3.1.4 小结
开放定址的前提是:散列表未满时,总能找到不发生冲突的地址
3.2 拉链法
拉链法的思想是当发生冲突时,将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,而散列表中只存储所有同义词子表的头指针(每个子表可以理解为一个bucket,而散列表可以为一个Content)。
还是对上述的关键字通过拉链法进行散列{ 12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34 },依旧MOD 12:
拉链法思路:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的节点,均插入到以T[i]为头指针的单链表中。T中个分量的初值均应为空指针。
注意:这里的单链表并没有进行长度限制,这是不合理的,通常会规定一个阀值,当超过阀值的时候,会将这个单链表(也可称为bucket)进行分裂处理。如果从时间复杂度的角度考虑,当存在m个单链表,寻找到确定的单链表的时间复杂度为O(1)(只进行了一次散列),当从单链表进行元素匹配时的最坏时间复杂度将为O(n)(n为元素个数)。这在某些场景下是不允许的。涉及到的分裂,将是动态Hash了,会下后续的文章中讨论,并介绍几种实现。
3.3 题外话,数组与链表的优劣
数组:查找容易,删除难
链表:查找难,删除容易
四、小结
本次介绍了一些散列的基本概念,包括散列的定义,散列的优势、劣势等,在随后篇幅中将会对散列的实现、动态散列的实现,散列的应用场景加以描述!
五、参考资料