自己动手实现java数据结构(五)哈希表
1.哈希表介绍
前面我们已经介绍了许多类型的数据结构。在想要查询容器内特定元素时,有序向量使得我们能使用二分查找法进行精确的查询((O(logN)对数复杂度,很高效)。
可人类总是不知满足,依然在寻求一种更高效的特定元素查询的数据结构,哈希表/散列表(hash table)就应运而生啦。哈希表在特定元素的插入,删除和查询时都能够达到O(1)常数的时间复杂度,十分高效。
1.1 哈希算法
哈希算法的定义:把任意长度的输入通过哈希算法转换映射为固定长度的输出,所得到的输出被称为哈希值(hashCode = hash(input))。哈希映射是一种多对一的关系,即多个不同的输入有可能对应着一个相同的哈希值输出;也意味着,哈希映射是不可逆,无法还原的。
举个例子:我们有一个好朋友叫熊大,大家都叫他老熊。可以理解为是一个hash算法:对于一个人名,我们一般称呼为"老" + 姓氏(单姓) (hash(熊大) = 老熊)。同时,我们还有一个好朋友叫熊二,我们也叫他老熊(hash(熊二) = 老熊)。当熊大和熊二两个好朋友同时和我们聚会时,都称呼他们为老熊就不太合适啦,因为这时出现了hash冲突。老熊这个称呼同时对应了多个人,多个不同的输入对应了相同的哈希值输出。
java在Object这一最高层对象中实现了hashCode方法,并允许子类重写更适应自身,冲突概率更低的hashCode方法。
1.2 哈希表实现的基本思路
哈希表存储的是key-value键值对结构的数据,其基础是一个数组。
由于采用hash算法会出现hash冲突,一个数组下标对应了多个元素。常见的解决hash冲突的方法有:开放地址法、重新哈希法、拉链法等等,我们的哈希表实现采用的是拉链法解决hash冲突。
采用拉链法的哈希表将内部数组的每一个元素视为一个插槽(slot)或者桶(bucket),并将数据存放在键值对节点(EntryNode)中。EntryNode除了存放key和value,还维护着一个next节点的引用。为了解决hash冲突,单个插槽内的多个EntryNode构成一个简单的单向链表,插槽指向链表的头部节点,新的数据将会插入当前链表的尾部。
key值不同但映射的hash值相同的元素在哈希表的同一个插槽中以链表的形式共存。
1.3 哈希表的负载因子(loadFactor):
哈希表在查询数据时通过直接计算数据hash值对应的插槽,迅速获取到key值对应的数据,进行非常高效的数据查询。
但依然存在一个问题:虽然设计良好的hash函数可以尽可能的降低hash冲突的概率,但hash冲突还是不可避免的。当发生频繁的哈希冲突时,对应的插槽内可能会存放较多的元素,导致插槽内的链表数据过多。而链表的查询效率是非常低的,在极端情况下,甚至会出现所有元素都映射存放在同一个插槽内,此时的哈希表退化成了一个链表,查询效率急剧降低。
一般的,哈希表存储的数据量一定时,内部数组的大小和数组插槽指向的链表长度成反比。换句话说,总数据量一定,内部数组的容量越大(插槽越多),平均下来桶链表的长度也就越小,查询效率越高。
同等数据量下,哈希表内部数组容量越大,查询效率越高,但同时空间占用也越高,这本质上是一个空间换时间的取舍。
哈希表允许用户在初始化时指定负载因子(loadFactor):负载因子代表着存储的总数据量和内部数组大小的比值。插入新数据时,判断哈希表当前的存储量和内部数组的比值是否超过了负载因子。当比值超过了负载因子时,哈希表认为内部过于拥挤,查询效率太低,会触发一次扩容的rehash操作。rehash会对内部数组扩容,将存储的元素重新进行hash映射,使得哈希表始终保持一个合适的查询效率。
通过指定自定义的负载因子,用户可以控制哈希表在空间和时间上取舍的程度,使哈希表能更有效地适应用户的使用场景。
指定的负载因子越大,哈希表越拥挤(负载高,紧凑),查询效率越低,空间效率越高。
指定的负载因子越小,哈希表越稀疏(负载小,松散),查询效率越高,空间效率越低。
2.哈希表ADT接口
和之前介绍的链表不同,我们在哈希表的ADT接口中暴露出了哈希表内部实现的EntryNode键值对节点。通过暴露出去的public方法,用户在使用哈希表时,可以获得内部的键值对节点,灵活的访问其中的key、value数据(但没有暴露setKey方法,不允许用户自己设置key值)。
public interface Map <K,V>{ /** * 存入键值对 * @param key key值 * @param value value * @return 被覆盖的的value值 */ V put(K key,V value); /** * 移除键值对 * @param key key值 * @return 被删除的value的值 */ V remove(K key); /** * 获取key对应的value值 * @param key key值 * @return 对应的value值 */ V get(K key); /** * 是否包含当前key值 * @param key key值 * @return true:包含 false:不包含 */ boolean containsKey(K key); /** * 是否包含当前value值 * @param value value值 * @return true:包含 false:不包含 */ boolean containsValue(V value); /** * 获得当前map存储的键值对数量 * @return 键值对数量 * */ int size(); /** * 当前map是否为空 * @return true:为空 false:不为空 */ boolean isEmpty(); /** * 清空当前map */ void clear(); /** * 获得迭代器 * @return 迭代器对象 */ Iterator<EntryNode<K,V>> iterator(); /** * 键值对节点 内部类 * */ class EntryNode<K,V>{ final K key; V value; EntryNode<K,V> next; EntryNode(K key, V value) { this.key = key; this.value = value; } boolean keyIsEquals(K key){ if(this.key == key){ return true; } if(key == null){ //:::如果走到这步,this.key不等于null,不匹配 return false; }else{ return key.equals(this.key); } } EntryNode<K, V> getNext() { return next; } void setNext(EntryNode<K, V> next) { this.next = next; } public K getKey() { return key; } public V getValue() { return value; } public void setValue(V value) { this.value = value; } @Override public String toString() { return key + "=" + value; } } }
3.哈希表实现细节
3.1 哈希表基本属性:
public class HashMap<K,V> implements Map<K,V>{ /** * 内部数组 * */ private EntryNode<K,V>[] elements; /** * 当前哈希表的大小 * */ private int size; /** * 负载因子 * */ private float loadFactor; /** * 默认的哈希表容量 * */ private final static int DEFAULT_CAPACITY = 16; /** * 扩容翻倍的基数 * */ private final static int REHASH_BASE = 2; /** * 默认的负载因子 * */ private final static float DEFAULT_LOAD_FACTOR = 0.75f; //========================================构造方法=================================================== /** * 默认构造方法 * */ @SuppressWarnings("unchecked") public HashMap() { this.size = 0; this.loadFactor = DEFAULT_LOAD_FACTOR; elements = new EntryNode[DEFAULT_CAPACITY]; } /** * 指定初始容量的构造方法 * @param capacity 指定的初始容量 * */ @SuppressWarnings("unchecked") public HashMap(int capacity) { this.size = 0; this.loadFactor = DEFAULT_LOAD_FACTOR; elements = new EntryNode[capacity]; } /** * 指定初始容量和负载因子的构造方法 * @param capacity 指定的初始容量 * @param loadFactor 指定的负载因子 * */ @SuppressWarnings("unchecked") public HashMap(int capacity,int loadFactor) { this.size = 0; this.loadFactor = loadFactor; elements = new EntryNode[capacity]; } }
3.2 通过hash值获取对应插槽下标:
获取hash的方法仅和数据自身有关,不受到哈希表存储数据量的影响。
因此getIndex方法的时间复杂度为O(1)。
/** * 通过key的hashCode获得对应的内部数组下标 * @param key 传入的键值key * @return 对应的内部数组下标 * */ private int getIndex(K key){ return getIndex(key,this.elements); } /** * 通过key的hashCode获得对应的内部数组插槽slot下标 * @param key 传入的键值key * @param elements 内部数组 * @return 对应的内部数组下标 * */ private int getIndex(K key,EntryNode<K,V>[] elements){ if(key == null){ //::: null 默认存储在第0个桶内 return 0; }else{ int hashCode = key.hashCode(); //:::通过 高位和低位的异或运算,获得最终的hash映射,减少碰撞的几率 int finalHashCode = hashCode ^ (hashCode >>> 16); return (elements.length-1) & finalHashCode; } }
3.3 链表查询方法:
当出现hash冲突时,会在对应插槽处生成一个单链表。我们需要提供一个方便的单链表查询方法,将增删改查接口的部分公用逻辑抽象出来,简化代码的复杂度。
值得注意的是:在判断Key值是否相等时使用的是EntryNode.keyIsEquals方法,内部最终是通过equals方法进行比较的。也就是说,判断key值是否相等和其它数据结构一样,依然是由equals方法决定的。hashCode方法的作用仅仅是使我们能够更快的定位到所映射的插槽处,加快查询效率。
思考一下,为什么要求在重写equals方法的同时,也应该重写hashCode方法?
/** * 获得目标节点的前一个节点 * @param currentNode 当前桶链表节点 * @param key 对应的key * @return 返回当前桶链表中"匹配key的目标节点"的"前一个节点" * 注意:当桶链表中不存在匹配节点时,返回桶链表的最后一个节点 * */ private EntryNode<K,V> getTargetPreviousEntryNode(EntryNode<K,V> currentNode,K key){ //:::不匹配 EntryNode<K,V> nextNode = currentNode.next; //:::遍历当前桶后面的所有节点 while(nextNode != null){ //:::如果下一个节点的key匹配 if(nextNode.keyIsEquals(key)){ return currentNode; }else{ //:::不断指向下一个节点 currentNode = nextNode; nextNode = nextNode.next; } } //:::到达了桶链表的末尾,返回最后一个节点 return currentNode; }
3.4 增删改查接口:
哈希表的增删改查接口都是通过hash值直接计算出对应的插槽下标(getIndex方法),然后遍历插槽内的桶链表进行进一步的精确查询(getTargetPreviousEntryNode方法)。在负载因子位于正常范围内时(一般小于1),桶链表的平均长度非常短,可以认为单个桶链表的遍历查询时间复杂度为(O(1))。
因此哈希表的增删改查接口的时间复杂度都是O(1)。
@Override public V put(K key, V value) { if(needReHash()){ reHash(); } //:::获得对应的内部数组下标 int index = getIndex(key); //:::获得对应桶内的第一个节点 EntryNode<K,V> firstEntryNode = this.elements[index]; //:::如果当前桶内不存在任何节点 if(firstEntryNode == null){ //:::创建一个新的节点 this.elements[index] = new EntryNode<>(key,value); //:::创建了新节点,size加1 this.size++; return null; } if(firstEntryNode.keyIsEquals(key)){ //:::当前第一个节点的key与之匹配 V oldValue = firstEntryNode.value; firstEntryNode.value = value; return oldValue; }else{ //:::不匹配 //:::获得匹配的目标节点的前一个节点 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); //:::获得匹配的目标节点 EntryNode<K,V> targetNode = targetPreviousNode.next; if(targetNode != null){ //:::更新value的值 V oldValue = targetNode.value; targetNode.value = value; return oldValue; }else{ //:::在桶链表的末尾 新增一个节点 targetPreviousNode.next = new EntryNode<>(key,value); //:::创建了新节点,size加1 this.size++; return null; } } } @Override public V remove(K key) { //:::获得对应的内部数组下标 int index = getIndex(key); //:::获得对应桶内的第一个节点 EntryNode<K,V> firstEntryNode = this.elements[index]; //:::如果当前桶内不存在任何节点 if(firstEntryNode == null){ return null; } if(firstEntryNode.keyIsEquals(key)){ //:::当前第一个节点的key与之匹配 //:::将桶链表的第一个节点指向后一个节点(兼容next为null的情况) this.elements[index] = firstEntryNode.next; //:::移除了一个节点 size减一 this.size--; //:::返回之前的value值 return firstEntryNode.value; }else{ //:::不匹配 //:::获得匹配的目标节点的前一个节点 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); //:::获得匹配的目标节点 EntryNode<K,V> targetNode = targetPreviousNode.next; if(targetNode != null){ //:::将"前一个节点的next" 指向 "目标节点的next" ---> 相当于将目标节点从桶链表移除 targetPreviousNode.next = targetNode.next; //:::移除了一个节点 size减一 this.size--; return targetNode.value; }else{ //:::如果目标节点为空,说明key并不存在于哈希表中 return null; } } } @Override public V get(K key) { //:::获得对应的内部数组下标 int index = getIndex(key); //:::获得对应桶内的第一个节点 EntryNode<K,V> firstEntryNode = this.elements[index]; //:::如果当前桶内不存在任何节点 if(firstEntryNode == null){ return null; } if(firstEntryNode.keyIsEquals(key)){ //:::当前第一个节点的key与之匹配 return firstEntryNode.value; }else{ //:::获得匹配的目标节点的前一个节点 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); //:::获得匹配的目标节点 EntryNode<K,V> targetNode = targetPreviousNode.next; if(targetNode != null){ return targetNode.value; }else{ //:::如果目标节点为空,说明key并不存在于哈希表中 return null; } } }
3.5 扩容rehash操作:
前面提到,当插入数据时发现哈希表过于拥挤,超过了负载因子指定的值时,会触发一次rehash扩容操作。
扩容时,我们的内部数组扩容了2倍,所以对于每一个插槽内的元素在rehash时存在两种可能:
1.依然映射到当前下标插槽处
2.映射到高位下标处(当前下标 + 扩容前内部数组长度大小)
注意观察0,4,8三个元素节点,在扩容前(对4取模)都位于下标0插槽;扩容后,数组容量翻倍(对8取模),存在两种情况,0,8两个元素哈希值依然映射在下标0插槽(低位插槽),而元素4则被映射到了下标4插槽(高位插槽)(当前下标(0) + 扩容前内部数组长度大小(4))。
通过遍历每个插槽,将内部元素按顺序进行rehash,得到扩容两倍后的哈希表(数据保留了之前的顺序,即先插入的节点依然位于桶链表靠前的位置)。
和向量扩容一样,虽然rehash操作的时间复杂度为O(n)。但是由于只在插入时偶尔的被触发,总体上看,rehash操作的时间复杂度为O(1)。
哈希表扩容前:
哈希表扩容后:
/** * 哈希表扩容 * */ @SuppressWarnings("unchecked") private void reHash(){ //:::扩容两倍 EntryNode<K,V>[] newElements = new EntryNode[this.elements.length * REHASH_BASE]; //:::遍历所有的插槽 for (int i=0; i<this.elements.length; i++) { //:::为单个插槽内的元素 rehash reHashSlot(i,newElements); } //:::内部数组 ---> 扩容之后的新数组 this.elements = newElements; } /** * 单个插槽内的数据进行rehash * */ private void reHashSlot(int index,EntryNode<K, V>[] newElements){ //:::获得当前插槽第一个元素 EntryNode<K, V> currentEntryNode = this.elements[index]; if(currentEntryNode == null){ //:::当前插槽为空,直接返回 return; } //:::低位桶链表 头部节点、尾部节点 EntryNode<K, V> lowListHead = null; EntryNode<K, V> lowListTail = null; //:::高位桶链表 头部节点、尾部节点 EntryNode<K, V> highListHead = null; EntryNode<K, V> highListTail = null; while(currentEntryNode != null){ //:::获得当前节点 在新数组中映射的插槽下标 int entryNodeIndex = getIndex(currentEntryNode.key,newElements); //:::是否和当前插槽下标相等 if(entryNodeIndex == index){ //:::和当前插槽下标相等 if(lowListHead == null){ //:::初始化低位链表 lowListHead = currentEntryNode; lowListTail = currentEntryNode; }else{ //:::在低位链表尾部拓展新的节点 lowListTail.next = currentEntryNode; lowListTail = lowListTail.next; } }else{ //:::和当前插槽下标不相等 if(highListHead == null){ //:::初始化高位链表 highListHead = currentEntryNode; highListTail = currentEntryNode; }else{ //:::在高位链表尾部拓展新的节点 highListTail.next = currentEntryNode; highListTail = highListTail.next; } } //:::指向当前插槽的下一个节点 currentEntryNode = currentEntryNode.next; } //:::新扩容elements(index)插槽 存放lowList newElements[index] = lowListHead; //:::lowList末尾截断 if(lowListTail != null){ lowListTail.next = null; } //:::新扩容elements(index + this.elements.length)插槽 存放highList newElements[index + this.elements.length] = highListHead; //:::highList末尾截断 if(highListTail != null){ highListTail.next = null; } } /** * 判断是否需要 扩容 * */ private boolean needReHash(){ return ((this.size / this.elements.length) > this.loadFactor); }
3.6 其它接口实现:
@Override public boolean containsKey(K key) { V value = get(key); return (value != null); } @Override public boolean containsValue(V value) { //:::遍历全部桶链表 for (EntryNode<K, V> element : this.elements) { //:::获得当前桶链表第一个节点 EntryNode<K, V> entryNode = element; //:::遍历当前桶链表 while (entryNode != null) { //:::如果value匹配 if (entryNode.value.equals(value)) { //:::返回true return true; } else { //:::不匹配,指向下一个节点 entryNode = entryNode.next; } } } //:::所有的节点都遍历了,没有匹配的value return false; } @Override public int size() { return this.size; } @Override public boolean isEmpty() { return (this.size == 0); } @Override public void clear() { //:::遍历内部数组,将所有桶链表全部清空 for(int i=0; i<this.elements.length; i++){ this.elements[i] = null; } //:::size设置为0 this.size = 0; } @Override public Iterator<EntryNode<K,V>> iterator() { return new Itr(); } @Override public String toString() { Iterator<EntryNode<K,V>> iterator = this.iterator(); //:::空容器 if(!iterator.hasNext()){ return "[]"; } //:::容器起始使用"[" StringBuilder s = new StringBuilder("["); //:::反复迭代 while(true){ //:::获得迭代的当前元素 EntryNode<K,V> data = iterator.next(); //:::判断当前元素是否是最后一个元素 if(!iterator.hasNext()){ //:::是最后一个元素,用"]"收尾 s.append(data).append("]"); //:::返回 拼接完毕的字符串 return s.toString(); }else{ //:::不是最后一个元素 //:::使用", "分割,拼接到后面 s.append(data).append(", "); } } }
4.哈希表迭代器
1. 由于哈希表中数据分布不是连续的,所以在迭代器的初始化过程中必须先跳转到第一个非空数据节点,以避免无效的迭代。
2. 当迭代器的下标到达当前插槽链表的末尾时,迭代器下标需要跳转到靠后插槽的第一个非空数据节点。
/** * 哈希表 迭代器实现 */ private class Itr implements Iterator<EntryNode<K,V>> { /** * 迭代器 当前节点 * */ private EntryNode<K,V> currentNode; /** * 迭代器 下一个节点 * */ private EntryNode<K,V> nextNode; /** * 迭代器 当前内部数组的下标 * */ private int currentIndex; /** * 默认构造方法 * */ private Itr(){ //:::如果当前哈希表为空,直接返回 if(HashMap.this.isEmpty()){ return; } //:::在构造方法中,将迭代器下标移动到第一个有效的节点上 //:::遍历内部数组,找到第一个不为空的数组插槽slot for(int i=0; i<HashMap.this.elements.length; i++){ //:::设置当前index this.currentIndex = i; EntryNode<K,V> firstEntryNode = HashMap.this.elements[i]; //:::找到了第一个不为空的插槽slot if(firstEntryNode != null){ //:::nextNode = 当前插槽第一个节点 this.nextNode = firstEntryNode; //:::构造方法立即结束 return; } } } @Override public boolean hasNext() { return (this.nextNode != null); } @Override public EntryNode<K,V> next() { this.currentNode = this.nextNode; //:::暂存需要返回的节点 EntryNode<K,V> needReturn = this.nextNode; //:::nextNode指向自己的next this.nextNode = this.nextNode.next; //:::判断当前nextNode是否为null if(this.nextNode == null){ //:::说明当前所在的桶链表已经遍历完毕 //:::寻找下一个非空的插槽 for(int i=this.currentIndex+1; i<HashMap.this.elements.length; i++){ //:::设置当前index this.currentIndex = i; EntryNode<K,V> firstEntryNode = HashMap.this.elements[i]; //:::找到了后续不为空的插槽slot if(firstEntryNode != null){ //:::nextNode = 当前插槽第一个节点 this.nextNode = firstEntryNode; //:::跳出循环 break; } } } return needReturn; } @Override public void remove() { if(this.currentNode == null){ throw new IteratorStateErrorException("迭代器状态异常: 可能在一次迭代中进行了多次remove操作"); } //:::获得需要被移除的节点的key K currentKey = this.currentNode.key; //:::将其从哈希表中移除 HashMap.this.remove(currentKey); //:::currentNode设置为null,防止反复调用remove方法 this.currentNode = null; } }
5.哈希表性能
5.1 空间效率:
哈希表的空间效率很大程度上取决于负载因子。通常,为了保证哈希表查询的高效性,负载因子都设置的比较小(小于1),因而可能会出现许多空的插槽,浪费空间。
总体而言,哈希表的空间效率低于向量和链表。
5.2 时间效率:
一般的,哈希表增删改查接口的时间复杂度都是O(1)。但是出现较多的hash冲突时,冲突范围内的key的增删改查效率较低,时间效率会有一定的波动。
总体而言,哈希表的时间效率高于向量和链表。
哈希表的时间效率很高,可天下没有免费的午餐,据统计,哈希表的空间利用率通常情况下还不到50%。
哈希表是一个使用空间来换取时间的数据结构,对查询性能有较高要求的场合,可以考虑使用哈希表。
6.哈希表总结
6.1 当前版本缺陷
至此,我们已经实现了一个基础的哈希表,但还存在许多明显缺陷:
1.当hash冲突比较频繁时,查询效率急剧降低。
jdk在1.8版本的哈希表实现(java.util.HashMap)中,对这一场景进行了优化。当内部桶链表的节点个数超过一定数量(默认为8)时,会将插槽中的桶链表转换成一个红黑树(查询效率为O(logN))。
2.不支持多线程
在多线程的环境,并发的访问一个哈希表会导致诸如:扩容时内部节点死循环、丢失插入数据等异常情况。
6.2 查询特定元素的方法
我们目前查询特定元素有几种不同的方法:
1.顺序查找
在无序向量或者链表中,查找一个特定元素是通过从头到尾遍历容器内元素的方式实现的,执行速度正比于数据量的大小,顺序查找的时间复杂度为O(n),效率较低。
2.二分查找
在有序向量以及后面要介绍的二叉搜索树中,由于容器内部的元素是有序的,因此可以通过二分查找比较的方式查询特定的元素,二分查找的时间复杂度为O(logN),效率较高。
3.哈希查找
在哈希表中,通过直接计算出数据hash值对应的插槽(slot)(时间复杂度O(1)),查找出对应的数据,哈希查找的时间复杂度为O(1),效率极高。
特定元素的查找方式和排序算法的关系
1.顺序查找对应冒泡排序、选择排序等,效率较低,时间复杂度(O(n²))。
2.二分查找对应快速排序、归并排序等,效率较高,时间复杂度(O(nLogn))。
3.哈希查找对应基排序,效率极高,时间复杂度(O(n))。
在大牛刘未鹏的博客中有更为详细的说明,http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick。
6.3 完整代码
哈希表ADT接口:
1 public interface Map <K,V>{ 2 /** 3 * 存入键值对 4 * @param key key值 5 * @param value value 6 * @return 被覆盖的的value值 7 */ 8 V put(K key,V value); 9 10 /** 11 * 移除键值对 12 * @param key key值 13 * @return 被删除的value的值 14 */ 15 V remove(K key); 16 17 /** 18 * 获取key对应的value值 19 * @param key key值 20 * @return 对应的value值 21 */ 22 V get(K key); 23 24 /** 25 * 是否包含当前key值 26 * @param key key值 27 * @return true:包含 false:不包含 28 */ 29 boolean containsKey(K key); 30 31 /** 32 * 是否包含当前value值 33 * @param value value值 34 * @return true:包含 false:不包含 35 */ 36 boolean containsValue(V value); 37 38 /** 39 * 获得当前map存储的键值对数量 40 * @return 键值对数量 41 * */ 42 int size(); 43 44 /** 45 * 当前map是否为空 46 * @return true:为空 false:不为空 47 */ 48 boolean isEmpty(); 49 50 /** 51 * 清空当前map 52 */ 53 void clear(); 54 55 /** 56 * 获得迭代器 57 * @return 迭代器对象 58 */ 59 Iterator<EntryNode<K,V>> iterator(); 60 61 /** 62 * 键值对节点 内部类 63 * */ 64 class EntryNode<K,V>{ 65 final K key; 66 V value; 67 EntryNode<K,V> next; 68 69 EntryNode(K key, V value) { 70 this.key = key; 71 this.value = value; 72 } 73 74 boolean keyIsEquals(K key){ 75 if(this.key == key){ 76 return true; 77 } 78 79 if(key == null){ 80 //:::如果走到这步,this.key不等于null,不匹配 81 return false; 82 }else{ 83 return key.equals(this.key); 84 } 85 } 86 87 EntryNode<K, V> getNext() { 88 return next; 89 } 90 91 void setNext(EntryNode<K, V> next) { 92 this.next = next; 93 } 94 95 public K getKey() { 96 return key; 97 } 98 99 public V getValue() { 100 return value; 101 } 102 103 public void setValue(V value) { 104 this.value = value; 105 } 106 107 @Override 108 public String toString() { 109 return key + "=" + value; 110 } 111 } 112 }
哈希表实现:
1 public class HashMap<K,V> implements Map<K,V>{ 2 3 //===========================================成员属性================================================ 4 /** 5 * 内部数组 6 * */ 7 private EntryNode<K,V>[] elements; 8 9 /** 10 * 当前哈希表的大小 11 * */ 12 private int size; 13 14 /** 15 * 负载因子 16 * */ 17 private float loadFactor; 18 19 /** 20 * 默认的哈希表容量 21 * */ 22 private final static int DEFAULT_CAPACITY = 16; 23 24 /** 25 * 扩容翻倍的基数 两倍 26 * */ 27 private final static int REHASH_BASE = 2; 28 29 /** 30 * 默认的负载因子 31 * */ 32 private final static float DEFAULT_LOAD_FACTOR = 0.75f; 33 34 //========================================构造方法=================================================== 35 /** 36 * 默认构造方法 37 * */ 38 @SuppressWarnings("unchecked") 39 public HashMap() { 40 this.size = 0; 41 this.loadFactor = DEFAULT_LOAD_FACTOR; 42 elements = new EntryNode[DEFAULT_CAPACITY]; 43 } 44 45 /** 46 * 指定初始容量的构造方法 47 * @param capacity 指定的初始容量 48 * */ 49 @SuppressWarnings("unchecked") 50 public HashMap(int capacity) { 51 this.size = 0; 52 this.loadFactor = DEFAULT_LOAD_FACTOR; 53 elements = new EntryNode[capacity]; 54 } 55 56 /** 57 * 指定初始容量和负载因子的构造方法 58 * @param capacity 指定的初始容量 59 * @param loadFactor 指定的负载因子 60 * */ 61 @SuppressWarnings("unchecked") 62 public HashMap(int capacity,int loadFactor) { 63 this.size = 0; 64 this.loadFactor = loadFactor; 65 elements = new EntryNode[capacity]; 66 } 67 68 //==========================================内部辅助方法============================================= 69 /** 70 * 通过key的hashCode获得对应的内部数组下标 71 * @param key 传入的键值key 72 * @return 对应的内部数组下标 73 * */ 74 private int getIndex(K key){ 75 return getIndex(key,this.elements); 76 } 77 78 /** 79 * 通过key的hashCode获得对应的内部数组插槽slot下标 80 * @param key 传入的键值key 81 * @param elements 内部数组 82 * @return 对应的内部数组下标 83 * */ 84 private int getIndex(K key,EntryNode<K,V>[] elements){ 85 if(key == null){ 86 //::: null 默认存储在第0个桶内 87 return 0; 88 }else{ 89 int hashCode = key.hashCode(); 90 91 //:::通过 高位和低位的异或运算,获得最终的hash映射,减少碰撞的几率 92 int finalHashCode = hashCode ^ (hashCode >>> 16); 93 return (elements.length-1) & finalHashCode; 94 } 95 } 96 97 /** 98 * 获得目标节点的前一个节点 99 * @param currentNode 当前桶链表节点 100 * @param key 对应的key 101 * @return 返回当前桶链表中"匹配key的目标节点"的"前一个节点" 102 * 注意:当桶链表中不存在匹配节点时,返回桶链表的最后一个节点 103 * */ 104 private EntryNode<K,V> getTargetPreviousEntryNode(EntryNode<K,V> currentNode,K key){ 105 //:::不匹配 106 EntryNode<K,V> nextNode = currentNode.next; 107 //:::遍历当前桶后面的所有节点 108 while(nextNode != null){ 109 //:::如果下一个节点的key匹配 110 if(nextNode.keyIsEquals(key)){ 111 return currentNode; 112 }else{ 113 //:::不断指向下一个节点 114 currentNode = nextNode; 115 nextNode = nextNode.next; 116 } 117 } 118 119 //:::到达了桶链表的末尾,返回最后一个节点 120 return currentNode; 121 } 122 123 /** 124 * 哈希表扩容 125 * */ 126 @SuppressWarnings("unchecked") 127 private void reHash(){ 128 //:::扩容两倍 129 EntryNode<K,V>[] newElements = new EntryNode[this.elements.length * REHASH_BASE]; 130 131 //:::遍历所有的插槽 132 for (int i=0; i<this.elements.length; i++) { 133 //:::为单个插槽内的元素 rehash 134 reHashSlot(i,newElements); 135 } 136 137 //:::内部数组 ---> 扩容之后的新数组 138 this.elements = newElements; 139 } 140 141 /** 142 * 单个插槽内的数据进行rehash 143 * */ 144 private void reHashSlot(int index,EntryNode<K, V>[] newElements){ 145 //:::获得当前插槽第一个元素 146 EntryNode<K, V> currentEntryNode = this.elements[index]; 147 if(currentEntryNode == null){ 148 //:::当前插槽为空,直接返回 149 return; 150 } 151 152 //:::低位桶链表 头部节点、尾部节点 153 EntryNode<K, V> lowListHead = null; 154 EntryNode<K, V> lowListTail = null; 155 //:::高位桶链表 头部节点、尾部节点 156 EntryNode<K, V> highListHead = null; 157 EntryNode<K, V> highListTail = null; 158 159 while(currentEntryNode != null){ 160 //:::获得当前节点 在新数组中映射的插槽下标 161 int entryNodeIndex = getIndex(currentEntryNode.key,newElements); 162 //:::是否和当前插槽下标相等 163 if(entryNodeIndex == index){ 164 //:::和当前插槽下标相等 165 if(lowListHead == null){ 166 //:::初始化低位链表 167 lowListHead = currentEntryNode; 168 lowListTail = currentEntryNode; 169 }else{ 170 //:::在低位链表尾部拓展新的节点 171 lowListTail.next = currentEntryNode; 172 lowListTail = lowListTail.next; 173 } 174 }else{ 175 //:::和当前插槽下标不相等 176 if(highListHead == null){ 177 //:::初始化高位链表 178 highListHead = currentEntryNode; 179 highListTail = currentEntryNode; 180 }else{ 181 //:::在高位链表尾部拓展新的节点 182 highListTail.next = currentEntryNode; 183 highListTail = highListTail.next; 184 } 185 } 186 //:::指向当前插槽的下一个节点 187 currentEntryNode = currentEntryNode.next; 188 } 189 190 //:::新扩容elements(index)插槽 存放lowList 191 newElements[index] = lowListHead; 192 //:::lowList末尾截断 193 if(lowListTail != null){ 194 lowListTail.next = null; 195 } 196 197 //:::新扩容elements(index + this.elements.length)插槽 存放highList 198 newElements[index + this.elements.length] = highListHead; 199 //:::highList末尾截断 200 if(highListTail != null){ 201 highListTail.next = null; 202 } 203 } 204 205 /** 206 * 判断是否需要 扩容 207 * */ 208 private boolean needReHash(){ 209 return ((this.size / this.elements.length) > this.loadFactor); 210 } 211 212 //============================================外部接口================================================ 213 214 @Override 215 public V put(K key, V value) { 216 if(needReHash()){ 217 reHash(); 218 } 219 220 //:::获得对应的内部数组下标 221 int index = getIndex(key); 222 //:::获得对应桶内的第一个节点 223 EntryNode<K,V> firstEntryNode = this.elements[index]; 224 225 //:::如果当前桶内不存在任何节点 226 if(firstEntryNode == null){ 227 //:::创建一个新的节点 228 this.elements[index] = new EntryNode<>(key,value); 229 //:::创建了新节点,size加1 230 this.size++; 231 return null; 232 } 233 234 if(firstEntryNode.keyIsEquals(key)){ 235 //:::当前第一个节点的key与之匹配 236 V oldValue = firstEntryNode.value; 237 firstEntryNode.value = value; 238 return oldValue; 239 }else{ 240 //:::不匹配 241 242 //:::获得匹配的目标节点的前一个节点 243 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); 244 //:::获得匹配的目标节点 245 EntryNode<K,V> targetNode = targetPreviousNode.next; 246 if(targetNode != null){ 247 //:::更新value的值 248 V oldValue = targetNode.value; 249 targetNode.value = value; 250 return oldValue; 251 }else{ 252 //:::在桶链表的末尾 新增一个节点 253 targetPreviousNode.next = new EntryNode<>(key,value); 254 //:::创建了新节点,size加1 255 this.size++; 256 return null; 257 } 258 } 259 } 260 261 @Override 262 public V remove(K key) { 263 //:::获得对应的内部数组下标 264 int index = getIndex(key); 265 //:::获得对应桶内的第一个节点 266 EntryNode<K,V> firstEntryNode = this.elements[index]; 267 268 //:::如果当前桶内不存在任何节点 269 if(firstEntryNode == null){ 270 return null; 271 } 272 273 if(firstEntryNode.keyIsEquals(key)){ 274 //:::当前第一个节点的key与之匹配 275 276 //:::将桶链表的第一个节点指向后一个节点(兼容next为null的情况) 277 this.elements[index] = firstEntryNode.next; 278 //:::移除了一个节点 size减一 279 this.size--; 280 //:::返回之前的value值 281 return firstEntryNode.value; 282 }else{ 283 //:::不匹配 284 285 //:::获得匹配的目标节点的前一个节点 286 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); 287 //:::获得匹配的目标节点 288 EntryNode<K,V> targetNode = targetPreviousNode.next; 289 290 if(targetNode != null){ 291 //:::将"前一个节点的next" 指向 "目标节点的next" ---> 相当于将目标节点从桶链表移除 292 targetPreviousNode.next = targetNode.next; 293 //:::移除了一个节点 size减一 294 this.size--; 295 return targetNode.value; 296 }else{ 297 //:::如果目标节点为空,说明key并不存在于哈希表中 298 return null; 299 } 300 } 301 } 302 303 @Override 304 public V get(K key) { 305 //:::获得对应的内部数组下标 306 int index = getIndex(key); 307 //:::获得对应桶内的第一个节点 308 EntryNode<K,V> firstEntryNode = this.elements[index]; 309 310 //:::如果当前桶内不存在任何节点 311 if(firstEntryNode == null){ 312 return null; 313 } 314 315 if(firstEntryNode.keyIsEquals(key)){ 316 //:::当前第一个节点的key与之匹配 317 return firstEntryNode.value; 318 }else{ 319 //:::获得匹配的目标节点的前一个节点 320 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); 321 //:::获得匹配的目标节点 322 EntryNode<K,V> targetNode = targetPreviousNode.next; 323 324 if(targetNode != null){ 325 return targetNode.value; 326 }else{ 327 //:::如果目标节点为空,说明key并不存在于哈希表中 328 return null; 329 } 330 } 331 } 332 333 @Override 334 public boolean containsKey(K key) { 335 V value = get(key); 336 return (value != null); 337 } 338 339 @Override 340 public boolean containsValue(V value) { 341 //:::遍历全部桶链表 342 for (EntryNode<K, V> element : this.elements) { 343 //:::获得当前桶链表第一个节点 344 EntryNode<K, V> entryNode = element; 345 346 //:::遍历当前桶链表 347 while (entryNode != null) { 348 //:::如果value匹配 349 if (entryNode.value.equals(value)) { 350 //:::返回true 351 return true; 352 } else { 353 //:::不匹配,指向下一个节点 354 entryNode = entryNode.next; 355 } 356 } 357 } 358 359 //:::所有的节点都遍历了,没有匹配的value 360 return false; 361 } 362 363 @Override 364 public int size() { 365 return this.size; 366 } 367 368 @Override 369 public boolean isEmpty() { 370 return (this.size == 0); 371 } 372 373 @Override 374 public void clear() { 375 //:::遍历内部数组,将所有桶链表全部清空 376 for(int i=0; i<this.elements.length; i++){ 377 this.elements[i] = null; 378 } 379 380 //:::size设置为0 381 this.size = 0; 382 } 383 384 @Override 385 public Iterator<EntryNode<K,V>> iterator() { 386 return new Itr(); 387 } 388 389 @Override 390 public String toString() { 391 Iterator<EntryNode<K,V>> iterator = this.iterator(); 392 393 //:::空容器 394 if(!iterator.hasNext()){ 395 return "[]"; 396 } 397 398 //:::容器起始使用"[" 399 StringBuilder s = new StringBuilder("["); 400 401 //:::反复迭代 402 while(true){ 403 //:::获得迭代的当前元素 404 EntryNode<K,V> data = iterator.next(); 405 406 //:::判断当前元素是否是最后一个元素 407 if(!iterator.hasNext()){ 408 //:::是最后一个元素,用"]"收尾 409 s.append(data).append("]"); 410 //:::返回 拼接完毕的字符串 411 return s.toString(); 412 }else{ 413 //:::不是最后一个元素 414 //:::使用", "分割,拼接到后面 415 s.append(data).append(", "); 416 } 417 } 418 } 419 420 /** 421 * 哈希表 迭代器实现 422 */ 423 private class Itr implements Iterator<EntryNode<K,V>> { 424 /** 425 * 迭代器 当前节点 426 * */ 427 private EntryNode<K,V> currentNode; 428 429 /** 430 * 迭代器 下一个节点 431 * */ 432 private EntryNode<K,V> nextNode; 433 434 /** 435 * 迭代器 当前内部数组的下标 436 * */ 437 private int currentIndex; 438 439 /** 440 * 默认构造方法 441 * */ 442 private Itr(){ 443 //:::如果当前哈希表为空,直接返回 444 if(HashMap.this.isEmpty()){ 445 return; 446 } 447 //:::在构造方法中,将迭代器下标移动到第一个有效的节点上 448 449 //:::遍历内部数组,找到第一个不为空的数组插槽slot 450 for(int i=0; i<HashMap.this.elements.length; i++){ 451 //:::设置当前index 452 this.currentIndex = i; 453 454 EntryNode<K,V> firstEntryNode = HashMap.this.elements[i]; 455 //:::找到了第一个不为空的插槽slot 456 if(firstEntryNode != null){ 457 //:::nextNode = 当前插槽第一个节点 458 this.nextNode = firstEntryNode; 459 460 //:::构造方法立即结束 461 return; 462 } 463 } 464 } 465 466 @Override 467 public boolean hasNext() { 468 return (this.nextNode != null); 469 } 470 471 @Override 472 public EntryNode<K,V> next() { 473 this.currentNode = this.nextNode; 474 //:::暂存需要返回的节点 475 EntryNode<K,V> needReturn = this.nextNode; 476 477 //:::nextNode指向自己的next 478 this.nextNode = this.nextNode.next; 479 //:::判断当前nextNode是否为null 480 if(this.nextNode == null){ 481 //:::说明当前所在的桶链表已经遍历完毕 482 483 //:::寻找下一个非空的插槽 484 for(int i=this.currentIndex+1; i<HashMap.this.elements.length; i++){ 485 //:::设置当前index 486 this.currentIndex = i; 487 488 EntryNode<K,V> firstEntryNode = HashMap.this.elements[i]; 489 //:::找到了后续不为空的插槽slot 490 if(firstEntryNode != null){ 491 //:::nextNode = 当前插槽第一个节点 492 this.nextNode = firstEntryNode; 493 //:::跳出循环 494 break; 495 } 496 } 497 } 498 return needReturn; 499 } 500 501 @Override 502 public void remove() { 503 if(this.currentNode == null){ 504 throw new IteratorStateErrorException("迭代器状态异常: 可能在一次迭代中进行了多次remove操作"); 505 } 506 507 //:::获得需要被移除的节点的key 508 K currentKey = this.currentNode.key; 509 //:::将其从哈希表中移除 510 HashMap.this.remove(currentKey); 511 512 //:::currentNode设置为null,防止反复调用remove方法 513 this.currentNode = null; 514 } 515 } 516 }
哈希表简单的测试代码:
1 public class MapTest { 2 public static void main(String[] args){ 3 testJDKHashMap(); 4 5 System.out.println("================================================="); 6 7 testMyHashMap(); 8 } 9 10 private static void testJDKHashMap(){ 11 java.util.Map<Integer,String> map1 = new java.util.HashMap<>(1,2); 12 System.out.println(map1.put(1,"aaa")); 13 System.out.println(map1.put(2,"bbb")); 14 System.out.println(map1.put(3,"ccc")); 15 System.out.println(map1.put(1,"aaa")); 16 System.out.println(map1.put(2,"bbb")); 17 System.out.println(map1.put(3,"ccc")); 18 System.out.println(map1.put(1,"111")); 19 System.out.println(map1.put(3,"aaa")); 20 System.out.println(map1.put(4,"ddd")); 21 System.out.println(map1.put(5,"eee")); 22 System.out.println(map1.put(6,"fff")); 23 System.out.println(map1.put(8,"ggg")); 24 System.out.println(map1.put(11,"bbb")); 25 System.out.println(map1.put(22,"ccc")); 26 System.out.println(map1.put(33,"111")); 27 System.out.println(map1.put(9,"111")); 28 System.out.println(map1.put(10,"111")); 29 System.out.println(map1.put(12,"111")); 30 System.out.println(map1.put(13,"111")); 31 System.out.println(map1.put(14,"111")); 32 33 System.out.println(map1.toString()); 34 System.out.println(map1.containsKey(1)); 35 System.out.println(map1.containsKey(11)); 36 System.out.println(map1.containsValue("bbb")); 37 System.out.println(map1.containsValue("aaa")); 38 System.out.println(map1.size()); 39 System.out.println(map1.get(1)); 40 System.out.println(map1.get(2)); 41 System.out.println(map1.get(3)); 42 System.out.println(map1.remove(1)); 43 System.out.println(map1.remove(2)); 44 System.out.println(map1.size()); 45 46 } 47 48 private static void testMyHashMap(){ 49 com.xiongyx.datastructures.map.Map<Integer,String> map2 = new com.xiongyx.datastructures.map.HashMap<>(1,2); 50 System.out.println(map2.put(1,"aaa")); 51 System.out.println(map2.put(2,"bbb")); 52 System.out.println(map2.put(3,"ccc")); 53 System.out.println(map2.put(1,"aaa")); 54 System.out.println(map2.put(2,"bbb")); 55 System.out.println(map2.put(3,"ccc")); 56 System.out.println(map2.put(1,"111")); 57 System.out.println(map2.put(3,"aaa")); 58 System.out.println(map2.put(4,"ddd")); 59 System.out.println(map2.put(5,"eee")); 60 System.out.println(map2.put(6,"fff")); 61 System.out.println(map2.put(8,"ggg")); 62 System.out.println(map2.put(11,"bbb")); 63 System.out.println(map2.put(22,"ccc")); 64 System.out.println(map2.put(33,"111")); 65 System.out.println(map2.put(9,"111")); 66 System.out.println(map2.put(10,"111")); 67 System.out.println(map2.put(12,"111")); 68 System.out.println(map2.put(13,"111")); 69 System.out.println(map2.put(14,"111")); 70 71 System.out.println(map2.toString()); 72 System.out.println(map2.containsKey(1)); 73 System.out.println(map2.containsKey(11)); 74 System.out.println(map2.containsValue("bbb")); 75 System.out.println(map2.containsValue("aaa")); 76 System.out.println(map2.size()); 77 System.out.println(map2.get(1)); 78 System.out.println(map2.get(2)); 79 System.out.println(map2.get(3)); 80 System.out.println(map2.remove(1)); 81 System.out.println(map2.remove(2)); 82 System.out.println(map2.size()); 83 } 84 }
我们的哈希表实现是demo级别的,功能简单,也比较好理解,希望这能够成为大家理解更加复杂的产品级哈希表实现的一个跳板。在理解了demo级别代码的基础之上,去阅读更加复杂的产品级实现代码,更好的理解哈希表,更好的理解自己所使用的数据结构,写出更高效,易维护的程序。
本系列博客的代码在我的 github上:https://github.com/1399852153/DataStructures ,存在许多不足之处,请多多指教。