哈希表原理
哈希表是一种非常重要的数据结构,几乎所有的编程语言都有直接或者间接的应用这种数据结构,它通常是基于数组实现的。
相对于数组,它有更多的优势
- 它可以提供非常快速的插入-删除-查找操作。
- 哈希表的速度比数组还要快,基本可以瞬间查找到想要的元素 。
- 哈希表相对于数组来说编码要容易的多。
但是哈希表相对于数组也有一些不足:
- 哈希表中的数组是没有顺序的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素。
- 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。
哈希化
将大数字转化成数组范围内下标的过程,我们就称之为哈希化
哈希函数
通常我们会将单词转成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数我们称为哈希函数
哈希表
最终将数据插入到的这个数组,对整个结构的封装,我们就称之为是一个哈希表
哈希化冲突问题
链地址法
链地址法是一种比较常见的解决冲突的方案(也称拉链法)
创建了一个内存为10的数组,现在,需要将一些数字存到数组内部,这些数字哈希化后可能会重复,将下标值相同的数通过链表或者数组链接起来的方法叫做链地址法。当我们要查找某值的时候,就可以先根据其下标找到对应的链表或者数组再在其内部寻找。
开放地址法
开放地址法的主要工作方式是寻找空白的单元格来添加重复的数据。
开放地址法的主要工作方式是寻找空白的单元格来添加重复的数据。如下图所示:
如果有一个数字32,现在要将其插入到数组中,我们的解决方案为:
新插入的32本来应该插入到52的位置,但是该位置已经包含数据, 可以发现3、5、9的位置是没有任何内容的 这个时候就可以寻找对应的空白位置来放这个数据 但是探索这个位置的方式不同,有三种方式:
1、线性探索
即线性的查找空白单元
插入32
经过哈希化得到的index=2,但是在插入的时候,发现该位置已经有52 此时就从index+1的位置开始一点点查找合适的位置来放置32 探测到的第一个空的位置就是该值插入的位置 查询32
首先经过哈希化得到index= 2,比较2的位置结果和查询的数值是否相同,相同则直接返回 不相同则线性查找,从index位置+1查找和32一样的。 需要注意的是:如果32的位置之前没有插入,并不需要将整个哈希表查询一遍来确定该值是否存在,而是如果查询到空位置,就停止。因为32之前不可能跳过空位置去其他的位置。
线性探测也有一个问题就是:如果之前插入的数据是连续插入的,则新插入的数据就需要很长的探测距离。
2、二次探索
二次探索就在线性探索的基础上进行了优化。
线性探测,我们可以看做是步长为1的探测,比如从下标值x开始,从x+1,x+2,x+3依次探测。
二次探测对步长做了优化,比如从下标值x开始,x+1²,x+2²,x+3²依次探测。
但是二次探测依然存在问题:比如我们连续插入的是32-112-82-42-52,那么他们依次累加的时候步长是相同的,也就是这种情况下会造成步长不一的一种聚集,还是会影响效率,怎样解决这个问题呢?来看看再哈希法。
3、再哈希法
再哈希法的做法就是:把关键字用另一个哈希函数在做一次哈希化,用这次哈希化的结果作为步长,对于指定的关键字,步长在整个探测中是不变的,不同的关键字使用不同的步长。
第二次哈希化需要具备以下特点:
和第一个哈希函数不同
不能输出为0(否则,将没有步长,每次叹词都是原地踏步,算法进入死循环)
而计算机专家已经设计好了一种工作很好的哈希函数。
stepSize = constant - (key % constant)
其中constant是质数,且小于数组的容量,key是第一次哈希化得到的值。
例如:stepSize = 5-(key%5),满足需求,并且结果不可能为0。
hashTable的实现
function hashTable() {
// 属性
this.storage = []; // 数组中存放的相关元素
this.count = 0; // 存了多少数据
this.limit = 7; // 用于标记数组中一共存放了多少元素
}
哈希函数
hashTable.prototype.hashFunc = function (str, size) {
// 1 定义hashcode变量
let hashCode = 0;
// 霍纳算法,来计算hashcode的值
// eg: cats --> Unicode编码
for (let i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i); // 37用的质数比较多
}
// 取余操作
let index = hashCode % size;
return index;
};
插入或修改
1根据key获取索引值,将数据插入到相对应的位置
2 根据索引值取出bucket(桶)
- 2.1 如果桶不存在,创建桶,并放置在该索引的位置
3 判断是新增还是修改
- 3.1 如果没有值,就新增,如果值存在,则修改
4 新增操作
hashTable.prototype.put = function (key, value) {
let index = this.hashFunc(key, this.limit); // 根据key找到对应的index
let bucket = this.storage[index]; // 根据索引值取出bucket(桶)
// 判断桶是否为null
if (bucket == null) {
bucket = [];
this.storage[index] = bucket;
}
// 判断是否是修改数据 根据key查找
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
// 找到相同的key,赋值value
if (tuple[0] === key) {
tuple[1] = value;
return;
}
}
// 新增
bucket.push([key, value]);
this.count += 1;
// 判断是否需要扩容
if (this.count > this.limit * 0.75) {
let newLimit = this.limit * 2; // 扩容,但不是质数 eg: 14
let primeNumber = this.getPrime(newLimit); // 获取质数
this.resize(primeNumber);
}
};
查找
1 根据key获取相对应的index
2根据index获取相对应的bucket
3 判断bucket是否为null
- 3.1为null return null
- 3.2 不为null 线性查找bucket中的每一个key 找到烦恼会value,否则返回null
hashTable.prototype.get = function (key) {
let index = this.hashFunc(key, this.limit);
let bucket = this.storage[index];
if (bucket == null) return null;
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i];
if (tuple[0] === key) {
return tuple[1]; // 返回value
}
}
return null;
};
删除
hashTable.prototype.remove = function (key) {
let index = this.hashFunc(key, this.limit);
let bucket = this.storage[index];
if (bucket == null) return null;
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i];
if (tuple[0] === key) {
bucket.splice(i, 1);
this.count--;
// 判断是否缩容
if (this.limit > 7 && this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2));
}
return tuple[1]; // 返回删除的value
}
}
return null;
};
扩容
hashTable.prototype.resize = function (newLimit) {
let oldStorage = this.storage; //备份数据
// 重置属性
this.storage = []; // 数组中存放的相关元素
this.count = 0; // 存了多少数据
this.limit = newLimit; // 用于标记数组中一共存放了多少元素
// 扩容后,空间变大,取出原先的数据重新放入
for (let i = 0; j < oldStorage.length; i++) {
let bucket = oldStorage[i]; //取桶
if (oldStorage[i] == null) continue; // 存在桶是空 遍历全部数据,取出全部桶
for (let j = 0; j < bucket.length; j++) {
let tuple = bucket[j];
this.put(tuple[0], tuple[1]);
}
}
};
判断是不是质数
hashTable.prototype.isPrime = function (num) {
let temp = parseInt(Math.sqrt(num)); // num的算术平方根;
// 判断质数,优化算法,只需要判断 小于等于 这个数的算术平方根 ,只能除够1和它本身
for (let i = 2; i <= temp; i++) {
if (num % i == 0) {
return false; // 能够除够i 说明不是质数
}
}
return true; // (2,temp] 没有一个能够整除,说明是质数
};
获取num后面的质数
hashTable.prototype.getPrime = function (num) {
while (!this.isPrime(num)) {
num++;
}
return num;
};
let ht = new hashTable();
ht.put("basketball", "姚明");
ht.put("football", "梅西");
ht.put("shooting", "许海峰");
ht.put("diving", "郭晶晶");
ht.put("snooker", "丁俊晖");
ht.put("basketball", "勒布朗詹姆斯");
let value = ht.get("basketball");
console.log(value);
// let removeValue = ht.remove("snooker");
// console.log(removeValue);
console.log("ht", ht);
let flag = ht.isPrime(12);
console.log("flag", flag);