文章目录
- 一、哈希表简介
- 1.1.认识哈希表
- 1.2.哈希化的方式
- 1.3.解决冲突的方法
- 1.4.寻找空白单元格的方式
- 线性探测
- 二次探测
- 再哈希化
- 1.5.不同探测方式性能的比较
- 1.6.优秀的哈希函数
- 快速计算
- 均匀分布
- 二、初步封装哈希表
- 2.1.哈希函数的简单实现
- 2.2.创建哈希表
- 2.3.put(key,value)
- 2.4.get(key)
- 2.5.remove(key)
- 2.6.其他方法的实现
- 三、哈希表的扩容
- 3.1.扩容与压缩
- 3.2.选择质数作为容量
- 四、哈希表的完整实现
哈希表通常是基于数组实现的,但是相对于数组,它存在更多优势:
哈希表同样存在不足之处:
哈希表是什么?
通过以下案例了解哈希表:
也就是说:哈希表最后还是基于数据来实现的,只不过哈希表能够通过哈希函数把字符串转化为对应的下标值,建立字符串和下标值的对应关系。
为了把字符串转化为对应的下标值,需要有一套编码系统,为了方便理解我们创建这样一套编码系统:比如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这样的单词),造成了数组空间的浪费。
两种方案总结:
现在需要一种压缩方法,把幂的连乘方案系统中得到的巨大整数范围压缩到可接受的数组范围中。可以通过取余操作来实现。虽然取余操作得到的结构也有可能重复,但是可以通过其他方式解决。
哈希表的一些概念:
仍然需要解决的问题:
解决冲突常见的两种方案:
如下图所示,我们将每一个数字都对10进行取余操作,则余数的范围0~9作为数组的下标值。并且,数组每一个下标值对应的位置存储的不再是一个数字了,而是存储由经过取余操作后得到相同余数的数字组成的数组或链表。
这样可以根据下标值获取到整个数组或链表,之后继续在数组或链表中查找就可以了。而且,产生冲突的元素一般不会太多。
总结:链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一条链条,这条链条常使用的数据结构为数组或链表,两种数据结构查找的效率相当(因为链条的元素一般不会太多)。
开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。
根据探测空白单元格位置方式的不同,可分为三种方法:
当插入13时:
当查询13时:
当删除13时:
线性探测存在的问题:
上文所说的线性探测存在的问题:
如果之前的数据是连续插入的,那么新插入的一个数据可能需要探测很长的距离;
二次探测是在线性探测的基础上进行了优化:
线性探测:我们可以看成是步长为1的探测,比如从下表值x开始,那么线性探测就是按照下标值:x+1、x+2、x+3等依次探测;
二次探测:对步长进行了优化,比如从下标值x开始探测:x+12、x+22、x+33 。这样一次性探测比较长的距离,避免了数据聚集带来的影响。
二次探测存在的问题:
在开放地址法中寻找空白单元格的最好的解决方式为再哈希化:
第二次哈希化需要满足以下两点:
优秀的哈希函数:
哈希化的效率
哈希表中执行插入和搜索操作效率是非常高的。
理解概念装填因子:
可以看到,随着装填因子的增大,平均探测长度呈指数形式增长,性能较差。实际情况中,最好的装填因子取决于存储效率和速度之间的平衡,随着装填因子变小,存储效率下降,而速度上升。
二次探测和再哈希法性能相当,它们的性能比线性探测略好。由下图可知,随着装填因子的变大,平均探测长度呈指数形式增长,需要探测的次数也呈指数形式增长,性能不高。
可以看到随着装填因子的增加,平均探测长度呈线性增长,较为平缓。在开发中使用链地址法较多,比如Java中的HashMap中使用的就是链地址法。
哈希表的优势在于它的速度,所以哈希函数不能采用消耗性能较高的复杂算法。提高速度的一个方法是在哈希函数中尽量减少乘法和除法。
性能高的哈希函数应具备以下两个优点:
霍纳法则:在中国霍纳法则也叫做秦久韶算法,具体算法为:
求多项式的值时,首先计算最内层括号内一次多项式的值,然后由内向外逐层计算一次多项式的值。这种算法把求n次多项式f(x)的值就转化为求n个一次多项式的值。
变换之前:
变换之后:
如果使用大O表示时间复杂度的话,直接从变换前的O(N2)降到了O(N)。
为了保证数据在哈希表中均匀分布,当我们需要使用常量的地方,尽量使用质数;比如:哈希表的长度、N次幂的底数等。
Java中的HashMap采用的是链地址法,哈希化采用的是公式为:index = HashCode(key)&(Length-1)
即将数据化为二进制进行与运算,而不是取余运算。这样计算机直接运算二进制数据,效率更高。但是JavaScript在进行叫大数据的与运算时会出现问题,所以以下使用JavaScript实现哈希化时还是采用取余运算。
哈希表的常见操作为:
首先使用霍纳法则计算hashCode的值,通过取余操作实现哈希化,此处先简单地指定数组的大小。
//设计哈希函数
//1.将字符串转成比较大的数字:hashCede
//2.将大的数字hasCode压缩到数组范围(大小)之内
function hashFunc(str, size){
//1.定义hashCode变量
let hashCode = 0
//2.霍纳法则,计算hashCode的值
//cats -> Unicode编码
for(let i = 0 ;i < str.length; i++){
// str.charCodeAt(i)//获取某个字符对应的unicode编码
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//3.取余操作
let index = hashCode % size
return index
}
测试代码:
//测试哈希函数
console.log(hashFunc('123', 7));
console.log(hashFunc('NBA', 7));
console.log(hashFunc('CBA', 7));
console.log(hashFunc('CMF', 7));
测试结果:
封装哈希表的数组结构模型:
首先创建哈希表类HashTable,并添加必要的属性和上面实现的哈希函数,再进行其他方法的实现。
//封装哈希表类
function HashTable() {
//属性
this.storage = []
this.count = 0//计算已经存储的元素个数
//装填因子:loadFactor > 0.75时需要扩容;loadFactor < 0.25时需要减少容量
this.limit = 7//初始长度
//方法
//哈希函数
HashTable.prototype.hashFunc = function(str, size){
//1.定义hashCode变量
let hashCode = 0
//2.霍纳法则,计算hashCode的值
//cats -> Unicode编码
for(let i = 0 ;i < str.length; i++){
// str.charCodeAt(i)//获取某个字符对应的unicode编码
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//3.取余操作
let index = hashCode % size
return index
}
哈希表的插入和修改操作是同一个函数:因为,当使用者传入一个
实现思路:
代码实现:
//插入&修改操作
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是否为null
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
//4.判断是否是修改数据
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
if (tuple[0] == key) {
tuple[1] = value
return//不用返回值
}
}
//5.进行添加操作
bucket.push([key, value])
this.count += 1
}
测试代码:
//测试哈希表
//1.创建哈希表
let ht = new HashTable()
//2.插入数据
ht.put('class1','Tom')
ht.put('class2','Mary')
ht.put('class3','Gogo')
ht.put('class4','Tony')
ht.put('class4', 'Vibi')
console.log(ht);
测试结果:
实现思路:
//获取操作
HashTable.prototype.get = function(key){
//1.根据key获取对应的index
let index = this.hashFunc(key, this.limit)
//2.根据index获取对应的bucket
let bucket = this.storage[index]
//3.判断bucket是否等于null
if (bucket == null) {
return null
}
//4.有bucket,那么就进行线性查找
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
if (tuple[0] == key) {//tuple[0]存储key,tuple[1]存储value
return tuple[1]
}
}
//5.依然没有找到,那么返回null
return null
}
测试代码:
//测试哈希表
//1.创建哈希表
let ht = new HashTable()
//2.插入数据
ht.put('class1','Tom')
ht.put('class2','Mary')
ht.put('class3','Gogo')
ht.put('class4','Tony')
//3.获取数据
console.log(ht.get('class3'));
console.log(ht.get('class2'));
console.log(ht.get('class1'));
测试结果:
实现思路:
代码实现:
//删除操作
HashTable.prototype.remove = function(key){
//1.根据key获取对应的index
let index = this.hashFunc(key, this.limit)
//2.根据index获取对应的bucket
let bucket = this.storage[index]
//3.判断bucket是否为null
if (bucket == null) {
return null
}
//4.有bucket,那么就进行线性查找并删除
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]
}
}
//5.依然没有找到,返回null
return null
}
测试代码:
//测试哈希表
//1.创建哈希表
let ht = new HashTable()
//2.插入数据
ht.put('class1','Tom')
ht.put('class2','Mary')
ht.put('class3','Gogo')
ht.put('class4','Tony')
//3.删除数据
console.log( ht.remove('class2'));
console.log(ht.get('class2'));
测试结果:
其他方法包括:isEmpty()、size():
代码实现:
//判断哈希表是否为null
HashTable.prototype.isEmpty = function(){
return this.count == 0
}
//获取哈希表中元素的个数
HashTable.prototype.size = function(){
return this.count
}
测试代码:
//测试哈希表
//1.创建哈希表
let ht = new HashTable()
//2.插入数据
ht.put('class1','Tom')
ht.put('class2','Mary')
ht.put('class3','Gogo')
ht.put('class4','Tony')
//3.测试isEmpty()
console.log(ht.isEmpty());
//4.测试isEmpty()
console.log(ht.size());
console.log(ht);
测试结果:
为什么需要扩容?
什么情况下需要扩容?
如何进行扩容?
实现思路:
代码实现:
//哈希表扩容
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
const bucket = oldStorage[i];
//3.2.判断bucket是否为null
if (bucket == null) {
continue
}
//3.3.bucket中有数据,就取出数据重新插入
for (let j = 0; j < bucket.length; j++) {
const tuple = bucket[j];
this.put(tuple[0], tuple[1])//插入数据的key和value
}
}
}
上述定义的哈希表的resize方法,既可以实现哈希表的扩容,也可以实现哈希表容量的压缩。
装填因子 = 哈希表中数据 / 哈希表长度,即 loadFactor = count / HashTable.length。
//判断是否需要扩容操作
if(this.count > this.limit * 0.75){
this.resize(this.limit * 2)
}
//缩小容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2))
}
质数的判断
首先我们来复习一下,判断质数的方法:
注意1不是质数
function isPrime(num){
if(num <= 1 ){
return false
}
for(let i = 2; i <= num - 1; i++){
if(num % i ==0){
return false
}
}
return true
}
这种方法虽然能实现质数的判断,但是效率不高。
function isPrime(num){
if (num <= 1) {
return false
}
//1.获取num的平方根:Math.sqrt(num)
//2.循环判断
for(var i = 2; i<= Math.sqrt(num); i++ ){
if(num % i == 0){
return false;
}
}
return true;
}
实现扩容后的哈希表容量为质数
实现思路:
2倍扩容之后,通过循环调用isPrime判断得到的容量是否为质数,不是则+1,直到是为止。比如原长度:7,2倍扩容后长度为14,14不是质数,14 + 1 = 15不是质数,15 + 1 = 16不是质数,16 + 1 = 17是质数,停止循环,由此得到质数17。
代码实现:
//判断传入的num是否质数
HashTable.prototype.isPrime = function(num){
if (num <= 1) {
return false
}
//1.获取num的平方根:Math.sqrt(num)
//2.循环判断
for(var i = 2; i<= Math.sqrt(num); i++ ){
if(num % i == 0){
return false;
}
}
return true;
}
//获取质数的方法
HashTable.prototype.getPrime = function(num){
//7*2=14,+1=15,+1=16,+1=17(质数)
while (!this.isPrime(num)) {
num++
}
return num
}
在put方法中添加如下代码:
//判断是否需要扩容操作
if(this.count > this.limit * 0.75){
let newSize = this.limit * 2
let newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
在remove方法中添加如下代码:
//缩小容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
let newSize = Math.floor(this.limit / 2)
let newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
测试代码:
let ht = new HashTable()
ht.put('class1','Tom')
ht.put('class2','Mary')
ht.put('class3','Gogo')
ht.put('class4','Tony')
ht.put('class5','5')
ht.put('class6','6')
ht.put('class7','7')
ht.put('class8','8')
ht.put('class9','9')
ht.put('class10','10')
console.log(ht.size());//10
console.log(ht.limit);//17
测试结果:
//封装哈希表类
function HashTable() {
//属性
this.storage = []
this.count = 0//计算已经存储的元素个数
//装填因子:loadFactor > 0.75时需要扩容;loadFactor < 0.25时需要减少容量
this.limit = 7//初始长度
//方法
//哈希函数
HashTable.prototype.hashFunc = function(str, size){
//1.定义hashCode变量
let hashCode = 0
//2.霍纳法则,计算hashCode的值
//cats -> Unicode编码
for(let i = 0 ;i < str.length; i++){
// str.charCodeAt(i)//获取某个字符对应的unicode编码
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//3.取余操作
let index = hashCode % size
return index
}
//一.插入&修改操作
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是否为null
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
//4.判断是否是修改数据
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
if (tuple[0] == key) {
tuple[1] = value
return//不用返回值
}
}
//5.进行添加操作
bucket.push([key, value])
this.count += 1
//6.判断是否需要扩容操作
if(this.count > this.limit * 0.75){
let newSize = this.limit * 2
let newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
}
//二.获取操作
HashTable.prototype.get = function(key){
//1.根据key获取对应的index
let index = this.hashFunc(key, this.limit)
//2.根据index获取对应的bucket
let bucket = this.storage[index]
//3.判断bucket是否等于null
if (bucket == null) {
return null
}
//4.有bucket,那么就进行线性查找
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
if (tuple[0] == key) {//tuple[0]存储key,tuple[1]存储value
return tuple[1]
}
}
//5.依然没有找到,那么返回null
return null
}
//三.删除操作
HashTable.prototype.remove = function(key){
//1.根据key获取对应的index
let index = this.hashFunc(key, this.limit)
//2.根据index获取对应的bucket
let bucket = this.storage[index]
//3.判断bucket是否为null
if (bucket == null) {
return null
}
//4.有bucket,那么就进行线性查找并删除
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]
//6.缩小容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
let newSize = Math.floor(this.limit / 2)
let newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
}
}
//5.依然没有找到,返回null
return null
}
/*------------------其他方法--------------------*/
//判断哈希表是否为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
const bucket = oldStorage[i];
//3.2.判断bucket是否为null
if (bucket == null) {
continue
}
//3.3.bucket中有数据,就取出数据重新插入
for (let j = 0; j < bucket.length; j++) {
const tuple = bucket[j];
this.put(tuple[0], tuple[1])//插入数据的key和value
}
}
}
//判断传入的num是否质数
HashTable.prototype.isPrime = function(num){
if (num <= 1) {
return false
}
//1.获取num的平方根:Math.sqrt(num)
//2.循环判断
for(var i = 2; i<= Math.sqrt(num); i++ ){
if(num % i == 0){
return false;
}
}
return true;
}
//获取质数的方法
HashTable.prototype.getPrime = function(num){
//7*2=14,+1=15,+1=16,+1=17(质数)
while (!this.isPrime(num)) {
num++
}
return num
}
}