在讲解HashMap集合之前,我们先说说一个重要的数据结构---哈希表。
哈希表是一种非常优秀数据结构,对哈希表进行数据的插入,查找(有时也包括删除)的时间复杂度都是O(1)。
从这个时间复杂度,我们就可以知道哈希表是基于数组实现的,因为只有数组才可以直接通过下标获取对应的元素,而其他的数据结构要获取某个位置元素,时间复杂度最少也是O(lg N)。
既然哈希表基于数组实现的,那么就有数组的缺陷,难以扩展。因为数组的长度在创建时就需要确定,超出了数组的长度,就要进行数组扩容。
另外我们不能按照存放顺序来遍历哈希表中元素,也就是说哈希表是无序的。
一.哈希化
怎样将数据存放到哈希表中呢?也就是说我们要将数据和哈希表中的数组下标相联系起来。
将数据转换成一个值,这个值我们就叫它哈希值hashCode,每个对象都应该有哈希值,因此在Object类中提供了hashCode()方法,我们要尽量保证数据转换成的哈希值hashCode是唯一的。
怎样保证哈希值hashCode是唯一的呢?可以用一种简单的方法,就是幂的连乘。
想一想数字101和110,为什么我们知道这两个值是不同的呢?就是因为它们使用了幂的连乘。101 = 1 * 100 + 0 * 10 + 1, 110 = 1* 100 + 1 * 10 + 0.
当然还有其他的方式,来实现hashCode值尽量唯一性。
通过幂的连乘我们保证hashCode的唯一,但是导致另一个问题,就是hashCode过大,既然hashCode值与数组下标是对应的,那么就要创建超级大的数组,这个显然是不能接受的。
怎么解决这个问题?那就要用到哈希化了,将一个很大的hashCode值转成在数组下标范围内的较小的值,这个过程叫做哈希化。
我们通过哈希化解决了hashCode值过大的问题,但是又导致了一个新的问题,就是转成之后的值,我们没有办法保证它的唯一。那么就可能导致不同数据(hashCode值)对应相同的数组下标。
这个时候就产生了冲突,而解决这个冲突,我们一般有两种方式:开放地址法和链地址法。
二. 开放地址法
如果我们要存储20个元素数据,一般创建大一倍长度的数组,这里选择创建长度为41的数组。
为什么要创建大一倍长度的数组呢?其实就是用来解决冲突问题的。而这里为什么选择41呢, 是因为使用再哈希法探测的时候,要求数组长度必须是质数,后面会有详细介绍。
开放地址法怎么解决冲突呢?就是如果发现哈希化得到的下标位置在数组中已经有值了,那么就要寻找数组中没有被占用的空白位置,来存放这个元素。
寻找数组中空白位置,有三种方法:线性探测,二次探测和再哈希探测。
2.1 线性探测
它的意思就是如果当前下标已经被占用了,那么就沿着当前位置向下查找,如果下个位置也被占用,就继续查找,直到找到一个没有被占用的空白位置,将元素存放到这个位置。
有人会疑惑,既然这个值已经不在哈希化得到那个下标位置了,甚至我们都不知道它到底在那个位置了,那么我们怎么查找它呢?
其实也简单,就是先通过哈希化得到下标,然后查看数组这个下标的元素对应的hashCode值,与我们要查找元素的hashCode值是不是相同的,如果相同,那么就是这个元素,如果不相同,那就继续按照线性探测的方式,查看下一个元素的hashCode值,直到相同hashCode值的元素查找成功,或者碰到空白位置查找失败。
注,很多时候我们不能简简单单只比较hashCode值。其实两个对象是否相等,只要equals方法返回true,那么这两个对象就是相等的,但是equals方法是一个比较复杂的方法,尤其是两个对象属性很多的时候,判断起来很耗时间。所以我们优先判断hashCode值是否相等,然后调用equals方法。
因此对象的hashCode值并不一定是唯一的,但是相同对象(即equals方法返回true)的hashCode值一定是相等的。不然哈希表就变得非常混乱。
这里还有个要注意的地方,就是开放地址法哈希表删除的问题?你不能把对应数组下标的元素真实删除了。
因为我们在查找元素的时候,结束查找的条件就是查到空白元素,所以我们随意删除元素,会导致查找元素失败。简单地方式我们将被删除元素位置替换成一个标志对象。查找时碰到这个标志对象,那就继续寻找下个位置元素。添加时,碰到这个标志对象,就直接在这个位置添加新元素。
class DataItem {
private String data;
// 用deleteItem这个静态变量标记已删除元素。因为在链表删除的时候,是不能将数组中的元素置位null,用这个标记
public static final DataItem deleteItem = new DataItem();
public DataItem() {
}
public DataItem(String data) {
this.data = data;
}
public String getData() {
return data;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof DataItem)) return false;
DataItem other = (DataItem) obj;
return this.data == other.data;
}
// 用来缓存hash值,避免每次都计算hashCode值
private int hash;
// 注意这里计算hashCode方法,很容易就数值溢出了,即超出int类型的最大值。
@Override
public int hashCode() {
if (hash != 0 ) return hash;
char[] chars = data.toCharArray();
int h = 0;
for (int i = 0; i < chars.length; i++) {
char ch = chars[i];
h = h * 31 + ch;
}
hash = h;
return h;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(" {");
sb.append("data=").append(data);
sb.append('}');
return sb.toString();
}
}
public class LineHash {
private DataItem[] store;
private int maxSize;
private int size;
public LineHash(int maxSize) {
this.maxSize = maxSize;
store = new DataItem[maxSize];
size = 0;
}
// 哈希化 函数
private int hashFunc(int hashCode) {
return hashCode % maxSize;
}
public void insert(DataItem value){
int hashCode = value.hashCode();
// 得到哈希化之后的值
int hashIndex = hashFunc(hashCode);
// hashIndex对应坐标的不为null,且没有被删除,那么就查找下一个元素。
// 直到找到hashIndex对应位置 元素 为空,或者被删除。那么就在这个位置插入新元素。
// 注意这里没有考虑 死循环的情况
while (store[hashIndex] != null && store[hashIndex] != DataItem.deleteItem) {
hashIndex = hashIndex + 1;
if (hashIndex == maxSize)
hashIndex = 0;
}
store[hashIndex] = value;
size++;
}
public DataItem get(int hashCode) {
// 得到哈希化之后的值
int hashIndex = hashFunc(hashCode);
// 跳出循环的条件有两个,一个是找到一个空白元素,表示没有查到对应元素,二是找到相同hashCode的值,返回它。
// 注意不能将被删除元素作为循环终止条件,被删除元素只能作为继续查找下一个元素的条件
while (store[hashIndex] != null) {
// 如果hashIndex对应位置元素被删除了,那么就查找下一个元素。
// 注意这里千万不能将store[hashIndex] != DataItem.deleteItem判断条件放到while循环中。
if (store[hashIndex] != DataItem.deleteItem) {
DataItem temp = store[hashIndex];
if (temp.hashCode() == hashCode) {
return temp;
}
}
hashIndex = hashIndex + 1;
if (hashIndex == maxSize)
hashIndex = 0;
}
return null;
}
public DataItem remove(int hashCode) {
int hashIndex = hashFunc(hashCode);
// 跳出循环的条件有两个,一个是找到一个空白元素,表示没有查到对应元素,
// 二是找到相同hashCode的值,就将hashIndex对应下标元素换成deleteItem,表示已删除
// 注意不能将被删除元素作为循环终止条件,被删除元素只能作为继续查找下一个元素的条件
while (store[hashIndex] != null) {
// 如果hashIndex对应位置元素被删除了,那么就查找下一个元素。
// 注意这里千万不能将store[hashIndex] != DataItem.deleteItem判断条件放到while循环中。
if (store[hashIndex] != DataItem.deleteItem) {
DataItem temp = store[hashIndex];
if (temp.hashCode() == hashCode) {
size --;
store[hashIndex] = DataItem.deleteItem;
return temp;
}
}
hashIndex = hashIndex + 1;
if (hashIndex == maxSize)
hashIndex = 0;
}
return null;
}
public int getSize() {
return size;
}
public static void main(String[] args){
LineHash lineHash = new LineHash(333);
for (int i = 0; i < 100; i++) {
DataItem dataItem = new DataItem(i+"_"+i);
lineHash.insert(dataItem);
}
System.out.println("size === "+ lineHash.getSize());
DataItem keyDataItem1 = new DataItem("50_50");
DataItem keyDataItem2 = new DataItem("25_25");
// 查询方法是否有效
System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
// 删除方法是否有效
System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
// 重复删除不会有异常
System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
// 删除之后,在哈希表中就查找不到了
System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
// 删除不会影响其他元素。
System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
System.out.println("get() ==" +lineHash.get(new DataItem("75_75").hashCode()));
System.out.println("get() ==" +lineHash.get(new DataItem("755").hashCode()));
System.out.println("size === "+ lineHash.getSize());
}
}
从代码中可以看出,如果数组元素较多时,哈希化得到下标被占据的可能性越大,而且查找下一个有用位置所要耗费的时间就越多,因为数组比较满,没有空白位置。
所以当数组元素越来越多的时候,哈希表的效率就会变得非常慢,这个时候就要扩充数组,重新将原来元素放入对应位置。那么什么时候扩充数组呢?我们设置一个负载因子loadFactor(比如0.75f),当数组中元素数量超过数组长度与负载因子的乘积时,那么就扩充数组。
注意,当我们扩充数组之后,数组的长度改变了,那么哈希化之后得到坐标值也和以前不一样了。所以每次扩容都很耗时间,因为要遍历元素全部元素,重新计算哈希化后的坐标值,在放入新数组对应位置。
2.2 二次探测
二次探测是相对于线性探测来说的。当发生冲突时,线性探测是每次以1的步长去查找空白位置,那么就有一点小问题?如果数组中的元素发生了局部聚集,也就是说在数组一段区间内,都有元素没有空白位置,那么线性探测要找到空白位置,就要移动很多步。
二次探测就是每次探测的步长是步数的平方,像x+1^2, x + 2 ^2, x + 3^2等等这样,这样就可以避免局部聚集的情况了。
但是二次探测也有一个小问题,对于哈希化得到相同值的元素来说,它们每次探测的时候,所走的路径是一样,这样也会产生另外一种形式的数据聚集。
2.3 再哈希探测
想一想二次探测的问题,是因为哈希化得到相同值的元素,它们每次探测的步长是一样的,所以也会产生聚集。
怎么解决这个问题呢?在哈希化得到相同值的元素得到相同位置时,但是它们的hashCode值是不一样的,我们要利用这个条件,通过这个hashCode值得到探测的步长,那么得到的步长一般不会重复,这个过程就叫做再哈希。
这个再哈希函数应该满足两个条件,一 和哈希化函数应该不同,二 得到的结果值不能是0. 因为步长是0,就会原地踏步,陷入死循环。
一般我们使用这样的公式,step = constant - ( hashCode % constant)。constant一般为质数,hashCode % constant得到的值在constant范围内,但是有可能为0,所以用constant减它。这样就将数 0 --- constant-1 变成 constant --- 1。
还有一个注意点,再哈希探测要求数组长度是质数,为什么有这个奇怪要求呢?主要是防止一种情况,得到的步长是数组长度的因子。例如 数组长度是10,哈希化得到下标值是0或者5,得到的步长正好也是5.
这个时候就有问题了,你会发现我们的探测序列一直是 0、5、10、0、5等等,是一个死循环,没办法找其他位置。但是如果数组长度是个质数,那么它的探测序列就不一样咯,0、5、10、4、9、3、8、2、7、1、6、0 你会发现会它会遍历数组所有位置。
class DataItem {
private String data;
// 用deleteItem这个静态变量标记已删除元素。因为在链表删除的时候,是不能将数组中的元素置位null,用这个标记
public static final DataItem deleteItem = new DataItem();
public DataItem() {
}
public DataItem(String data) {
this.data = data;
}
public String getData() {
return data;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof DataItem)) return false;
DataItem other = (DataItem) obj;
return this.data == other.data;
}
// 用来缓存hash值,避免每次都计算hashCode值
private int hash;
// 注意这里计算hashCode方法,很容易就数值溢出了,即超出int类型的最大值。
@Override
public int hashCode() {
if (hash != 0 ) return hash;
char[] chars = data.toCharArray();
int h = 0;
for (int i = 0; i < chars.length; i++) {
char ch = chars[i];
h = h * 31 + ch;
}
hash = h;
return h;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(" {");
sb.append("data=").append(data);
sb.append('}');
return sb.toString();
}
}
public class DoubleHash {
private DataItem[] store;
private int maxSize;
private int size;
public DoubleHash(int maxSize) {
this.maxSize = maxSize;
store = new DataItem[maxSize];
size = 0;
}
// 哈希化 函数
private int hashFunc(int hashCode) {
return hashCode % maxSize;
}
// 再哈希,得到步长
private int getFuncStep(int hashCode) {
return 5 - hashCode % 5;
}
public void insert(DataItem value){
int hashCode = value.hashCode();
// 得到哈希化之后的值
int hashIndex = hashFunc(hashCode);
// 通过哈希值,得到步长step
int step = getFuncStep(hashCode);
// hashIndex对应坐标的不为null,且没有被删除,那么就查找下一个元素。
// 直到找到hashIndex对应位置 元素 为空,或者被删除。那么就在这个位置插入新元素。
// 注意这里没有考虑 死循环的情况
while (store[hashIndex] != null && store[hashIndex] != DataItem.deleteItem) {
hashIndex = hashIndex + step;
// 不能使用hashIndex == maxSize这个条件了,因为hashIndex不再是每次自增1,那么就有可能跳过maxSize这个值。
hashIndex = hashIndex % maxSize;
}
store[hashIndex] = value;
size++;
}
public DataItem get(int hashCode) {
// 得到哈希化之后的值
int hashIndex = hashFunc(hashCode);
// 通过哈希值,得到步长step
int step = getFuncStep(hashCode);
// 跳出循环的条件有两个,一个是找到一个空白元素,表示没有查到对应元素,二是找到相同hashCode的值,返回它。
// 注意不能将被删除元素作为循环终止条件,被删除元素只能作为继续查找下一个元素的条件
while (store[hashIndex] != null) {
// 如果hashIndex对应位置元素被删除了,那么就查找下一个元素。
// 注意这里千万不能将store[hashIndex] != DataItem.deleteItem判断条件放到while循环中。
if (store[hashIndex] != DataItem.deleteItem) {
DataItem temp = store[hashIndex];
if (temp.hashCode() == hashCode) {
return temp;
}
}
hashIndex = hashIndex + step;
hashIndex = hashIndex % maxSize;
}
return null;
}
public DataItem remove(int hashCode) {
int hashIndex = hashFunc(hashCode);
// 通过哈希值,得到步长step
int step = getFuncStep(hashCode);
// 跳出循环的条件有两个,一个是找到一个空白元素,表示没有查到对应元素,
// 二是找到相同hashCode的值,就将hashIndex对应下标元素换成deleteItem,表示已删除
// 注意不能将被删除元素作为循环终止条件,被删除元素只能作为继续查找下一个元素的条件
while (store[hashIndex] != null) {
// 如果hashIndex对应位置元素被删除了,那么就查找下一个元素。
// 注意这里千万不能将store[hashIndex] != DataItem.deleteItem判断条件放到while循环中。
if (store[hashIndex] != DataItem.deleteItem) {
DataItem temp = store[hashIndex];
if (temp.hashCode() == hashCode) {
size --;
store[hashIndex] = DataItem.deleteItem;
return temp;
}
}
hashIndex = hashIndex + step;
hashIndex = hashIndex % maxSize;
}
return null;
}
public int getSize() {
return size;
}
public static void main(String[] args){
DoubleHash lineHash = new DoubleHash(333);
for (int i = 0; i < 100; i++) {
DataItem dataItem = new DataItem(i+"_"+i);
lineHash.insert(dataItem);
}
System.out.println("size === "+ lineHash.getSize());
DataItem keyDataItem1 = new DataItem("50_50");
DataItem keyDataItem2 = new DataItem("25_25");
// 查询方法是否有效
System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
// 删除方法是否有效
System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
// 重复删除不会有异常
System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
// 删除之后,在哈希表中就查找不到了
System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
// 删除不会影响其他元素。
System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
System.out.println("get() ==" +lineHash.get(new DataItem("44_44").hashCode()));
System.out.println("size === "+ lineHash.getSize());
}
}
三. 链地址法
在开放地址法中,如果我们发现有冲突(即哈希化得到下标位置已经有值了),是通过再寻找一个空白位置,也解决冲突。根本原因是数组对应下标只能存放一个值。
这时我们就想到了一种数据结构---链表,即采用数组和链表结合方式。数组中不在存放真实的值,而是一个链表。查找的时候,就是先通过哈希化得到的下标,找到数组对应下标存放的链表,然后再遍历链表,找到hashCode值相等元素。
class DataItem {
private String data;
// 指向下一个DataItem的引用,形成一个链表
DataItem next;
public DataItem(String data) {
this(data, null);
}
public DataItem(String data, DataItem next) {
this.data = data;
this.next = next;
}
public String getData() {
return data;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof DataItem)) return false;
DataItem other = (DataItem) obj;
return this.data == other.data;
}
// 用来缓存hash值,避免每次都计算hashCode值
private int hash;
// 注意这里计算hashCode方法,很容易就数值溢出了,即超出int类型的最大值。
@Override
public int hashCode() {
if (hash != 0 ) return hash;
char[] chars = data.toCharArray();
int h = 0;
for (int i = 0; i < chars.length; i++) {
char ch = chars[i];
h = h * 31 + ch;
}
hash = h;
return h;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(" {");
sb.append("data=").append(data);
sb.append('}');
return sb.toString();
}
}
public class LinkedHash {
private DataItem[] store;
private int maxSize;
private int size;
public LinkedHash(int maxSize) {
this.maxSize = maxSize;
store = new DataItem[maxSize];
size = 0;
}
// 哈希化 函数
private int hashFunc(int hashCode) {
return hashCode % maxSize;
}
public void insert(DataItem value){
size++;
int hashCode = value.hashCode();
// 得到哈希化之后的值
int hashIndex = hashFunc(hashCode);
System.out.println("hashCode=="+hashCode+" hashIndex=="+hashIndex+" value=="+value);
DataItem temp = store[hashIndex];
// 如果对应下标对应元素为空,就将value值存到这个下标位置。value相当于一个链表
if (temp == null) {
store[hashIndex] = value;
return;
}
// 利用循环查找 链表尾,然后将value插入链表尾
while (true) {
if (temp.next == null) break;
temp = temp.next;
}
temp.next = value;
}
public DataItem get(int hashCode) {
// 得到哈希化之后的值
int hashIndex = hashFunc(hashCode);
DataItem temp = store[hashIndex];
// 链表为null,没有这个元素,返回null
if (temp == null) {
return null;
}
// 链表不为空就,遍历链表,查找与hashCode相同的元素,并返回它
while (temp != null) {
if (temp.hashCode() == hashCode) {
return temp;
}
temp = temp.next;
}
// 链表遍历完成之后,还是没有找到,就返回null
return null;
}
public DataItem remove(int hashCode) {
// 得到哈希化之后的值
int hashIndex = hashFunc(hashCode);
DataItem temp = store[hashIndex];
// 链表为null,没有这个元素,返回null,删除失败
if (temp == null) {
return null;
}
DataItem prev = null;
// 链表不为空就,遍历链表,查找与hashCode相同的元素,从列表中删除它。
while (temp != null) {
if (temp.hashCode() == hashCode) {
// 如果prev为空,表示temp是链表头,所以要重新给store[hashIndex]数组赋值
if (prev == null) {
store[hashIndex] = temp.next;
} else {
// prev不为空,就将它的next指向temp的 next,这样就将temp从链表中删除了
prev.next = temp.next;
}
size -- ;
return temp;
}
temp = temp.next;
}
return null;
}
public int getSize() {
return size;
}
public static void main(String[] args){
LinkedHash lineHash = new LinkedHash(333);
for (int i = 0; i < 100; i++) {
DataItem dataItem = new DataItem(i+"_"+i);
lineHash.insert(dataItem);
}
System.out.println("size === "+ lineHash.getSize());
DataItem keyDataItem1 = new DataItem("50_50");
DataItem keyDataItem2 = new DataItem("25_25");
// 查询方法是否有效
System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
// 删除方法是否有效
System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
// 重复删除不会有异常
System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
// 删除之后,在哈希表中就查找不到了
System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
// 删除不会影响其他元素。
System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
System.out.println("get() ==" +lineHash.get(new DataItem("44_44").hashCode()));
System.out.println("size === "+ lineHash.getSize());
}
}
总结
哈希值与哈希化
哈希表是基于数组实现的,对数据的插入,查找(有时也包括删除)的时间复杂度都是O(1)。有两个很重要的概念哈希值与哈希化
哈希值:每个对象都有哈希值hashCode,所以在顶层父类Object中提供了hashCode()方法。
对于哈希值,我们要尽量使不同对象有不同的哈希值,但是极个别情况下发生相等情况也是可以的。
但是我们必须保证,相等对象一定有相等的哈希值,这时使用哈希表的基石。所以也解释了为什么复写Object的equals(Object obj)方法时,要求复写hashCode(),不然你使用哈希表存放对象时,就会产生意想不到的情况。
哈希化:将较大的哈希值hashCode转成一定区间范围内的较小值(数组长度范围之内)。
为什么要哈希化?因为我们在数组中存放值,就要知道对应下标,如果直接使用哈希值hashCode作为数组下标,就非常不现实。一般哈希值hashCode比较大,那要创建很大的数组。我们一般根据存放数据的数量来来创建相应大小数组。
这里我们实现哈希化,是使用取余操作,即hashCode % 数组长度,这样就能得到在数组长度范围内的值。但是取余操作是非常耗时的,不建议使用。在HashCode中我们会介绍一种更好的实现方式。
但是有一个问题,哈希化得到的下标值可能是相同的,这个时候就产生冲突,而解决这个冲突,我们一般有两种方式:开放地址法和链地址法。
开放地址法
开放地址法解决冲突的方式,它会创建一个比要存放数据数量大一倍的数组,如果发生冲突,它会沿着数组下标向下查找一个空白位置,将元素存放到这个位置。
根据向下查找步长不同,又分为三种方式线性探测,二次探测和再哈希探测
- 线性探测:步长固定,且为1
- 二次探测:步长固定,是步数的平方,像x+1^2, x + 2 ^2, x + 3^2等等这样。
- 再哈希探测:步长不固定,根据哈希值得到不同步长,一般使用公式 step = constant - (key % constant),其中constant为质数。
查找元素时,先通过哈希化得到一个下标值,获取数组对应位置值,比较这它们的哈希值,如果相同就表示查找到了,如果不同,就根据探测方式不同,向下查找下一个位置元素,直到找到哈希值相等元素查找成功,或者一个空白元素查找失败。
注:一般情况下,我们不只是仅仅比较哈希值,因为可能存在不同对象哈希值相等情况,所以我们在哈希值相等情况下,继续调用equals方法,这样才能确保相等。
那为什么不直接调用equals方法,还干嘛进行哈希值的比较。那是因为equals方法有可能是非常耗时的,而比较哈希值,我们可以排除不相等情况,这个非常重要。
因为开放地址法查找元素的特性,所以删除元素时,不能真的将对应数组下标位置设置为null,这样可能会导致查找元素失败。
当哈希表元素较多时,会发现哈希表效率会变得特别低,因为发生冲突的可能性越来越大,并且查找下一个空白位置,所需要的耗时也越多。这个时候我们就要进行数组的扩容,来提高哈希表的效率。
注意数组的长度改变了,那么哈希化得到的值也发生了改变,所以我们必须重新遍历哈希表中元素,通过哈希化,将它们存放到新数组对应位置。
什么时候进行数组的扩容呢? 这里就有两个概念了 负载因子 和 阈值。
阈值等于 数组长度 * 负载因子,当哈希表元素个数超过阈值时,就扩充数组。
链地址法
链地址法哈希表采用数组和链表结合的方式,来解决冲突的。即数组不再直接存放元素,而是存放一个链表,元素存放在链表中。
哈希化得到下标值,获取对应位置的值,如果为null,那么新创建一个链表,将元素存放到链表头,然后将链表存入数组中,如果不为null,就是一个链表,将元素存入这个链表尾。
查找元素时,哈希化得到下标值,获取对应位置的值,如果为null,那就是查找失败,如果不为null,就是一个链表,遍历一个链表,查找哈希值相等的元素,如果相等就表示查找成功,如果遍历完整个链表,还是没有找到,就表示查找失败。
理论上说,链地址法哈希表是不需要扩容的,因为链表的存储几乎是无限,但是这样的话,就没有哈希表的优势了,因为链表过长,遍历链表耗时变长。
所以我们也要确定一个阈值,当哈希表元素数量超过阈值时,就要进行数组扩容,并重新分配元素位置。