hash,哈希表这个词,以前接触过好多次了,对哈希表了解最多的就是,这玩意儿也是一种数据结构,而且哈希表查找、添加和删除元素都很快。
哈希表,跟链表一样,大都使用结点类型来存储键跟值。哈希表的存储类型是种叫 键值对
在本文中,我用的是一个数组 + 链表实现一个简单的哈希表。
我们得到键与值之后,用这个键得到这个值在数组中存储的下标(至于这个下标怎么得到,需要用到一种 hash 算法去实现这个值),就可以将值存储起来。
当想查找一个值时,我们只需通过 hash 算出下标,就可以在数组该下标下得到值。
hash()算法:
在JDK源码中,使用的是hash = (h = key.hashCode) ^ (h >>> 16),
index = hash & table.length - 1;
为什么需要链表捏?我们通过对 key 进行 hash() 得到的下标值 index,可能不同的 key 得到同样的下标,我们就不能轻易的在此处放置要添加的新值。这个时候,我们可以像链表一样把新值链在旧值后面(但是要进行判断,判断条件与结果在 put() 种)。
这也就是所谓的哈希冲突,我们通过对 key 进行 hash() 得到了下标值,这个下标却被别人占了(暂时简单的这样理解),我们要做其它处理呢对不对。
如此,就建立了一个简单的哈希表。
哈希表综合了数组和链表的优点:key ➡ index ➡ 操作值
查找、添加、删除都几乎可以一次找到位置,
只是发生哈希冲突的情况下要在链表中遍历。
一个好的哈希算法的冲突贼低,所以查找、添加和删除元素的时间复杂度都是O(1)。
逻辑:
首先,明白这个结构。
是在一个数组里放值,那么通过 hash() 算法 对 key 得到下标 index ,如果得到的下标所在的结点不为空,说明有相同的 key 或者不同的 key ,通过 hash 得到了相同的 index ,此时要对链表进行操作。如果为空,直接放。
如果不为空,在 HashMap 中不允许存在相同的 key 值,我们从到到尾遍历链表,如果欲添加的 key 等于某个结点的 key 值,用新值覆盖旧值(这里使用),否则链链表。
代码:
public void put(K key, V value){
int hash = hash(key);
int index = hash & table.length - 1;
Node<K, V> firstNode = table[index];
//当前 index 下没有节点
if (firstNode == null){
table[index] = new Node<>(hash, key, value);
size++;
} else {
//index 下已被某个结点占据
Node<K, V> curNode = firstNode;
//从该已被占据的节点处开始遍历
//while 循环
//如果当前结点有下一个结点并且当前结点的 key 不等于欲添加的 key,向后遍历
while (curNode.next != null && !curNode.key.equals(key)){
curNode = curNode.next;
}
//退出 while 循环条件
//是最后一个结点或者 key 相等
//条件1:不是最后一个结点,欲添加的 key 等于当前结点的 key 值
if (curNode.next != null){
curNode.value = value;
} else if(curNode.next == null && curNode.key.equals(key)){
//条件2:欲添加的 key 等于最后一个结点的 key 值
curNode.value = value;
} else {
//条件3:欲添加的 key 不等于最后一个结点的 key 值
curNode = new Node<>(hash, key, value);
size++;
}
}
}
对于put()方法里面某个 index 处的链表来说,如果链表太长,比如在JDK源码中,链表中结点超过8的话,就把当前链表转为红黑树。
由于不会,暂且不做讨论。
逻辑:
首先得到 index ,看当前结点为空吗,不为空就从当前结点开始尝试往后找,
匹配 hash 值和 key 值,如果匹配上就返回该结点的值,不匹配就代表没有的;为空,该节点就没有值。
为什么要同时匹配 hash 值和 key 值呢?匹配 key 值是其一,继续匹配 hash 值是为了保险,对于两个不同的对象来说,通过哈希算法有可能得到相同的 hash 值。
代码:
public V get(K key){
int hash = hash(key);
int index = hash & table.length - 1;
Node<K, V> firstNode = table[index];
//当前结点没东西
if (firstNode == null){
return null;
} else {
//有的话,遍历,找到相符结点
Node<K, V> curNode = firstNode;
while (curNode != null){
if (curNode.key.equals(key) && curNode.hash == hash){
return curNode.value;
}
curNode = curNode.next;
}
}
return null;
}
逻辑:
先搞到下标不用说。
得到下标的结点没有就返回也不用说。
如果有结点,开删。
看看是头结点嘛?是的话删,
不是的话,找到要删的继续删。
否则仍然没找到
代码:
public boolean remove(K key) {
int hash = hash(key);
int index = hash & table.length - 1;
Node<K, V> firstNode = table[index];
//第一个结点为空
if (firstNode == null) {
return false;
} else {
//删除第一个结点
if (firstNode.key.equals(key) && firstNode.hash == hash){
table[index] = firstNode.next;
return true;
} else {
//不是头节点
Node<K, V> curNode = firstNode;
while (curNode.next != null){
if (curNode.next.key.equals(key) && curNode.next.hash == hash){
curNode.next = curNode.next.next;
return true;
} else {
curNode = curNode.next;
}
}
//没返回 true ,指定没找到
return false;
}
}
}
class HashMap<K, V> {
static class Node<K, V> {
private int hash;
private K key;
private V value;
private Node<K, V> next;
public Node(int hash, K key, V value) {
this.hash = hash;
this.key = key;
this.value = value;
}
}
private Node<K, V>[] table;
private int size;
public HashMap(int capacity) {
table = new Node[capacity];
}
private int hash(K key) {
int h;
return (h = key.hashCode()) ^ (h >>> 16);
}
}
注:
完整的哈希表代码