数据结构与算法
1、哈希表
哈希表的结构就是数组,它神奇的地方在对于下标值的一种变换,这种变换我们称之为哈希函数,通常哈希函数可以获取到HashCode
哈希表通常是基于数组实现的,但是相对数组,有很多优势
- 1、它可以提供非常快速的插入--删除--查找操作
- 2、无论多少数据,插入和删除仅需要接近常量时间O(1)的时间级
- 3、哈希表的速度比树还快,基本可以瞬间查找到想要的元素 哈希表相对不足的地方
- 1、哈希表中的数据是没有顺序的,所以不能以一种固定的方式来遍历其中的元素
- 2、哈希表的key不允许重复
- 3、空间利用率不高,某些单元是没有被利用的
- 4、不能快速的查找出哈希表中的最大值和最小值这些特殊的值
哈希表的的原理
- 1、哈希化:将大数字转化成数组范围内下标的过程,我们称之为哈希化
- 2、哈希函数:通常我们将单词转化为大数字,大数字在进行哈希化的代码实现放在一个函数 中,这个函数我们称之为哈希函数
- 3、哈希表:最终将数据插入到这个数组,对整个结构的封装,我们称之为是一个哈希表
哈希表使用范例
将0~199的数字选取5个放入长度为10的单元格中。如果我们所及选出来的是33,82,11,45,90。通过取除以10的余数的方式,他们最终的下标值为3-2-1-5-0,没发生冲突。但是如果将90替换成73呢,机会发生下标值冲突。冲突的解决方案如下:
- 1、链地址法(实操推荐使用)
- 从图中我们可以看出,链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一个链条
- 这个链条常用的数据结构是数组或者链表
- 比如是数组,也就是每个数组单元总存储这一个数组,一旦发现重复,将重复的元素插入到数组的首端或者末端
- 当查询时,先根据哈希化的下标值找到对应位置,再取出数组,依次查询找到数据 - 2、开放地址发
开放地址发的主要工作方式是寻找空白的单元格来添加重复的数据,方式有一下三种
线性探测法
- 插入操作:当发现插入的位置已经有元素了,则进行index位置+1开始进行查找空的位置进行插入
- 查询操作:通过哈希化获取index值,通过index值找到元素进行对比,如果元素不合符,则进行index位置+1进行依次查找,若index+1的位置为空,则代表没有该元素
- 删除操作:删除操作与插入和查询操作类似,但是删除后该该位置的值不能设置为null,为了防止影响查询操作,例如可以设置成-
- 问题:比较严重的问题,聚集。比如:在没有任何数据时,插入22-23-24-25-26,那么意味着下标2-3-4-5-6位置都有元素,当插入32时,会发现连续的单元都不允许插入数据。会影响哈希表性能,无论是插入、查询、删除
二次探测法:与线性探测法原理一样,优化了探测时的步长。
- 优点:线性探测法的步长为x+1,x+2依次探测,二次探测法为x+1²,x+2²,这样一性探测的距离比较长,避免聚集带来的问题
- 问题:也会造成步长不一的一种聚集,但是相比连续数字的概率会小一些
再哈希法:把关键字用另一个哈希函数,再做一次哈希化,用这次哈希化的结果作为步长
- 特点:和第一个哈希函数不同,不能输出为0(否则每次探测都在原地踏步)
- 哈希函数: stepSize = constant - (key % constant).constant是质数,且小于数组的容量。例如 stepSize = 5 - (key % 5)
优秀的哈希函数特点:快速计算(霍纳法则),均匀分布(初始长度为质数)
哈希函数
//1、将字符串抓成比较大的数字:hashCode
// 2、将大的数字hashCode压缩到数组范围(大小)之内
function hashFunc(str,size){
// 1、定义hashCode变量
let hashCode = 0
// 2、霍纳法则,来计算hashCode的值
// cats -> Unicode编码
for(let i=0; i<str.length; i++){
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3、取余操作
let index = hashCode % size
return index
}
哈希表封装
function HashTabel() {
//属性
this.stroage = []
this.count = 0
this.limit = 7 //数组的长度,质数
//方法
//哈希函数
HashTabel.prototype.hashFunc = function (str, size) {
// 1、定义hashCode变量
let hashCode = 0
// 2、霍纳法则,来计算hashCode的值
// cats -> Unicode编码
for (let i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3、取余操作
let index = hashCode % size
return index
}
//插入&修改操作
HashTabel.prototype.put = function (key, value) {
// 1、根据key获取对应的index
let index = this.hashFunc(key, this.limit)
// 2、根据index取出对应的bucket
let bucket = this.stroage[index]
// 3、判断bucket是否为null
if (bucket == null) {
bucket = []
this.stroage[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)
}
}
//获取操作
HashTabel.prototype.get = function(key){
// 1、根据key获取对应的index
let index = this.hashFunc(key, this.limit)
// 2、根据index取出对应的bucket
let bucket = this.stroage[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){
return tuple[1]
}
}
// 5、依然没有找到数据,返回null
return null
}
// 删除操作
HashTabel.prototype.remove = function(key){
// 1、根据key获取对应的index
let index = this.hashFunc(key, this.limit)
// 2、根据index取出对应的bucket
let bucket = this.stroage[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--
//缩小容量
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)
}
return tuple[1]
}
}
// 5、依然没有找到数据,返回null
return null
}
//其他方法
//判断哈希表是否为空
HashTabel.prototype.isEmpty = function(){
return this.count == 0
}
//哈希表的元素个数
HashTabel.prototype.size = function(){
return this.count
}
//哈希表的扩容
HashTabel.prototype.resize = function(newLimit){
// 1、保存旧的数组内容
let oldStorage = this.stroage
// 2、重置所有属性
this.stroage = []
this.count = 0
this.limit = newLimit
// 3、遍历oldStorage中所有的bucke
for(let i=0; i<oldStorage.length; i++){
// 3.1取出对应的bucket
let bucket = oldStorage[i]
// 3.2判断bucket是否为null
if(bucket == null){
continue
}
// 3.3bucket有数据,取出数据重新插入
for(let i=0; i<bucket.length; i++){
let tuple = bucket[i]
this.put(tuple[0],tuple[1])
}
}
}
//判断某个数字是否为质数
HashTabel.prototype.isPrime =function(num){
//求num的开平方根
let temp = parseInt(Math.sqrt(num))
//循环判断
for(let i=2; i <= temp; i++){
if(num % i == 0 ){
return false
}
}
return true
}
//获取质数方法
HashTabel.prototype.getPrime = function(num){
while(!this.isPrime(num)){
num++
}
return num
}
}
哈希表的扩容思想(根据哈希表封装代码)
- 1、目前,我们是将所有数据项放入长度为7的数组中的
- 2、因为我们使用的是链地址法,loadFactor可以大于1,所以这个哈希表可以无限制的插入数据
- 3、但是随着数据量的增多,每一个index对应的bucket会越来越长,也就造成效率的降低,
- 4、所以,在合适的位置对数组进行扩容,例如扩容两倍 如何进行扩容
- 1、扩容可以简单的将容量增大两倍后的质数
- 2、扩容的时候,所有数据项一定同时进行修改(重新调用哈希函数,来获取不同的位置)。例如Hashcode=12的数据项,在length=8时,index=4,在长度为16的时候呢?index=12
- 3、扩容是个耗时的过程,但是如果数组需要扩容,这个耗时是必要的
- 4、在laodFactor > 0.75的时候进行扩容