1、哈希表的引入:
前言:
顺序结构 以及 平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素时,必须要经过关键码的多次比较 。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( log2 N),搜索的效率 取决于搜索过程中元素的 比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的 存储位置 与它的 关键码 之间能够建立一 一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
- 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放 - 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为 哈希(散列)方法 ,哈希方法中使用的 转换函数 称为 哈希(散列)函数,构造出来的结构 称为 哈希表(HashTable)
- 哈希表 : 数组(存储元素) + 哈希函数(根据元素,得到数组中的一个合法下标)
(哈希函数的计算:一般是 该元素 % 数组长度)
举个例子:
数据集合: {1,7,6,4,5,9}
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
2、哈希冲突
(1)概念:
哈希冲突:不同关键字通过相同哈希函数计算出相同的哈希地址
例如上面的那个例子中:
如果集合中多添加了一个元素:14
此时,元素 4 和 14 所计算出来哈希地址是相同的,那数组中该怎么保存这两个元素,这就是哈希冲突
(哈希表底层数组的容量往往是小于实际要存储的关键字的数量的)
哈希冲突的发生是必然的 - 衣柜的容量是有限的,而衣服是无限的,总会出现哈希冲突的
但是我们可以尽量的 降低冲突率
- 1、重新设计哈希函数
- 2、理论上,数组长度可以使用素数(数组长度是 2 的 n 次方)
- 3、设置一个负载因子的阈值,达到阈值时,通过扩容,来降低冲突率
(2)避免哈希冲突负载因子调节
(3)哈希冲突的解决方法:(重重重点)
1、探测法(开放寻址法)
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的“下一个” 空位置中去。
那如何寻找下一个空位置呢?
以这个为例,此时 4 和 14位置冲突
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
- 通过哈希函数获取待插入元素在哈希表中的位置
- 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
- 采用这种方式处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,14查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
2、拉链法(哈希桶)(Java 的 HashMap选用这种)(重点学习!!)
使用另一种数据结构存储冲突元素 (单链表 -> 红黑树)
(具有相同地址的元素归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。)
从上图可以看出,每个桶中放的都是发生哈希冲突的元素。
如果数组中 存储的元素不是 int 类型 怎么办?(Person 对象)
(所有的 Object 类,都有一个方法 int hashCode())
- 1、 通过该方法,将一个对象变为 int 类型(int hashValue = key.hashCode())
- 2、把 int 类型转成一个合法下标(int index = hashValue % array.length)
代码实现拉链法:(重点掌握 插入元素的过程!!!)
public class MyHashTable {
//1、数组:
private Node[] array = new Node[11];
//2、维护哈希表中的元素个数
private int size;
//true:key 之前不在哈希表中
//false:key 之前已经在哈希表
public boolean insert(Integer key) {
//1、把对象转成 int 类型
int hashValue = key.hashCode();
//2、把 hashValue 转成合法下标
int index = hashValue % array.length;
//3、遍历 index 位置的链表,确定 key 在不在元素中
Node cur = array[index];
while (cur != null) {
if (key.equals(cur.key)) {
return false;
}
cur = cur.next;
}
//4、把 key 装入结点中,对插入到对应的链表中(JDK 1.8 改成尾插了)
Node node = new Node(key);
node.next = array[index];
array[index] = node;
//5、维护元素个数
size++;
return true;
}
public boolean remove(Integer key) {
int hashValue = key.hashCode();
int index = hashValue % array.length;
Node cur = array[index];//要删除的当前结点
//删除第一个结点
if (key.equals(cur.key)) {
cur.next = null;
return true;
}
Node prev = null;//要删除的结点的前一个结点
while (cur != null) {
if (key.equals(cur.key)) {
if (cur.next != null) {
//删除中间结点
prev.next = cur.next;
} else {
//删除最后一个结点
prev.next = null;
}
size--;
return true;
}
prev = cur;
cur = cur.next;
}
//没找到要删除的元素
return false;
}
public boolean contains(Integer key) {
//找到对应的下标
int hashValue = key.hashCode();
int index = hashValue % array.length;
Node cur = array[index];
while (cur != null) {
if (key.equals(cur.key)) {
return true;
}
cur = cur.next;
}
return false;
}
}
HashMap 和 HashSet 中有关对象时,都需要覆写 equels 和 hashCode 方法
如果不重写 hashCode ,就不能保证两个 index 是相同的(默认的 hashCode 十有八九是不相同的)
如果两个对象想通过 hashMap 找到,要意味两者的 hashCode 必须是相等的
3、Java 类集的关系:
- 1、HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set 接口
- 2、 java 中使用的是哈希桶方式解决冲突的
- 3、java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
- 4、java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的
转载:https://blog.csdn.net/qq_45658339/article/details/115828883