采用数组/链表的方式,时间复杂度为0(n)
但当需要我们查找的数据量过大时,所需时间较多。当我们需要查找的时间复杂度比较小时可以采用哈希表。
1.哈希算法
哈希算法是其中最重要的部分,对于不同的数据进行哈希算法之后,得到一个不同的数字编码。
不同数据:大小 长度 不同,字符串长度不同等等。不同的数据计算出的hash值不一样,一定要保证相同的数据计算出现的编码是一样的,并且过程不可逆,无法从hash值判断出他是什么元素。
不同的数字编码:都会确定到一个范围之类 比如int范围。
但当数据量过大时可能会出现两个不同的数据计算出一样的编码。
2.哈希表存储方法
2.1存储方式:
K,V 要求K不重复。由K算出hash值。
2.2存储结构:
数组为主要结构 链表/树辅助存储,本篇采用链表的方式辅助。
2.3属性:
数组的长度,当前元素个数,数组被使用的空间大小,扩容(为原容量的两倍)初始容量为16 当空间大小>=数组的长度*0.75进行扩容。
2.4存储原理
由key计算hash值,使用hash值与数组当前最大下标进行&运算,映射一个下标,存入k,v节点
注:1.如果当前存在节点,根据比较hash值以及key来决定是否替换当前节点,如果链表中每一个都不相等,此时存在当前节点的最后一个,增加一个新节点。
2.5查找
通过计算hash值,映射下标,找到该下标是否有节点,有则逐一比较k是否一致,一致则返回。
3.练习:
package lcr0408;
import java.util.Arrays;
import java.util.HashMap;
class Node<K, V> {
K key;
V value;
int hash;
Node<K, V> next;
public Node(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
public class MyHashMap<K, V> {
Node<K, V>[] table;
int tableSize;
int elementSize;
int length;
final int def_Length = 16;
public MyHashMap() {
table = new Node[def_Length];
length = def_Length;
}
public V get(K key) {
}
//扩容2.0
private void grow() {
}
public void put(K key, V val) {
}
}
首先定义一个节点类,属性为K,V,以及hash值,指向下一个节点的next.改变构造方法直接传值。
接着定义MyHashMap的类,需要上面所说tablesize数组被使用的空间大小,elementsize元素多少,length数组长度。def_length数组初始长度。
3.1put存入方法
public void put(K key, V val) {
System.out.println("放入key:" + key);
Node<K, V> node = new Node<>(key, val, key.hashCode());
int h = key.hashCode();
int index = h & (length - 1);
if (table[index] == null) {
table[index] = node;
tableSize++;
elementSize++;
} else {
Node<K, V> first = table[index];
if (first.hash == h && first.key == key || first.key.equals(key)) {
first.value = val;
} 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 = val;
return;
}
}
temp.next = node;
// System.out.println("放入node.key:" + node.key);
elementSize++;
}
}
if (tableSize >= length * 0.75) {
grow();
/*System.out.println("tableSIle"+tableSize);
System.out.println("length:"+length);*/
}
}
存入需要的key,val后定义一个node用于接受,算出它的hash值h,已经映射的下标index。如果此时这个数组下标为空,则直接将node传入即可。不为空进行下一步运算,再次定义一个节点first将此头结点赋给他,便于遍历。遍历数组,并进行判断hash以及k相同则进行替代。没有则将放在最后一个节点后即可。此时后面先进行扩容的判断。
3.2get查找
public V get(K key) {
int h = key.hashCode();
int index = h & (length - 1);
System.out.println("length:" + length);
Node<K, V> first = table[index];
if (first != null) {
V v = null;
if (first.hash == h && first.key == key || first.key.equals(key)) {
v = first.value;
} else {
Node<K, V> temp = first;
while (temp.next != null) {
temp = temp.next;
if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
v = temp.value;
break;
}
}
}
return v;
}
return null;
}
根据key算出哈希值以及下标,进行比较找到则返回,没有则返回null。
3.3扩容
private void grow() {
int new_length = length + length;
tableSize = 0;
Node<K, V>[] newTable = new Node[new_length];
for (int i = 0; i < length; i++) {
if (table[i] != null) {
Node<K, V> e = table[i];
if (e.next == null) {
newTable[e.hash & (new_length - 1)] = e;
tableSize++;
} else {//有链表
Node<K, V> lowHead = null, lowNext = null;
Node<K, V> hiHead = null, hiNext = null;
Node<K, V> next;
do {
next = e.next;
if ((e.hash & length) == 0) {//放在原下标处 与原长度相比较 只看重最新一位
if (lowNext == null) {
lowHead = e;
} else {
lowNext.next = e;
}
lowNext = e;
} else {
if (hiNext == null) {
hiHead = e;
} else {
hiNext.next = e;
}
hiNext = e;
}
} while ((e = next) != null);
if (lowHead != null) {
newTable[i] = lowHead;//放原本下标
tableSize++;
}
if (hiHead != null) {
newTable[i + length] = hiHead;//放原本下表加上原最大下标
tableSize++;
}
}
}
}
table = newTable;
length = new_length;
}
扩容最为关键,因为不仅需要创建一个新数组,传入原数组。但当我们创建新数组时大小扩大此时数组最大下标变化,此时需要重新计算每个的下标。而在这采用的方法是当我们进行&运算时 原数组与15(以扩容第一次为例)进行1111 扩容后与1 1111此时可以发现后四位是一样的 决定元素关键在于扩容的那一位即第一个1 只有两个结果:1.放于原下标 位置 2.放于原下标再加上原数组长度的位置。那么我们可以将其hash值与原长度1 0000 进行&运算 只会出现 0/1 0000。0放在原下标,另一个放于第二个结果即可。
首先定义一个新长度为原长度的两倍,将tablesize清零。创建新数组。接着遍历整个旧数组,节点不为0处,记录此节点 用e来表示。如果e的下一个为空则只有一个元素,直接传入新数组即可。不为空那么还有链表,此时定义两个链表,一个用lowHead表示传入原下标的,hiHead表示第二种。接着判断e的hash值与原长度&。为0传入lowHead中。结束后将lowHead hiHead分别放入新数组中即可。