在实际项目的开发,经常遇到字典的需求。所谓字典,就是把一堆key-value的键值对存入容器中,用key作为条件能快速查出value。几乎每一种编程语言都实现了字典,但是C语言标准库没有字典的实现。按照上述我对字典的定义,任何数据结构都可以实现字典。就拿数组来说,数组每个位置放上键值对,那也是个字典啊,只是性能会很差。字典的高性能实现有多种方案,常见的方案有哈希表、跳表和红黑树实现。
哈希表的常见的实现方案是这样的:
底层是一个数组,数组的每一项都是一个链表,哈希值相同的在一个链表上。在扩容时,需要把底层数组扩大,然后每一个元素计算在新数组的的哈希值,然后再拷贝过去。如图所示:
扩容后就是这样的:
哈希表并不复杂,但是一定要动手练一练,亲自写哈希表的实现。以下是我自己的实现代码:
package com.yongthing.map.hash;
import java.util.LinkedList;
import java.util.ListIterator;
/**
* 哈希表
* created at 22/01/2022
*
* @author 花书粉丝
* <a href="mailto://yujianbo@chtwm.com">yujianbo@chtwm.com</a>
* @since 1.0.0
*/
public class HashMap<K, V> {
static class Entry<K, V> {
K key;
V value;
}
private LinkedList[] elements;
private int size;
public HashMap() {
elements = new LinkedList[8];
}
/**
* 添加元素方法
*
* @param key
* @param value
*/
public void put(K key, V value) {
if (key == null) {
throw new IllegalArgumentException("key不能为空");
}
final Entry<K, V> entry = new Entry<>();
entry.key = key;
entry.value = value;
// 扩容
if (size > elements.length * 0.75) {
//elements = makeCapacity(elements);
}
add(entry, elements);
size++;
}
public V get(K key) {
if (key == null) {
return null;
}
final int index = hash(key, this.elements);
final LinkedList element = elements[index];
if (element == null) {
return null;
}
for (Object e : element) {
final Entry<K, V> entry = (Entry<K, V>) e;
if (entry.key.equals(key)) {
return entry.value;
}
}
return null;
}
private static <K, V> LinkedList[] makeCapacity(LinkedList[] elements) {
final LinkedList[] newElements = new LinkedList[(elements.length + 1) << 1];
// 将数据拷贝过来
for (LinkedList linkedList : elements) {
if (linkedList != null && !linkedList.isEmpty()) {
for (Object e : linkedList) {
add((Entry<K, V>) e, newElements);
}
}
}
return newElements;
}
private static <K, V> void add(Entry<K, V> entry, LinkedList[] elements) {
K key = entry.key;
final int index = hash(key, elements);
LinkedList list = elements[index];
if (list == null) {
list = new LinkedList<Entry<K, V>>();
list.add(entry);
elements[index] = list;
} else {
final ListIterator<Entry<K, V>> iterator = list.listIterator();
while (iterator.hasNext()) {
final Entry<K, V> next = iterator.next();
if (next.key.equals(key)) {
// 重复时替换
iterator.set(entry);
return;
}
}
// 不存在重复则添加
list.add(entry);
}
}
private static <K> int hash(K key, LinkedList[] elements) {
return key.hashCode() % elements.length;
}
}
java.util包下自带了一个HashMap,是面试的热门,有时候会问十几个关于HashMap的细节问题。基本原理肯定是不会问那么多,其实问题最多的就是两个问题:扩容与并发。
哈希表扩容
首先有个扩容因子0.75,这个扩容因子的意思是如果键值对数量已经占据哈希表大小的0.75,再加元素就会对哈希表进行扩容了。哈希表的扩容是按2的指数扩容的,根据哈希算法,扩容后的元素要么在原索引,要么再移动2的指数。
举个例子,哈希值为9,扩容前哈希表大小为8,那么索引为
9
&
(
8
−
1
)
=
1
9 \& (8-1)=1
9&(8−1)=1,扩容后就是
9
&
(
16
−
1
)
=
9
9\& (16-1)=9
9&(16−1)=9.而对于哈希值为17的,扩容前是
17
&
(
8
−
1
)
=
1
17 \& (8-1)=1
17&(8−1)=1,扩容后是
17
&
(
16
−
1
)
=
1
17 \& (16-1)=1
17&(16−1)=1,扩容前后在哈希表中的索引是不变的。
HashMap是放完元素后再进行扩容的。扩容机制比较复杂,我就不展开说了。