赞
踩
散列表(hash table)是实现字典操作的有效数据结构。在最坏情况下,查找一个元素的时间复杂度是O(n);而在合理假设情况下,查找一个元素的时间复杂度是O(1)。因此,散列表查找元素的性能是极好的。
常用于作为在常量时间范围内辅助查找一个元素。
对于普通数组,可以直接寻址,即可以直接通过数组下标访问一个元素。如果空间允许的话,可以将元素的key作为数组下标映射。但是,如果实际存储的元素要比实际可存储的元素小很多时,仍使用直接寻址法,会带来很大的空间浪费。主要缺点:
1、空间浪费;
2、一次申请庞大的空间,内存容量限制。
因此,散列表是作为对普通数组的扩展。在散列表中,不是通过关键字作为数组下标,而是根据关键字来计算出数组下标。如果多个关键字计算出同一个下标,就会出现“冲突”。所以,在理想情况下,如果完全可以避免冲突的话,散列表的元素查找时间就是O(1)。
在直接寻址方式下,元素是放在关键字为K的槽中;而散列方式下,元素是放在f(K)的槽中,其中,函数f()称为散列函数,通过散列函数来计算出槽的位置。
f(k) = k % m
其中,k是关键字,m是所映射的槽数。假设:m = 10,k = 99,则f(99) = 99 % 10 = 9
m不要取2的幂。为什么?
假设k是0~1000范围内数值,m为8,即2的3次幂。
96 % 8 = 0110 0000 % 2³ = (1*2³*2³ + 1*2²*2³) % 2³ = 0
97 % 8 = 0110 0001 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2º) % 2³ = 1*2º= 1
98 % 8 = 0110 0010 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2¹) % 2³ = 2¹= 2
99 % 8 = 0110 0011 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2¹ + 1*2º) % 2³ = 2¹+ 2º = 3
100 % 8 = 0110 0100 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2²) % 2³ = 2² = 4
101 % 8 = 0110 0101 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2² + 1*2º) % 2³ = 2² + 2º = 5
102 % 8 = 0110 0110 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2² + 1*2¹) % 2³ = 2² + 2¹ = 6
103 % 8 = 0110 0111 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2² + 1*2¹+ 1*2º) % 2³ = 2² + 2¹ + 2º = 7
104 % 8 = 0110 1000 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2³) % 2³ = 0
105 % 8 = 0110 1001 % 2³ = (1*2³*2³ + 1*2²*2³ + 1*2³ + 1*2º) % 2³ = 2º = 1
以上分析可知,余数在0~7范围内,不会超过8,也就是k的后三位就可以完全表示余数。如果关键字均匀分布在后三位,此方法可行。但如果遇到:
0000 0000(0)
0000 1000(8)
0001 0000(16)
0001 1000(24)
如上后三位均为0的情况,这些关键字都处于同一个槽中,无法很好的避免冲突。所以,好的散列函数为了尽可能的避免冲突的发生,应该尽量考虑所有位。
一个不接近2的幂次的素数是一个好的选择。
假设一个存放2000个字符的hash表,可以接受的冲突是3次,则2000/3 ≈ 667,m的选择是接近667,但不接近2的幂次的素数,因此m可以选择为673,或677,或683,或691,或701。
f(k) = ⎣m(kA % 1)⎦
其中:
1、0
2、kA % 1得到的是kA结果的小数部分
3、m是槽数,一般取2的p次
假设k=123_456,p=14,m=2^14=16_384,w=32(每个关键字占32位)
∵ 0
∴ A=s / 2^32,其中 0 < s < 2^32,当A ≈(√5 - 1)/2时,是个比较理想的值
s = A * 2^32
s=2.6544357694972305E9
∵ k和s都是32位
∴ ks = r1 * 2^32 + r2 <1>
kA = ks / 2^32,相当于将ks右移32位 <2>
∵ kA % 1 取小数部分
∴ 其中r2即为小数部分 <3>
∵ m(kA % 1),m=2^14
∴ 即将r2左移14位 <4>
r2的高14位即是f(k)的结果,f(123_456) = 67
把散列在同一个槽中的所有元素都放在一个链表中。
1、插入最坏情况时间复杂度是O(1),因为在发生冲突时,始终插入到链表头位置;
2、查找平均情况是O(1),最坏情况是O(n),即所有元素都散列到同一个槽中。
假设有n个元素,放到m个槽位中,则α=n/m,α称为装载因子。在简单均匀散列假设下:
1)一次不成功查找平均时间为O(1+α);
2)一次成功查找平均时间为O(1+α);
3、删除,对于双向链表最坏情况O(1),而对于单向链表删除和查找渐进相同。
全部字典操作都可以在平均情况O(1)时间内完成。
所有元素都放在散列表中,当插入元素发生冲突时,通过线性探查(二次探查/双重散列)将元素放入到槽位为NULL的位置。查找和插入都可以在线性时间O(1)内完成。
开放寻址法不需要额外的空间存储冲突元素,而是通过计算出存放的槽位提高检索速度,从而提高了空间的利用率。
α绝对不会超过1,因为当散列表被填满时,不能再插入任何元素,此时n和槽数m相等。
假设有n个元素,放到m个槽位中,则α=n/m<1。在简单均匀散列假设下:
1)一次不成功查找平均时间为1/(1-α);
2)一次成功查找平均时间为(1/α)㏑(1/(1-α));
1、线性探查
h(k, i) = (h'(k) + i) % m
1)i = 0, 1, 2, ... m-1
2)共有m中不同的探查序列
3)会造成一次群集(重度),连续槽被占用(查找是顺序自增),相应的查找时间也会相应增加。
2、二次探查
h(k, i) = (h'(k) + c1i+c2i²) % m
1)i = 0, 1, 2, ... m-1
2)共有m中不同的探查序列
3)会造成二次群集(轻度),当初始位置相同时,探查序列也就相同了。
3、双重散列
h(k, i) = (h1(k) + ih2(k)) % m
1)开放寻址的最好方法之一,所产生的探查序列具有随机性。
2)为了可以探查整个散列表,要求h2(k)同m必须互素(公约数为1)
a、m是2的幂次,h2(k)总返回基数
b、m是素数,h2(k)总返回比m小的正整数
能够在O(1)最坏情况时间内完成关键字的查找。要求关键字集合是静态的,各个关键字一旦进入散列表,就不会再变化了。如:cd-rom的文件名,程序设计语言的关键字。
HashSet
1、基于Hash表,实际上是基于HashMap实例。每一个元素对应HashMap的key。
2、无序(基于hash表),元素不可重复。
3、对于基本操作,如add、remove、contains、size提供了时间复杂度是O(1)的性能。对于iterate,同Hash表的容量相关,因此为保证性能,初始容量不要太大或负载因子不要太小。
HashMap
1、与Hashtable大致一致,除了它是不同步、key和value接受null值。
2、无序,特别是不能保证一直保持不变。
3、对于基本操作,如get、put提供了时间复杂度是O(1)的性能。对于iterate,同Hash表的容量相关,因此为保证性能,初始容量不要太大或负载因子不要太小。
4、有两个参数影响其性能
1)初始容量,Hash表的槽数。
2)负载因子,一个度量散列表在自动增加其容量之前被允许达到的容量占比的程度。当哈希表的长度超过了负载因子和当前的容量的乘积,哈希表会重建(重新散列),最后的容量是原来容量的两倍。
5、默认情况容量是16,负载因子是0.75。
6、HashMap使用链接法解决冲突(相同hash值),当链表长度大于等于TREEIFY_THRESHOLD(默认为8)时,将链表转换为红黑树,当小于等于UNTREEIFY_THRESHOLD(默认为6)时,又会将红黑树转换为链表以达到最佳性能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。