这篇文章我们来学习哈希表。
目录
1.什么是哈希表
下面来看一下哈希表
1.1前言
在学习什么是哈希表之前,我们先来看下面在这一种情况。
我这里有一堆数据,我给每个数据上一个编号,假设就是从0-10,然后,我准备一个表格,就是一个一维数组。现在,我数据有了,表格有了,下面,我在数据的编号和表格索引之间建立一个关系。假设把编号0的数据放到数组索引0的位置,把编号1的数据放到数组索引1的位置,以此类推的进行存储。那么现在,只要我知道我数据的编号,那么我就能立刻的查找到我的数据,并且时间复杂度为O(1),这个是很好理解的,就是数组的查找嘛。
建立编号与表格索引的关系,将来就可以通过编号快速查找数据,在理想情况下,编号唯一,并且数组足够大能够容纳所有的数据。但是现实情况却是你不可能造一个容纳所有数据的数组,并且编号也可能会有重复。那怎么解决呢?我们可以通过拉链法来解决。当我们的数组放满后,再放入数据时,我们可以让新放入的数据的编号再与我们数组的一个索引构成映射关系,然后新方式的数据和数组中原数据以链表的形式穿成一串,以这种方式继续存储。这就是拉链法。对于数据中编号重复的问题,我们可以通过数据自身来进行区分。
上面的例子中,我们是用数组+链表的方式解决了问题。
1.2哈希表的介绍
哈希表也叫散列表,它是一种数据结构,底层是由数组+链表实现的,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1),因为哈希表的查找速度非常快,所以在很多程序中都有使用哈希表,例如拼音检查器。
哈希表也有自己的缺点,哈希表是基于数组的,我们知道数组创建后扩容成本比较高,所以当哈希表被填满时,性能下降的比较严重。
哈希表采用的是一种转换思想,其中一个重要的概念是如何将「键」或者「关键字」转换成数组下标就是数组索引?在哈希表中,这个过程由哈希函数来完成,哈希函数也是哈希表中非常重要的一部分,但是并不是每个「键」或者「关键字」都需要通过哈希函数来将其转换成数组下标,有些「键」或者「关键字」可以直接作为数组的下标。
2.哈希表的实现
下面,我们来看一下哈希表的实现
package Tree;
//哈希表的设计与实现
public class L6_HashTable {
//节点类
static class Entry{
/*
哈希码,可以理解为我们数据的一个编号,这个是我们根据key值,
用hash算法算出来的,这个编号是要与数组的索引对应起来的
*/
int hash;
Object key;//关键字,这个是我们人为设置的
Object value;//值,这个就是存的具体数据了
Entry next;//指向下一个节点的指针
public Entry(int hash, Object key, Object value) {
this.hash = hash;
this.key = key;
this.value = value;
}
}
Entry[] table = new Entry[16];//最开始的那个数组,里面存链表头结点的地址
int size = 0; //链表中元素的个数
float loadFactor = 0.75f; //元素个数与链表长度的比值
int threshold = (int) (loadFactor * table.length); //阈值
/**
* 求模运算替换为位运算
* 前提:数组长度是2的n次方
* hash % 数组长度 等价于 hash &〔数组长度-1)
* */
//根据 hash 码 获取 Value
Object get(int hash, Object key){
int idx = hash & (table.length - 1);//数据的索引位置
if (table[idx] == null){
return null;
}
Entry p = table[idx];
while (p != null){
if (p.key.equals(key)){
return p.value;
}
p = p.next;
}
return null;
}
//向哈希表中存入新的key和value,如果key重复,则更新Value
void put(int hash, Object key, Object value){
int idx = hash & (table.length - 1);//数据的索引位置
if (table[idx] == null){
table[idx] = new Entry(hash,key,value);//为空,新增操作
size++;
}else {
Entry p = table[idx];
while (true){
if (p.key.equals(key)){
p.value = value;//更新操作
return;
}
if (p.next == null)
break;
p = p.next;
}
p.next = new Entry(hash,key,value);//新增
size++;
if(size > threshold){
resize();//扩容
}
}
}
private void resize() {
Entry[] newTable = new Entry[table.length << 1];
for (int i = 0; i < table.length-1; i++) {
Entry p = table[i];//拿到每个链表头
if (p != null){
//拆分链表,并移动到新数组
/**
* 拆分规律
* 旧数组中,一个链表最多被拆分为两个链表
* hash & table.length == 0 的一组
* hash & table.length != 0 的一组
* */
Entry a = null;
Entry b = null;
Entry aHead = null;
Entry bHead = null;
while (p != null){
if ((p.hash & table.length) == 0){
if (a != null){
a.next = p;
}else {
aHead = p;
}
a = p;//分配到a
}else {
if (b != null){
b.next = p;
}else {
bHead = p;
}
b = p;//分配到b
}
p = p.next;
}
if (a != null){
a.next = null;
newTable[i] = aHead;
}
if (b != null){
b.next = null;
newTable[i+ table.length] = bHead;
}
}
}
table = newTable;//扩容完,用新数组代替旧数组
threshold = (int) (loadFactor * table.length);
}
//根据 hash 码 删除,返回删除的Value
Object remove(int hash, Object key){
int idx = hash & (table.length - 1);//数据的索引位置
if (table[idx] == null){
return null;
}
Entry p = table[idx];
Entry prev = null;
while (p != null){
if (p.key.equals(key)){//找到了
if (prev == null){
table[idx] = p.next;
}else {
prev.next = p.next;//单向链表的删除操作
}
size--;
return p.value;
}
prev = p;
p = p.next;
}
return null;
}
}
上面代码就是hashTable的实现了,总体来说不算太难,就是扩容时需要思考一下,其余的就是链表+数组的操作而已。
这个实现中没有写通过hash算法生成哈希码的方法,Hash算法的使用我会在后面单独出一篇文章来写,这里就先这样吧。