携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
1.哈希表
1.哈希表通常是基于数组实现的,但是相对于数组,他存在更多的优势
2.哈希表可以提供快速的插入-删除-查找操作
3.插入和删除的时间复杂度为O(n);
4.哈希表的速度比树还要快
5.哈希表结构是数组,原理在于下标值的一种变换
6.将大数字转化为数组范围内下标的过程,称为哈希化
7.哈希函数:将单词转化成大数字,把大数字进行哈希化,转化成数组下标
8.哈希表,对最终插入的数组进行整个结构的封装,得到的就是哈希表
2.哈希表的缺点
1.哈希表中的数据是没有顺序的,所以不能以某种方式来遍历其中的元素
2.通常,哈希表中的key是不允许重复的,不能使用相同的key保存元素
3.哈希化的方式
为了把字符串转化为对应的下标值,需要有一套编码系统,为了方便理解我们创建这样一套编码系统:比如a为1,b为2,c为3,以此类推z为26,空格为27(不考虑大写情况)。
有了编码系统后,将字母转化为数字也有很多种方式:
方式一:数字相加。例如cats转化为数字:3+1+20+19=43,那么就把43作为cats单词的下标值储存在数组中;
但是这种方式会存在这样的问题:很多的单词按照该方式转化为数字后都是43,比如was。而在数组中一个下标值只能储存一个数据,所以该方式不合理。
方式二:幂的连乘。我们平时使用的大于10的数字,就是用幂的连乘来表示它的唯一性的。比如: 6543=6 * 103 + 5 * 102 + 4 * 10 + 3;这样单词也可以用该种方式来表示:cats = 3 * 273 + 1 * 272 + 20 * 27 + 17 =60337;
虽然该方式可以保证字符的唯一性,但是如果是较长的字符(如aaaaaaaaaa)所表示的数字就非常大,此时要求很大容量的数组,然而其中却有许多下标值指向的是无效的数据(比如不存在zxcvvv这样的单词),造成了数组空间的浪费。
两种方案总结:
第一种方案(让数字相加求和)产生的数组下标太少;
第二种方案(与27的幂相乘求和)产生的数组下标又太多;
现在需要一种压缩方法,把幂的连乘方案系统中得到的巨大整数范围压缩到可接受的数组范围中。可以通过取余操作来实现。虽然取余操作得到的结构也有可能重复,但是可以通过其他方式解决。
哈希化之后下标仍然可能重复,我们需要采用方法来解决冲突
4.解决冲突常见的两种方案:
方案一:链地址法(拉链法);
如下图所示,我们将每一个数字都对10进行取余操作,则余数的范围0~9
作为数组的下标值。并且,数组每一个下标值对应的位置存储的不再是一个数字了,
而是存储由经过取余操作后得到相同余数的数字组成的数组或链表。
这样可以根据下标值获取到整个数组或链表,之后继续在数组或链表中查找就可以了。而且,产生冲突的元素一般不会太多。
总结:链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,
而是一条链条,这条链条常使用的数据结构为数组或链表,两种数据结构查找的效率相当(因为链条的元素一般不会太多)。
方案二:开放地址法;
开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。
根据探测空白单元格位置方式的不同,可分为三种方法:
线性探测
二次探测
再哈希法
5.哈希表常见操作
put(key,value):插入或修改操作;
get(key):获取哈希表中特定位置的元素;
remove(key):删除哈希表中特定位置的元素;
isEmpty():如果哈希表中不包含任何元素,返回trun,如果哈希表长度大于0则返回false;
size():返回哈希表包含的元素个数;
resize(value):对哈希表进行扩容操作;
6.哈希表的实现
function hashTable(){
// 属性
this.storage = []; //最外层使用数组实现哈希表
this.count = 0; //哈希表的数据的个数,总长度中存在空数据
this.limit = 7; //哈希表的总长度,初始让其等于7,时一个质数
// 设置哈希函数
hashTable.prototype.hashFunc = function (str,size){
// 1.定义hashCode变量
let hashCode = 0;
// 2.霍纳法则,计算hashCode的值
// 让hashCode乘一个质数(7),是下标尽量均匀分布
for(let i=0;i<str.length;i++){
hashCode = hashCode * 7 + str.charCodeAt(i);
}
// 取余操作
let index = hashCode % size;
return index;
}
// 插入和修改操作
hashTable.prototype.put = function(key,value){
//1.使用哈希函数找到数组下标
let index = this.hashFunc(key,this.limit);
// 2.bucket是桶的意思,这里是数组,也可以用链表实现,取出index对应的数组
let bucket = this.storage[index];
// 3. 判断桶是否为空,为空的话就要添加数据
if(bucket == null){
bucket = [];
this.storage[index] = bucket;
}
// 4.遍历桶中的数据,桶中的数据仍是一个个数组
for(let i=0;i<bucket.length;i++){
// tuple仍是一个数组
let tuple = bucket[i];
if(tuple[0] == key){
tuple[1]=value;
return true;
}
}
// 5.如果没有找到,那就要添加了
bucket.push([key,value]);
this.count++;
// 6.判断是否需要扩容
if(this.count > this.limit * 0.75){
let newSize = this.limit * 2;
let newData = this.getPrime(newSize);
this.resize(newData);
}
return true;
}
hashTable.prototype.get = function(key){
let index = this.hashFunc(key,this.limit);
let bucket = this.storage[index];
// 2.1如果为空就直接返回
if(bucket == null) return null;
// 2.2不为空就遍历
for(let i=0;i<bucket.length;i++){
let tuple = bucket[i];
if(tuple[0] == key){
return tuple[1];
}
}
// 2.3如果没有找到
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++){
let tuple = bucket[i];
if(tuple[0] == key){
bucket.splice(i,1);
this.count--;
// // 缩小容量
if(this.limit > 7 && this.count < this.limit * 0.25){
let newSize = Math.floor(this.limit / 2);
let newData = this.getPrime(newSize);
this.resize(newData);
}
return tuple[1];
}
}
return null;
}
hashTable.prototype.isEmpty = function(){
return this.count == 0;
}
hashTable.prototype.size = function(){
return this.count;
}
// 扩容操作或缩容操作
hashTable.prototype.resize = function(newLimit){
// 1.保存旧的storage数组内容
let oldStorage = this.storage;
// 2.重置所有属性
this.storage = [];
this.count = 0;
this.limit = newLimit;
// 3.遍历oldStorage中所有bucket
for(let i=0;i<oldStorage.length;i++){
// 3.1取出对应的bucket
let bucket = oldStorage[i];
// 3.2如果为空就返回
if(bucket== null) continue;
// 3.3有数据就取出数据重新插入
for(let j=0;j<bucket.length;j++){
let tuple = bucket[j];
this.put(tuple[0],tuple[1]);
}
}
}
// 判断某个数字是否是质数
hashTable.prototype.isPrime = function(num){
// 1.获取num的平方跟
var temp = parseInt(Math.sqrt(num));
console.log('temp',temp);
if(temp <=1) return false;
// 2.循环判断
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;
}
}
上述定义的哈希表的resize方法,既可以实现哈希表的扩容,也可以实现哈希表容量的压缩。
装填因子 = 哈希表中数据 / 哈希表长度,即 loadFactor = count / HashTable.length。
通常情况下当装填因子laodFactor > 0.75时,对哈希表进行扩容。在哈希表中的添加方法(push方法)中添加如下代码,判断是否需要调用扩容函数进行扩容:
//判断是否需要扩容操作
if(this.count > this.limit * 0.75){
this.resize(this.limit * 2)
}
当装填因子laodFactor < 0.25时,对哈希表容量进行压缩。在哈希表中的删除方法(remove方法)中添加如下代码,判断是否需要调用扩容函数进行压缩:
//缩小容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2))
}
测试代码:
let ht = new hashTable();
console.log(ht.put('qd','backetball'));
console.log(ht.put('kbhm','football'));
console.log(ht.put('yn','pingpong'));
console.log(ht.put('aaa','123'));
console.log(ht.get('aaa'));
console.log(ht.put('aaa','charmer'));
console.log(ht.get('aaa'));
console.log(ht.get('aaaa'));
console.log(ht.remove('aaa'));
console.log(ht);