一、哈希表概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。搜索的效率取决于搜索过 程中元素的比较次数。
如果构造一种存储结构,通过某种函 数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快 找到该元素。
当向该结构插入、搜索元素,此方法即为哈希(散列)方法。
哈希方法中使用的转换函数称为哈希 (散列) 函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表) 。
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 。
二、哈希冲突
若不同关键字通过相同哈希数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
例:
在上图中,若数据中还含有 66,则 Hash(66) = 66 % 10 = 6;
与数据中的 6 发生哈希冲突。
由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的。这就导致一个问题,冲突的发生是必然的,因此我们能做的应该是尽量的降低冲突率。
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 到 m - 1 之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。
三、避免哈希冲突
-
开放地址法(闭散列):当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的“下一个” 空位置中去。
寻找空位置常用方法:
a. 线性探测法:
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素 6,如果直接删除掉,66 查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
如图:若需插入数据 66,下标也为 6,则与 6 发生哈希冲突。
通过线性探测法,发现下标为 9 的值为 空。因此将 66 插入下标为 9 的位置。
b. 二次探测:
线性探测的缺陷是产生冲突的数据堆积在一块,这与顺序找下一个空位置有关系,二次探测避免了该问题。
总结:开放地址法最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。 -
链地址法(开散列):
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
总结:开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
public class Hash {
class Node {
public int key;
public int value;
public Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node[] array = new Node[101];
// 101 的空间不容易发生 下标重合
private int size = 0;
// 负载因子:size / array.length
private int hashFunc(int key) {
return key / array.length;
}
public void put(int key, int value) {
// 1. 根据 key 代入到 hash 函数中, 计算得到下标
int index = hashFunc(key);
// 2. 根据下标得到对应的链表
Node head = array[index];
// 3. 先判定 key 是否存在. 如果存在就修改 value(不插入新节点).
for (Node cur = head; cur != null; cur = cur.next) {
if (cur.key == key) {
cur.value = value;
return;
}
}
// 4. 如果不存在再进行插入, 链表头插比较简单一些.
Node newNode = new Node(key, value);
newNode.next = head;
array[index] = newNode;
size++;
}
public Integer get(int key) {
// 1. 根据 key 得到 hash 值
int index = hashFunc(key);
// 2. 在对应的链表上查找指定的 key 对应的节点
Node head = array[index];
for (Node cur = head; cur != null; cur = cur.next) {
if (cur.key == key) {
return cur.value;
}
}
// 3. 如果没找到
return null;
}
}