哈希表介绍
注意:
这里的 无顺序 和 key不允许重复 是不是很熟悉?没错,就是集合和字典的特点,这就是为什么集合和字典可以基于哈希表!
什么是哈希表?
需要一种办法:使得知道值而直接返回出对应的下标值,那么效率就会变得很高!
但是怎么将不确定的值(比如:字符串、数字等)和下标值找出对应的关系呢?
字符串转下标
方法一:数字相加
方法二:幂的连乘
总结:
第一种方案产生的下标太少,导致会有多个字符串公用一个下标;第二种方案产生的下标太多,导致浪费空间(基本上内存创造不出那么大的数组)。
所以上面两个都不能很好的解决问题!这就要引出哈希化了,什么是哈希化?
哈希化
这里应该这样理解:
类比字母放入700...00个数组范围内,但是并不是每一个位置都有正确的英文字母,正确的英文是随机出现的且只有50000个。那么现在再来看0 ~ 199,假设其实只有5个数是需要的,然后这5个数是随机的,取余之后放入的是长度为10的数组(0 ~ 9),所以余数相同概率确实很小。
理解:
- 哈希化就是将大数(幂的连乘)转化为小数(取模/余),且减少重复(解决重复)或者不重复的一个过程
- 哈希函数就是哈希化过程实现的一个函数代码
- 整个函数与返回结果的封装就是一个哈希表
如何解决重复?
链地址法(拉链法)
开放地址法
线性探测
二次探测
注意:
- 这里连续插入元素余数为2的情况,比前面插入连续的数的情况要更加少,所以该方法还是要强一点!但是还是不好!
- 菜鸟认为的缺陷不是很准确,具体看到后面再哈希的质数重要性才知道,他是循环探测的,如果大于就再从头开始!
再哈希法
这里理解不和第一个哈希函数相同:
如果还和原来的一样,那么2-22-122结果还是都余二,那么最后还是每个都移动两格,最后就和二次探测类似了!
这里质数很关键,质数可以让分布更加均匀!
哈希化效率
开放地址法
线性探测效率
二次探测和再哈希 -- 这两个差不多 > 线性
链地址法
优秀的哈希函数
优化 -- 幂的连乘
注意:
计算算法复杂度 不用考虑系数 !
优化 -- 均匀分布(质数)
再哈希的质数重要性 -- 哈希表长度
java中的hashmap
总结
学了这么多,其实感觉大致知道了哈希表是个什么东西以及流程!这里会有两个问题:
- 为什么JavaScript中进行较大的位运算时会出问题?
- 怎么写出哈希表?
封装Hash函数
代码
// 设计哈希函数
// 1> 将字符串转成比较大的数字:hashCode
// 2> 将大的数字hashCode压缩到数组范围(大小)之内
function hashFunc(str,size){
// 1 定义hashCode变量
let hashCode = 0;
// 2 霍纳算法,来计算hashCode的值
// eg: cats -> Unicode编码
for(let i = 0;i<str.length; i++){
// 用得比较多的质数 -> 37
hashCode = 37 * hashCode + str.charCodeAt(i);
}
// 3 取余操作
let index = hashCode % size;
return index;
}
// 测试哈希函数
alert(hashFunc('abc',7)); // 4
alert(hashFunc('cba',7)); // 3
alert(hashFunc('nba',7)); // 5
alert(hashFunc('jkd',7)); // 3
其实就是理解怎么将字符串转化成大的数字就行,转化成大数时又由于需要计算很多的乘法,所以就将其用霍纳法则代替,就出现了这里的for循环里面的操作,菜鸟感觉难一点的可能就是这里了!
封装Hash表结构
代码
// 链地址法 -- 数组实现
function Hashtable(){
// 属性
this.storage = [];
// 后面要使用count计算装填因子,>0.75就扩容,<0.25就缩小
this.count = 0;
// 当前总长度
this.limit = 7; // 这个值随意,因为要扩容或者缩小的,但是最好每次变化都是质数
//哈希函数
Hashtable.prototype.hashFunc = function (str,size){
let hashCode = 0;
for(let i = 0;i<str.length; i++){
hashCode = 37 * hashCode + str.charCodeAt(i);
}
let index = hashCode % size;
return index;
}
// 方法
}
这里是用数组嵌套数组实现哈希表的,但是感觉常见的别人封装的哈希表都是数组加链表,所以感兴趣的读者最好用链表写写,也希望积极在评论区留言!
菜鸟感觉算法题的话,用数组更多一点!说实话,感觉算法题基本上都是数组加算法,其它的数据结构很少出现在算法题中,因为太复杂容易超时!
后面发现这个结构不是特别好,对于同名的人员的信息是存不了的,会替换原来的,那感觉用 对象/Map 存k/v结构的确实比较好!
方法实现
插入和修改
注意
这里面有一点不是很严谨,就是当bucket不存在时,应该是undefined,但是这用的却是null,因为undefined == null 是true,但是菜鸟建议用 ===,所以后面或者上面的截图,null理解为undefined就行!
代码
// 1 插入修改
Hashtable.prototype.put = function(key,value){
// 1 根据key获取对应的index
let index = this.hashFunc(key,this.limit);
// 2 根据index取出对应的bucket
let bucket = this.storage[index];
// 3 判断该bucket是否为undefined
if(bucket === undefined){
bucket = [];
this.storage[index] = bucket;
}
// 4 判断是否是修改数据,是修改,就修改了直接return,不会执行后面了;不是修改就直接执行后面的,加入
for(let i = 0;i < bucket.length;i++){
let tuple = bucket[i];
// 这个tuple是存放 k/v 的数组
if(tuple[0] == key){
tuple[1] = value;
return ;
}
}
// 添加操作
bucket.push([key,value]);
this.count += 1;
}
获取
哈希表不存在用下标来获取值,而是直接通过key转化的下标获取值,所以知道key求value就相当于知道下标求值!
如果是知道value求key的话,感觉哈希表并不适合!
代码
// 2 获取操作
Hashtable.prototype.get = function(key){
// 根据key获取index
let index = this.hashFunc(key,this.limit);
// 2 根据index获取对应的bucket
let bucket = this.storage[index];
// 3 判断bucket是否为undefined
if(bucket === undefined){
return null;
}
// 4 有bucket,进行线性查找
for(let i = 0;i<bucket.length;i++){
let tuple = bucket[i];
if(tuple[0] === key){
return tuple[1];
}
}
// 5 没找到
return null;
}
删除
代码
// 3 删除操作
Hashtable.prototype.remove = function(key){
let index = this.hashFunc(key,this.limit);
let bucket = this.storage[index];
if(bucket === undefined){
return null;
}
for(let i=0;i<bucket.length;i++){
let tuple = bucket[i];
if(tuple[0] === key){
bucket.splice(i,1);
this.count -= 1;
return tuple[1];
}
}
return null;
}
菜鸟感觉,其实只要会写一个,其它的只要知道思路,类比一下就很好写,反正这种数组的方式书写哈希表挺简单的!
其它方法
哈希表测试(有冲突)
let Hash = new Hashtable();
Hash.put('abc',2);
Hash.put('cba',7);
Hash.put('nba',7);
Hash.put('jkd',7);
alert(Hash.get("abc"));
console.log(Hash);
Hash.put("abc",222);
alert(Hash.get("abc"));
console.log(Hash);
Hash.remove("nba");
alert(Hash.get("nba"));
console.log(Hash);
菜鸟这里建议,测一个就注释一下,因为 console.log 手动打开的异步性!这里菜鸟在之前的数据结构和算法的文章中提及过,这里就不多说了!
哈希表扩容思想
代码
// 插入修改 --> 加入扩容
Hashtable.prototype.put = function(key,value){
// 1 根据key获取对应的index
let index = this.hashFunc(key,this.limit);
// 2 根据index取出对应的bucket
let bucket = this.storage[index];
// console.log(bucket);
// 3 判断该bucket是否为null
if(bucket === undefined){
bucket = [];
this.storage[index] = bucket;
}
// 4 判断是否是修改数据,是修改,就修改了直接return,不会执行后面了;不是修改就直接执行后面的,加入
for(let i = 0;i < bucket.length;i++){
let tuple = bucket[i];
// 这个tuple是存放 k/v 的数组
if(tuple[0] == key){
tuple[1] = value;
return ;
}
}
// 添加操作
bucket.push([key,value]);
this.count += 1;
// 判断是否需要扩容
if(this.count > this.limit * 0.75){
this.resize(this.limit * 2);
}
}
// 删除操作 --> 加入缩容
Hashtable.prototype.remove = function(key){
let index = this.hashFunc(key,this.limit);
let bucket = this.storage[index];
if(bucket === undefined){
return null;
}
for(let i=0;i<bucket.length;i++){
let tuple = bucket[i];
if(tuple[0] === key){
bucket.splice(i,1);
this.count -= 1;
// 判断是否缩小
if(this.limit > 7 && this.count < this.limit * 0.25){
// 向下取整,因为总是除以2,可能会有小数
this.resize(Math.floor(this.limit / 2));
}
return tuple[1];
}
}
return null;
}
// 哈希表扩容/缩小
Hashtable.prototype.resize = function(newLimit){
// 1 保存旧数组的内容
let oldStorage = this.storage;
// 2 重置所有属性
this.storage = [];
this.count = 0;
this.limit = newLimit;
// 3 遍历oldStorage中所有的bucket
for(let i = 0;i<oldStorage.length;i++){
// 1 取出bucket
let bucket = oldStorage[i];
// 2 判断bucket是否为undefined
if(bucket === undefined){
continue;
}
// 3 bucket中有数据,取出数据,重新插入
for(let j = 0;j<bucket.length;j++){
let tuple = bucket[j];
this.put(tuple[0],tuple[1]);
}
}
}
这里菜鸟本来是想到了自己的一个办法,代码如下
// 哈希表扩容/缩小
Hashtable.prototype.resize = function(newLimit){
// 1 保存旧数组的内容
let oldStorage = this.storage;
// 2 重置所有属性
this.storage = [];
this.count = 0;
this.limit = newLimit;
// 3 遍历oldStorage中所有的bucket
for(let i = 0;i<oldStorage.length;i++){
// 1 取出bucket
let bucket = oldStorage[i];
console.log(i,bucket);
// 2 判断bucket是否为undefined
if(bucket === undefined){
continue;
}
// 3 bucket中有数据,取出数据,重新插入
// 我的方法
let a = bucket[0];
let ind = this.hashFunc(a[0],this.limit);
console.log(this.limit,ind);
this.storage[ind] = bucket;
}
}
菜鸟的方法有一个好处,就是不需要两层循环!
这里循环就是为了把bucket里面的所有元素移到新的bucket所以又要一层循环,菜鸟当时就想,循环一层为什么不直接把一整个bucket给移过去?那样不就直接了当了吗?但是发现结果不一样,然后想了好久,菜鸟对比代码差别才发现,我那个考虑漏掉了count+1,顿时醍醐灌顶,结束了自己的困惑!
扩容保证质数
判断质数1 -- 不好
// 封装函数:判断传入的数字是否是质数 --> 效率不高,不准确(eg:0,1)
// 特点:只能被1和自己整除,不能被2到num-1之间的数字整除
function isPrime(num){
for(let i = 2;i<num;i++){
if(num % i === 0){
return false;
}
}
return true;
}
alert(isPrime(0));
alert(isPrime(1));
alert(isPrime(2));
alert(isPrime(3));
alert(isPrime(4));
判断质数2 -- 不全
// 封装函数:判断传入的数字是否是质数 --> 效率较高,不准确(eg:0,1)
function isP(num){
let temp = parseInt(Math.sqrt(num));
for(let i = 2;i<=temp;i++){
if(num % i ===0){
return false;
}
}
return true;
}
这里规定了limit最小是7所以没有关系,如果没规定,一定要将0、1当成一种情况,单独return!
质数补全代码
判断质数和获得质数
// 判断某个数字是否是质数
Hashtable.prototype.isPrime = function(num){
let temp = parseInt(Math.sqrt(num));
for(let i = 2;i<=temp;i++){
if(num % i ===0){
return false;
}
}
return true;
}
// 获取质数
Hashtable.prototype.getPrime = function(num){
while(!this.isPrime(num)){
num++;
}
return num;
}
插入的修改
// 1 插入修改
Hashtable.prototype.put = function(key,value){
// 1 根据key获取对应的index
let index = this.hashFunc(key,this.limit);
// 2 根据index取出对应的bucket
let bucket = this.storage[index];
// console.log(bucket);
// 3 判断该bucket是否为null
if(bucket === undefined){
bucket = [];
this.storage[index] = bucket;
}
// 4 判断是否是修改数据,是修改,就修改了直接return,不会执行后面了;不是修改就直接执行后面的,加入
for(let i = 0;i < bucket.length;i++){
let tuple = bucket[i];
// 这个tuple是存放 k/v 的数组
if(tuple[0] == key){
tuple[1] = value;
return ;
}
}
// 添加操作
bucket.push([key,value]);
this.count += 1;
// 判断是否需要扩容
if(this.count > this.limit * 0.75){
let newSize = this.limit * 2;
let newPri = this.getPrime(newSize);
this.resize(newPri);
}
}
删除的修改
// 3 删除操作
Hashtable.prototype.remove = function(key){
let index = this.hashFunc(key,this.limit);
let bucket = this.storage[index];
if(bucket === undefined){
return null;
}
for(let i=0;i<bucket.length;i++){
let tuple = bucket[i];
if(tuple[0] === key){
bucket.splice(i,1);
this.count -= 1;
// 判断是否缩小
if(this.limit > 7 && this.count < this.limit * 0.25){
// 向下取整,因为总是除以2,可能会有小数
let newSize = Math.floor(this.limit / 2);
let newPri = this.getPrime(newSize);
this.resize(newPri);
}
return tuple[1];
}
}
return null;
}