1 前言
在讲散查找之前,我们先说一下为什么要使用散列查找
现在已知的几种查找方法:
查找方法 | 复杂度 |
---|---|
顺序查找 | O(N) |
二分查找(静态查找) | O(log2N) |
插值查找 | |
斐波那契查找 | O(log2N) |
二叉搜索树 | O(h) h为树的高度 |
平衡二叉树 | O(log2N) |
用二叉搜索树查找前面在介绍 二叉排序树(BST)的创建,查找,插入,删除及最大最小结点的时候给出了详细的实现。
除了上面的查找算法,还有其他方法吗?
下面来看一个例子:在登录QQ的时候,QQ服务器是如何核对你的身份?面对庞大的用户群,如何快速找到用户信息?
分析:看看是否可以用二分法查找,解决下面三个问题。
- 十亿(109 ≈ 230)有效用户,用二分查找30次。
√ 可行
- 十亿(109 ≈ 230) × 1K ≈ 1024G,1T连续空间(每个用户1k的空间储存信息)。
√ 可行
- 按有效QQ号大小有序存储:在连续存储空间中,插入和删除一个新QQ号码将需要移动大量数据。
× 不可行
我们这里是动态查找,需要不断的插入,删除元素,采用二分查找,每次插入删除都需要移动大量的元素,这样显然是不可行的。
现在问题来了,如何快速搜索到需要的关键词?如果关键词不方便比较怎么办?
-
查找的本质: 已知对象找位置。
- 有序安排对象:全序(二分查找)、半序(查找树)
- 直接“算出”对象位置:散列直接“算出”对象位置:
散列
-
散列查找法的两项基本工作:
- 计算位置:构造散列函数确定关键词存储位置;
- 解决冲突:应用某种策略解决多个关键词位置相同的问题
-
时间复杂度几乎是常量:O(1),即查找时间与问题规模无关!
以上通俗的说,就是数据太多了,整理起来太麻烦了,按照大小有序存放,组织成树都太麻烦了,工作量非常大。干脆不整理,给我一个关键字,直接带入公式,计算出关键字的位置,就找到想要的元素了,由于是用公式计算出关键字的存储位置,所以时间复杂度是常数级别的,O(1),不管有多少元素,一次计算出结果。但是这也有问题,因为不同的关键字带入同一个公式,可能计算出的结果是相同的,这就产生了冲突,所以这就是散列查找的另一个工作,解决冲突,如何计算位置,如何解决冲突,后面将会一一说明。
2 散列表(哈希表)
类型名称:符号表(SymbolTable)
数据对象集:符号表是“名字(Name)-属性(Attribute)”对的集合。
操作集:Table ∈ SymbolTable,Name ∈ NameType,Attr ∈ AttributeType
1、SymbolTable InitializeTable( int TableSize ):创建一个长度为TableSize的符号表;
2、Boolean IsIn( SymbolTable Table, NameType Name):查找特定的名字Name是否在符号表Table中;
3、AttributeType Find( SymbolTable Table, NameType Name):获取Table中指定名字Name对应的属性;
4、SymbolTable Modefy(SymbolTable Table, NameType Name, AttributeType Attr):将Table中指定名字Name的属性修改为Attr;
5、SymbolTable Insert(SymbolTable Table, NameType Name, AttributeType Attr):向Table中插入一个新名字Name及其属性Attr;
6、 SymbolTable Delete(SymbolTable Table, NameType Name):从Table中删除一个名字Name及其属性。
“散列(Hashing)” 的基本思想是:
- 以关键字key为自变量,通过一个确定的函数 h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址。
- 可能不同的关键字会映射到同一个散列地址上,可能不同的关键字会映射到同一个散列地址上,
即h(keyi) = h(keyj)(当keyi ≠keyj),称为“冲突(Collision)”。
----需要某种冲突解决策略
下面用两个具体的例子来说明散列查找的具体过程,也好有一个直观的感受。
【例】 有n = 11个数据对象的集合{18,23,11,20,2,7,27,30,42,15,34}。
符号表的大小用TableSize = 17,选取散列函数h如下:
h(key) = key mod TableSize (求余)
地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
关键词 | 34 | 18 | 2 | 20 | 23 | 7 | 42 | 27 | 11 | 30 | 15 |
-
存放:
h(18)=1, h(23)=6, h(11)=11, h(20)=3, h(2)=2, …….
如果新插入35, h(35)=1, 该位置已有对象!冲突!!
-
查找:
key = 22, h(22)= 5,该地址空,不在表中
key = 30, h(30)= 13,该地址存放是30,找到!
装填因子
(Loading Factor):设散列表空间大小为m,填入表中元素个数是n,则称α= n / m为散列表的装填因子
α=11 / 17 ≈ 0.65。
【例】将acos、define、float、exp、char、atan、ceil、floor、clock、ctime,顺次存入一张散列表中。
散列表设计为一个二维数组Table[26][2],2列分别代表2个槽。
设计散列函数h(key) = key[0] – ‘a’
将cos、define、float、exp、char、atan、ceil、floor、clock
、ctime
依次装入散列表中,如下图所示,最后两个元素clock、ctime冲突,溢出了,没有地方放置。
假设没有上面溢出的问题,看一下散列查找的时间复杂度:
T查询= T插入= T删除= O( 1 )
3 总结
- 为什么要使用散列查找(动态查找,时间复杂度近似常数)
- 散列查找基本工作:计算位置,解决冲突