JavaScript数据结构与算法(3)哈希表

231 阅读4分钟

数据结构与算法

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、链地址法(实操推荐使用)

image.png

  1. 从图中我们可以看出,链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一个链条
  2. 这个链条常用的数据结构是数组或者链表
  3. 比如是数组,也就是每个数组单元总存储这一个数组,一旦发现重复,将重复的元素插入数组的首端或者末端
  4. 当查询时,先根据哈希化的下标值找到对应位置,再取出数组,依次查询找到数据 - 2、开放地址发

image.png 开放地址发的主要工作方式是寻找空白的单元格来添加重复的数据,方式有一下三种

线性探测法

  1. 插入操作:当发现插入的位置已经有元素了,则进行index位置+1开始进行查找空的位置进行插入
  2. 查询操作:通过哈希化获取index值,通过index值找到元素进行对比,如果元素不合符,则进行index位置+1进行依次查找,若index+1的位置为空,则代表没有该元素
  3. 删除操作:删除操作与插入和查询操作类似,但是删除后该该位置的值不能设置为null,为了防止影响查询操作,例如可以设置成-
  4. 问题:比较严重的问题,聚集。比如:在没有任何数据时,插入22-23-24-25-26,那么意味着下标2-3-4-5-6位置都有元素,当插入32时,会发现连续的单元都不允许插入数据。会影响哈希表性能,无论是插入、查询、删除

二次探测法:与线性探测法原理一样,优化了探测时的步长

  1. 优点:线性探测法的步长为x+1,x+2依次探测,二次探测法为x+1²,x+2²,这样一性探测的距离比较长,避免聚集带来的问题
  2. 问题:也会造成步长不一的一种聚集,但是相比连续数字的概率会小一些

再哈希法:把关键字用另一个哈希函数,再做一次哈希化,用这次哈希化的结果作为步长

  1. 特点:和第一个哈希函数不同,不能输出为0(否则每次探测都在原地踏步)
  2. 哈希函数: 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的时候进行扩容