数据结构(四)
说明:本文基于哔哩哔哩视频【JavaScript数据结构与算法】整理
哈希表(hash)
-
它的结构是数组,是通过一种哈希函数将 hashcode,转化成数组的下标
-
特性:相对于数组,优点:插入、查询和删除的操作,效率非常高;缺点:无序,不能重复,空间利用率不高;
-
相关概念
-
哈希化:将很大的数字转化成数组范围内下标的过程(取余操作);
-
哈希函数:将单词转化为大数字hashcode(幂的连乘相加,避免重复),大数字再进行哈希化的代码,封装在一个函数里;
-
优秀哈希函数的目标:
- 快速,(霍纳法则:幂的连乘相加,多项式的优化):Pn(x)= anx n+a(n-1)x(n-1)+…+a1x+a0=((…(((anx +an-1)x+an-2)x+ an-3)…)x+a1)x+a0
- 下标均匀分布:在使用常量的地方, 尽量使用质数,比如 哈希表的长度,N次幂的底数(我们之前使用的是27)
-
哈希表:最终将数据插入到这个数组,对整个结构的封装,称为一个哈希表
-
填装因子:总数据项 和 哈希表的总长的比值。填装因子越小,存储效率越低,探测速度越快
-
注意事项
- 取余操作也有余数重复的时候,但是哈希表不能重复,解决这个冲突的方法:
- 哈希化的效率排名:1️⃣2️⃣3️⃣4️⃣
- 链地址法(拉链法):哈希化之后的数组单元中存储一个数组或者是链表(根据业务需求:查找用数组,其他用链表);先通过数组下标找到所在位置之后,再在链表或者数组中进行操作。1️⃣
- 开放地址法:寻找空白的单元格来添加新元素,寻找的方法有:
- 二次查找(二分查找),在线性查找的基础上对步长做了处理;x,x+12,x+22 2️⃣
- 线性查找,删除的时候不能将下标设置为null,因为会被特殊处理,可以成-1;x,x+1,x+2 4️⃣
- 再哈希法;依赖关键字,将关键字做哈希化之后作为 前面哈希函数的步长;哈希化结果和第一次的不一样,不能为0.【 stepSize=constant-(key % constant) , 永远不会为0】 3️⃣
-
-
实现方式:基于数组实现()
-
常见操作
- put 插入或者修改
- get
- remove
- 容量质数化
- 其他:isEmpty,size
相关操作的代码:
- 哈希函数
// 哈希函数:将名称 key 转化为下标 值的函数
// 转化为较大数字:hashcode
// 将hashcode 转到数组范围(大小)内
function hashFunc(str, size) {
var hashcode = 0;
for (let i = 0; i < str.length; i++) {
// 底数为质数 31,37等
hashcode = 37 * hashcode + str.charCodeAt(i);
console.log(hashcode, str[i], i);
}
index = hashcode % 7;
return index
}
// console.log('aa'.charCodeAt(0));
// hashFunc('cc', 7)
console.log(hashFunc('22', 11)); //4
console.log(hashFunc('32', 11)); // 1
console.log(hashFunc('34', 11)); // 2
- 判断质数
// 高效判断 :开平方根 效率较高
// 一个数,进行因数分解之后的两个数,一个肯定比开平方根之后的数小,一个比开平方根大。既然平方根小的都没被整除,大的也不可能别整出
// 16= 2 * 8 ,4*4 = 16 1-4之间的数字有一个被整除,则不是质数
// 17=1*17 ,4.12 * 4.12 ==17 4.12 之前没有数字 能整除,所以是质数
function isPrimeSqrt(num) {
if (num == 0 || num == 1) {
return false
}
const temp = parseInt(Math.sqrt(num));
for (let i = 2; i <= temp; i++) {
if (num % 2 == 0) {
return false;
}
}
return true;
}
- 哈希表
// 哈希表
// 使用 链地址法 做哈希化
// 表结构最终的解构 [[[key,val],[key,val]],[key,val],[key,val],[key,val],[key,val],]
function HashTable() {
// 属性
this.storage = []; //存储空间
this.count = 0; // 记录当前数据 长度
this.limit = 3; //表的容量 最好为质数
// 使容量恒为 质数 :双倍扩容之后 ,肯定不是 质数,可以给二倍数每次加一,进行判断是不是质数
this.loadFactor = this.count / this.limit; //填充因子 > 0.75要扩容 一倍
// 方法
// 哈希函数
HashTable.prototype.hashFunc = function(str, size) {
var hashcode = 0;
// 霍纳法则,转化为很大数,hashcode
for (var i = 0; i < str.length; i++) {
// 37 ,31,41等可供选择
hashcode = 37 * hashcode + str.charCodeAt(i);
// console.log(hashcode, str[i], i);
}
// 取余,返回下标
index = hashcode % size;
return index;
};
// 插入、修改数据
// 参数:key:比如是一个名字,value:比如是这个名字对应的身份信息
HashTable.prototype.put = function(key, value) {
// 根据key找到 索引所在的位置,
var index = this.hashFunc(key, this.limit);
// 根据索引找到该位置放置的元素 buket,这个元素应该是一个数组
var bucket = this.storage[index];
//判断bucket 是否为null,为null则创建, 将元素赋值为数组,再将这个数组元素放置到这个位置
if (bucket === undefined) {
bucket = [];
this.storage[index] = bucket;
}
// console.log(bucket, this.count);
// 判断bucket 里面是不是存在这个key,
// 存在则修改
var override = false;
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i];
if (tuple[0] == key) {
tuple[1] = value;
override = true;
return;
}
}
// 不存在则添加
if (!override) {
bucket.push([key, value]);
this.count++;
// 扩容
if (this.count > this.limit * 0.75) {
var size = this.limit * 2;
var newPrimeSize = this.turnPrime(size);
this.resize(newPrimeSize);
}
}
};
// 获取
HashTable.prototype.get = function(key) {
var index = this.hashFunc(key, this.limit);
var bucket = this.storage[index];
if (bucket == null || bucket == undefined) {
return null;
}
// 线性查找 bucket
if (bucket) {
for (var i = 0; i < bucket.length; i++) {
const tuple = bucket[i];
if (tuple[0] === key) {
return tuple[1];
}
}
return null;
}
};
// 删除
HashTable.prototype.remove = function(key) {
var index = this.hashFunc(key, this.limit);
var bucket = this.storage[index];
if (bucket == null) {
return null;
}
for (var i = 0; i < bucket.length; i++) {
const tuple = bucket[i];
if (tuple[0] == key) {
bucket.splice(i, 1);
this.count -= 1;
// 缩小容量
// 避免太小
if (this.limit > 7 && this.count < this.limit * 0.25) {
var size = Math.floor(this.limit / 2);
var newPrimeSize = this.turnPrime(size);
this.resize(newPrimeSize);
}
return tuple[1];
}
}
return null;
};
// isEmpty,size
HashTable.prototype.isEmpty = function() {
return this.count == 0;
};
HashTable.prototype.size = function() {
return this.count;
};
// 哈希表扩容
HashTable.prototype.resize = function(newlimit) {
var oldStorage = this.storage;
this.storage = [];
this.count = 0;
this.limit = newlimit;
for (let i = 0; i < oldStorage.length; i++) {
const bucket = oldStorage[i];
if (bucket == null) {
continue;
}
for (let j = 0; j < bucket.length; j++) {
const tuple = bucket[j];
this.put(tuple[0], tuple[1]);
}
}
};
// 判断是不是质数
HashTable.prototype.isPrime = function(num) {
if (num == 0 || num == 1) {
return false;
}
const temp = parseInt(Math.sqrt(num));
for (let i = 2; i <= temp; i++) {
if (num % 2 == 0) {
return false;
}
}
return true;
}
// 转化为质数
HashTable.prototype.turnPrime = function(num) {
// 给 num 每次加一,直到找到质数、
while (!this.isPrime(num)) {
num++;
}
return num;
};
}
const tabs = new HashTable();
tabs.put("age", 12);
tabs.put("name", "wei");
tabs.put("sex", "man");
tabs.put("sex1", "man");
console.log(tabs.get("age"), "table");
console.log(tabs.limit, 'limit');
tabs.put("name", "sss");
console.log(tabs.get("name"), "修改");
//
tabs.remove("name");
console.log(tabs.get("name"), "remove");
其他数据结构可访问以下地址:
数据结构与算法(一)之数组,队列,栈(偏向JS)
数据结构与算法(二)之单向链表和双向链表(偏向JS)
数据结构与算法(三)之集合,字典