数据结构-教你手写Hash表

博客介绍了哈希表,它是一种查找算法,在JavaScript对象字面量中基于此实现。阐述了计算地址编号的方法,如幂的连乘及哈希函数压缩。还指出冲突不可避免,介绍了开放地址法(包括线性探测、二次探测、再哈希法)和链地址法两种解决冲突的办法,最后提及有完整源码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

哈希表(Hash)又称“散列表”,在JavaScript中对象字面量的实现就是基于哈希表,哈希表是一种算法简单、性能优越的一种查找算法。

以JavaScript对象字面量为例:比如: 'name':'chn' 这一个键值对属性想插入对象中,首先需要根据键name计算出其对应的一个“编号”,而这个编号就是后面需要查找的地址编号,所以这个编号如何获取就比较的重要;当然有很多种办法,比如比较常用的根据字符的ASCII码进行相加得到一个编号;例如 'name' ASCII码相加就是:110+97+109+101=417,这就是name这个属性的地址编号;但是很快就会出现冲突的问题,比如ASCII码相加等于417的字符串不只name这一个吧?那比如下一次想插入一个同样编号为417的键但是键不是name的,就可以直接将name属性值覆盖了,这就不对了吧?

于是为了保证使用ASCII码得出来的地址编号能够唯一,可以采用幂的连乘;比如:name=110*37^3+97*37^2+109*37+101=5708757,得到一个比较大的值,而通过此方法得出来的值基本上是唯一的,那既然唯一性保证了,还差什么呢?差就差在得出来的数字太大了,一是不方便存储,二是很容易造成计算机的“溢出”,于是必须使用一种方法来压缩这个数,将他变成一个较小的数字,而压缩的方法就是哈希函数。

注意:幂的连乘对计算机来说是比较消耗性能的,可以根据霍纳法则,将公共的质数提取出来;

例如:110*37^3+97*37^2+109*37+101 简化成:((110*37+97)*37+109)*37+101

哈希函数:此方法是最常用的构造哈希函数方法,对于基础长度为size的哈希函数公式为:

   f(key) = key % size (size为数组的长度,最好为质数) 

假设数组长度是7,此时上面那个巨大的数5708757 %  7 = 5,得出来一个5就是最终的地址编号,将name存在数组下标为5的地方即可;

再比如:

 我们使用上述公式,将这12个数字依次模除12,得到的下标如表1所示,此时你以为一切都风平浪静的时候,冲突发生了,比如我此时再插入一个key:18,18%12=6,下标为6,但是下标为6的已经存在78了,难不成覆盖?

甚至更极端点:

 这些key模除12得到的下标全部是0,这不仅产生了冲突还产生了聚集。这对后面查找性能来说都是致命打击。

而这一切都是因为模除的size没有选好,上面公式说过了,size最好选取质数,就是这个原因,这也是那些大神多年总结的经验,如果我们将模除的size换成11,那么情况就好太多了:

 所以我们就宁可将数组的长度选择一个质数。

解决冲突

 如上述表3,我们即使选择了质数,但是模除得出来的下标仍然会出现相同的,1就出现了两次,这就说明:冲突不可避免,只能解决冲突。

解决冲突的办法有两种:

一、开放地址法

核心思想就是:当产生了同样的key时,就让第二个key去寻找下一个最近的空地址。

公式为:

f(key)=f(key+step) % size (setp=1,2,3,...,size-1)

 比如:我们插入前五个:12、67、56、16、25

 此时没有发生冲突,但是当我们插入到第六个数37,37%12=1,位置1存在了25,发生冲突,根据开放地址法公式:(37+1) % 12 = 2,即存入2,而且2刚好也是空地址,于是将其插入;但是如果2也名花有主了呢?那就(37+2)%12=3...依次类推,按照这种方法,一个一个去探测下一个地址是否为空称为“线性探测” 

 显然线性探测性能有限,总要一个一个去遍历,属实太麻烦。于是想出来了前后双向遍历:

f(key)=f(key+step) % size (step=1^2,-1^2,2^2,-2^2,3^2,...q^2,-q^2)

 增加平方的运算使得关键字不聚集,这种方法叫做“二次探测”

当然还有“再哈希法”,核心公式就是:

step=Prime-(key% Prime)(Prime为一个小于数组长度的质数)

 当产生了冲突的时候,就在准备一个哈希函数,对key在进行一次哈希求key,将这个key作为下一次探测的步长。

二、链地址法

核心思路是:产生冲突后,不寻炸新地址,而是在该地址后面开辟新地址存储,可以使用数组也可以使用链表。

如图:

 形似拉链,实际上每个地址存储的都是每个链表的头结点或第一个结点的引用。

 

 完整源码:

'use strict'
/*本次案例采用链地址法,所以需要用到链表,
在此之前已经封装过一个链表,在此可以直接导入,
获取链表源码请前往JavaScript数据结构之链表*/

import LinkList from "./test.js"

//封装哈希函数
const HashTable=function(){
	const BASE=37 //定义幂的连乘底数,最好选取质数,一般算法都选取37
	function HashTable(){
		//数组总长-质数
		this.size=7;
		//当前数组个数
		this.count=0;
		//动态分配数组空间
		this.table=new Array(this.size);
	}

	//哈希函数
	HashTable.prototype.Hash=function(key,length){
		//如果key是数字
		if (typeof key === 'number') {
			return key % length;
		}

		let code = 0;

		for (let i = 0; i < key.length; i++) {
			code = code * BASE + key.charCodeAt(i);
		}
		return code % length;
	}

	//插入
	HashTable.prototype.insertElem=function(key,value){
		//根据key获取地址
		const addr=this.Hash(key,this.size);
		//判断该位置是否为空
		const data=this.table[addr];
		if(!data){ //如果为空,尾插法插入
			const newNode=new LinkList();
			this.table[addr]=newNode;
			//规定链表存放的数据域是一个存储键值对数组,0为key,1为value
			newNode.insertListByTail([key,value]);
			this.count++;
			if ((this.count / this.size).toFixed(2) >= 0.75) {
				let newSize = this.size * 2;
				newSize=this.returnPrime(newSize);
				this.extendTable(newSize);
			}
			return;
		}

		/*如果该位置上存在数据
		1.key已经存在,则对key的value进行更新
		2.key不存在,直接尾插
		*/
		let p=this.table[addr].head;

		// while(p){             ---可忽略
		// 	if(p.data[0]===key){ ---可忽略
		// 		p.data[1]=value; ---可忽略
		// 		return;          ---可忽略
		// 	}                    ---可忽略
		// 	p=p.next;            ---可忽略
		// }                     ---可忽略

        //重新用findNode方法封装寻找结点的部分代码
		const res=this.findNode(function(p1){
			if (p1.data[0] === key) {
				p1.data[1] = value;
				return 'update';
			}
		},p);

		if(res==='update')
			return;

		data.insertListByTail([key,value]);
		this.count++;
		//判断填充因子,是否需要扩容
		if((this.count/this.size).toFixed(2)>=0.75){
			let newSize=this.size*2;
			newSize=this.returnPrime(newSize);
			this.extendTable(newSize);
		}
	}

	//以键取值
	HashTable.prototype.getElem=function(key){
		const addr=this.Hash(key,this.size);
		if(!this.table[addr]){
			return null;
		}

		let p=this.table[addr].head;

		// while(p){                 ---可忽略
		// 	if(p.data[0]===key){     ---可忽略
		// 		return p.data[1];    ---可忽略
		// 	}
		// 	p=p.next;                ---可忽略
		// }

		const node=this.findNode(function(p1){
			// console.log(p1);
			if(p1.data[0]===key){
				return p1.data[1];
			}
		},p);

		return node;
	}

	//删除
	HashTable.prototype.deleteElem=function(key){
		const addr=this.Hash(key,this.size);
		if(!this.table[addr]){
			console.log(new Error('不能删除一个不存在的值'));
			return;
		}
		let p=this.table[addr].head;
		let Pos=1;
		while(p){
			if(p.data[0]===key){
				this.table[addr].deleteData(Pos);
				//如果对应的头结点为空,则删除该位置的链表引用
				if(!this.table[addr].head){
					delete this.table[addr];
				}
				this.count--;

				//判断填充因子是否需要将数组减半
				if((this.count/this.size).toFixed(2)<=0.25 && this.size > 7){
					let newSize = Math.ceil(this.size/2);
					newSize=this.returnPrime(newSize);
					this.extendTable(newSize);
				}

				return true;
			}
			Pos++;
			p=p.next;
		}
		
		console.log(new Error('不能删除一个不存在的值'));
		return;
	}

	/**
	 * 工厂方法-查询节点
	 * fn为业务逻辑 @type function
	 * p为结点、Pos为结点位置
	 * */
		HashTable.prototype.findNode = function (fn, p,Pos=1) {
		let res;
		while (p) {
			res=fn.call(this,p);
			if(res){
				return res;
			}
			p=p.next;
		}
	}

	/**重要:
	 * 对基础数组进行扩容
	 * 填充因子=当前数组个数/数组总长
	 * 当填充因子>0.75 此时线性查找效率很低,必须扩容
	 * 当填充因子<0.25 此时如果数组空间很大而实际插入的数据很少就会造成空间浪费,此时必须缩小个数
	 * */
	HashTable.prototype.extendTable=function(size){
		const temp = this.table;
		this.table = [];
		this.count = 0;
		this.size = size;

		temp.forEach(item => {
			let p = item.head;
			while (p) {
				this.insertElem(p.data[0], p.data[1]);
				p = p.next;
			}
		});
	}

	//判断质数
	HashTable.prototype.isPrime=function(data){
		const _data=Math.ceil(Math.sqrt(data));
		for(let i=2;i<=_data;i++){
			if(data%i===0){
				return false;
			}
		}
		return true;
	}

	//返回合适的质数
	HashTable.prototype.returnPrime=function(data){
		while(!this.isPrime(data)){
			data++;
		}
		return data;
	}

	return HashTable;
}();
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值