哈希表
1.哈希表也叫散列表,是一种数据结构。
2.哈希表本质上是数组。通过哈希函数将键映射到数组的特定位置,以便快速访问和查找键对应的值。
3.实现哈希表采用的两种方法:①数组+链表;②数组+二叉树。
4.哈希表提供了 O(1) 时间复杂度的查找操作。
1.哈希表用来快速判断一个元素是否出现集合里。
- 存储时采用键值对存储方式 ,Key-Value,其中要求Key不重复
- 先计算Key的哈希值(通常为int),将哈希值与数组(设置初始值是16length)的长度范围进行下标映射,将哈希值映射到大小为16(2的整数倍)的表中。
- 获取一个下标: int index = hash & len;
- 直接到数组 index位置取出数据(节点数据(K-V-Next))
2.解决哈希冲突的方法
(1)开放寻址法
假设当hash值为3冲突时(假设此时hash表长度为15)。
①线性探测法:顺着表查找,直到找到一个空单元或查遍全表。
H1 = (3+1)%15 = 4,此时若4依旧冲突,则往下一个查找
H2 = (3+2)%15 = ...
②二次探查法:当哈希冲突时,在表的左右进行跳跃探测。1^2,-1^2,2^2,-2^2...
H1 = (3+1^2)%15 = 4,此时若4依旧冲突,则再hash,即
H2 = (3+(-1)^2)%15 = 2 …
③伪随机探测法:产生一些随机系列值,并给定随机数作为起点
假设产生的随机系列为2,5,9 …,则
H1 = (3+2)%15 = 5
H2 = (3+5)%15 = 8...
(2)拉链法
(3)再哈希法
对原始哈希函数重新计算哈希值,然后将冲突的元素插入到重新计算的哈希值对应的位置。
再哈希法的函数表达式可以表示为:newValue = (hash_value + f(key)) % table_size
newHvalue 是重新计算后的哈希值,hash_value 是原始哈希值,f(key) 是一个用于计算新的偏移量的函数,table_size 是哈希表的大小。
(4)公共溢出区法
设立两个表:基础表和溢出表。将所有关键字通过哈希函数计算出相应的地址。然后将未发生冲突的关键字放入相应的基础表中,将具有冲突的元素存储在溢出表中。(通常是一个链表或者其他数据结构)
在查找时,先用给定值通过哈希函数计算出相应的散列地址,与基本表的相应位置进行比较,如果不相等,再到溢出表中顺序查找。
3.开放定址法与拉链法的比较:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②拉链法中各链表上的结点空间是动态申请的,更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α(装填因子 = 元素数量 / 表的大小)较小,当结点规模较大时会浪费很多空间。
而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
4.存在问题:
5.数组+链表的哈希表实现方式:
/**
* 哈希表:
* 节点类:Node类
* 属性:
* 存储结构-节点数组
* 元素个数
* 存储结构数组被占用的格子数量
* 默认初始长度 16
* 扩容阈值 0.75
* 数组长度
* 方法:
* put: 存储
* resize(); 扩容
* get: 获取
*
* @param <K>
* @param <V>
*/
class Node<K, V> {
K key;
V value;
Node<K, V> next;
int hash;// K的hash值
public Node(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
public class MyHashMap<K,V> {
private Node<K, V>[] table;
private int modCount;// 数组被占用的格子数量
private int elmSize;// 元素个数
private int capacity;// 数组的容量
private static final int DEFAULT_CAPACITY = 16;
private static final double LOAD_FACTOR = 0.75;
/**
* 构造方法 根据传进来的容量进行初始化哈希表
*
* @param initCapacity
*/
public MyHashMap(int initCapacity) {
if (initCapacity < DEFAULT_CAPACITY) {
initCapacity = DEFAULT_CAPACITY;
}
table = new Node[initCapacity];
capacity = initCapacity;
elmSize = 0;
modCount = 0;
}
public V get(K key) {
int h = key.hashCode();
int index = h & (capacity - 1);
Node<K, V> first = table[index];
// 判断 目标位置的节点是否为null
if (first != null) {
// 判断 first 是否是咱们需要的节点
// 1: 哈希值一致 才需要比较后面的
// 2: 接着比较key的引用地址 如果地址一致 就不需要比较内容
// 3: 地址不一致的情况会存在内容一致的情况 使用equals比较
if (first.hash == h && first.key == key || first.key.equals(key)) {
return first.value;
}
// 第一个节点比较不成功 从这个节点开始作为头节点遍历链表
Node temp = first;// 此处与java底层实现有区别:判断 first 是链表节点(写循环进行遍历)还是红黑树节点(写方法去遍历红黑树)
while (temp.next != null) {
temp = temp.next;
if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
return (V) temp.value;
}
}
}
return null;
}
/**
* 设置元素 用键值对进行存放
*
* @param key
* @param value
*/
public void put(K key, V value) {
// System.out.println("Put");
int h = key.hashCode();// 计算Key 的hash
Node<K, V> elmNode = new Node<>(key, value, h);// 新节点
int index = h & (capacity - 1);// 根据hash值 与 数组长度 得到下标
// System.out.println(index);
Node<K, V> first = table[index];
if (first == null) {
table[index] = elmNode;
modCount++;
elmSize++;
} else {
Node<K, V> oldNode = null;
// 如果first 与 新节点的key一致 更新 first节点的v
if (first.hash == h && first.key == key || first.key.equals(key)) {
first.value = value;
oldNode = new Node<>(key, first.value, h);
} else {
Node<K, V> temp = first;
while (temp.next != null) {
temp = temp.next;
if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
temp.value = value;
oldNode = new Node<>(key, temp.value, h);
break;
}
}
if (oldNode == null) {// 新增节点
temp.next = elmNode;
elmSize++;
}
}
}
// 扩容: 数组的被占用格子数 比例大于数组容量的百分之75的时候扩容
if (modCount >= capacity * LOAD_FACTOR) {
resize();
}
}
/**
* 扩容:
* 扩容 2倍扩容
* 创建一个更大的数组 将原本的所有元素存储进去
* (每个元素取出 重新映射位置 因为放置进去时是根据 int index = h & (capacity - 1); capacity放置的,扩容后capacity发生改变)
* 每个元素存储的位置与当前数组的长度capacity相关
* 写函数时,如果出现问题,自己重新写一遍
*/
private void resize() {
System.out.println("扩容进入:" + capacity);
modCount = 0;
elmSize = 0;
int oldCapacity = capacity;
int newCapacity = oldCapacity + oldCapacity; //加法比乘法快
Node<K, V>[] oldTable = table;
Node<K, V>[] newTable = new Node[newCapacity];
//把表中的数据取出来一个一个放到新的表中去
for (int i = 0; i < oldCapacity; i++) {
// System.out.println("扩容循环");
// 取出旧节点
Node<K, V> node = oldTable[i];
//此处与对比处的bug进行对比
//取出旧表中的一个node,看这个node的头是否为空。如果头为空,设置该节点放入元素;如果头不为空,循环遍历到链表尾部,尾部下一个节点设置为该节点放入元素
//如果oldTable中node为空,则不考虑
while (node != null) {
Node<K, V> newNode = new Node<>(node.key, node.value, node.hash);
System.out.println("遍历旧数组中的链表");
int h = node.hash;
System.out.println("旧数组中元素重新进行映射");
int index = h & (newCapacity - 1);
Node<K, V> first = newTable[index];
if (first == null) {
newTable[index] = newNode;
elmSize++;
modCount++;
} else {
System.out.println("新数组目标位置是一条链表");
Node<K, V> temp = first;
while (temp.next != null) {
// System.out.println("temp.next !=null");
// System.out.println(temp.value);
temp = temp.next;
}
temp.next = newNode;
elmSize++;
}
node = node.next;
}
}
//更新全局的 哈希表与容量
table = newTable;
capacity = newCapacity;
System.out.println("扩容:" + capacity);
}
/**
* 测试存储以及输出
* @param args
*/
public static void main(String[] args) {
MyHashMap<String, Integer> map = new MyHashMap<>(16);
for (int i = 0; i < 100; i++) {
map.put("Hello" + i, i); //存进去的字符Hello是哈希值就比较大了
}
System.out.println("数组长度:" + map.capacity);
System.out.println("数组被占用格子数量:" + map.modCount);
System.out.println("元素个数:" + map.elmSize);
}
}