首先是哈希表的优点:
-
无论数据有多少,处理起来都特别的快
-
能够快速地进行
插入修改元素
、删除元素
、查找元素
等操作 -
代码简单(其实只需要把哈希函数写好,之后的代码就很简单了)
然后再来讲讲哈希表的缺点:
-
哈希表中的数据是没有顺序的
-
数据不允许重复
=============================================================
前面提到了冲突,其含义就是在哈希化以后有几个元素的下标值相同,这就叫做 冲突。 那当两个元素的下标值冲突时,是后一个元素是不是要替换掉前一个元素呢?当然不是!
那么如何解决冲突这个现象呢?一般是有两种方法,即拉链法(链地址法) 和 开放地址法
这种方法是很常用的解决冲突的方法。我们还是拿上面那个例子来说,10本图书通过哈希化以后存入到长度为10的数组当中,难免有几本书的下标值是相同的,那么我们可以将这两个下标值相同的元素存入到一个单独的数组中,然后将该数组存放在他们原本所在数组的下标位置,如图
假设图书1
和 图书2
哈希化后的下标值都为3,那么我们就可以在原数组下标3的位置放一个数组,同时存放这两本图书。因此,无论是查询哪本图书,计算机获得的下标值都是3,然后再对这个位置的数组进行遍历即可获得想要的图书。
这是第一种解决 冲突 的方法,但使用是还是需要考虑数组长度是否合适的,之后会进行讲解。
这种方法简单来说就是当元素下标值发生冲突时,寻找空白的位置插入数据。假设当前下标值为1和3的位置分别已经插入了 图书3
和 图书5
,这时将 图书6
进行哈希化,发现它的下标值也是1,此时与 图书3
发生冲突,那么此时 图书6
就可以找到下一个空着的没插入元素的位置进行插入,如图
其实当发生冲突时,寻找空白的位置也有三种方法,分别是 线性探测 、二次探测 、再哈希法
1. 线性探测
顾名思义,线性探测的意思就是,当某两个元素发生冲突时,将当前索引+1,查看该位置是否为空,是的话就插入数据,否则就继续将索引+1,以此类推……直到插入数据位置。
但这种方法有一个缺点,那就是当数组中连续很长的一个片段都已经插入了数据,此时用线性探测就显得效率没那么高了,因为每次探测的步长都为1,所以这段都已经插入了数据的片段都得进行探测一次,这种现象叫做 聚集。如下图,就是一个典型的聚集现象
图书8
的下标值为1,与 图书3
冲突,然后进行线性探测,依次经过 图书6、5、1、7
都没有发现有空白位置可以插入,直到末尾才找到空白位置插入,这样挺不好的,所以我们可以选用 二次探测 来缓解 聚集 这种现象。
2. 二次探测
二次探测 在线性探测的基础上,将每次探测的步长改为了当前下标值 index + 1²
、index + 2²
、 index + 3²
…… 直到找到空白位置插入元素为止
还是举一个例子来理解一下 二次探测 吧
假如现在已存入 图书3
、图书5
、图书7
,如图
然后此时要存入一个 图书6
,通过哈希化以后求得的下标值为2,与 图书5
冲突了,所以就从索引2的位置向后再移动 1²
个位置,但此时该位置上已存有数据,如下面这个动图演示
所以此时从索引为2的位置向后移动 2²
个位置,此时发现移动后的位置上也已存有数据,所以仍无法插入数据,如下面这个动图演示
因此,我们继续从索引2的位置向后移动 3²
个位置,此时发现,移动后的位置上有空余位置,于是直接在此插入数据,这样一个二次探测的过程就完成了,如下列动图演示
我们可以看到,二次探测 在一定程度上解决了 线性探测 造成的 聚集 问题,但是它却在另一种程度造成了一种聚集,就比如 1²
、2²
、3²
…… n²
上的聚集。所以这种方式还是有点不太好。
3. 再哈希法
再哈希法 就是再将我们传入的值进行一次 哈希化,获得一个新的探测步数 step
,然后按照这个步数进行探测,找到第一个空着的位置插入数据。这在很大的程度上解决了 聚集 的问题。
既然要再进行哈希化获得一个探测的步数,那么这个哈希化的处理过程一定要跟第一次哈希化的处理过程不一样,这样才能确认一个合适的搜索步长,提高查找效率。
这里,我们就不用担心如何写一个不一样的哈希函数了,给大家看一个公认的比较好的哈希函数:step = constant - (key % constant)
其中,constant
是一个自己定的质数常量,且小于数组的容量; key
就是第一次哈希化得到得值。
然后我们再通过这个函数算得的步长来进行查找搜索空位置进行插入即可,这里就不做过多的演示了。
====================================================================
在了解哈希表的扩容之前,我们来了解一个概念,叫做填充因子,它表示的是哈希表中的数据个数与哈希表长度的比值。其决定了哈希表的存取数据所需的时间大小。
当我们用第一种解决冲突的办法——拉链法,填充因子最小为0,最大为无限大,这是因为该方法是通过在数组中的某个位置插入一个数组用来存储互相冲突的元素,因此,只要有可能,哈希表的长度可以很小,然后数据都存储在内置的数组中,这样填充因子就可以无限大了。
那当我们用第二种解决冲突的办法——开放地址法,填充因子最小为0,最大只能为1,这是因为开放地址法的实现原理是找哈希表中空位置插入元素,因此哈希表中的数据量不会大于哈希表的长度,从而填充因子最大也只能是1。
把哈希表比作是个教室,如果教室里坐满了人,然后让你从中找出你的朋友,那密密麻麻的,是不是特别不好找,可能会眼花缭乱;但是如果这个教室里只坐了 2/3 或者 1/2 的人,那么人群看起来就没那么密密麻麻,那让你找到你的朋友也许会相对容易一点。
也正因为这样的情况,我们可以在适当的时候根据填充因子的大小对哈希表的长度进行扩大,从而减小填充因子的大小,降低我们数据存取所耗费的时间。
这里我们就将填充因子等于 0.75
作为哈希表扩容的临界点,同时会在后面封装哈希表的时候实现扩容。
当然,填充因子太小也是不合适地,所以我们也会在适当的地方添加减容功能,即将填充因子等于 0.25
作为哈希表减容的临界点。
=================================================================
老规矩,我们在封装哈希表之前,先来看看哈希表常见的方法都有哪些
| 方法 | 含义 |
| — | — |
| put() | 向哈希表中插入数据或修改哈希表中数据 |
| get() | 获取哈希表中的某个数据 |
| del() | 删除哈希表中某个数据 |
| isEmpty() | 判断哈希表是否为空 |
| size() | 返回哈希表内元素个数 |
| resize() | 改变哈希表容量,并将数据放到正确的位置上 |
| isPrime() | 判断某个数是不是质数 |
| toPrime() | 获取离某个数最近的质数 |
===================================================================
前提:
-
本文选用链地址法解决冲突问题
-
涉及到常量的地方,都选用质数,例如哈希表容量 、霍纳算法的常量等。因为在数论上,使用质数可以尽可能地使数据在哈希表中均匀分布
首先创建一个大的构造函数,用于存放哈希表的一些属性和方法。
function HashTable() {
// 属性
// 用于存储数据
this.storage = []
// 统计哈希表内数据个数
this.count = 0
// 设定哈希表初始长度
this.length = 7
}
因为我们是通过数组来实现的哈希表,所以设置了属性 storage
来存储数据;然后定义了属性 count
用于统计哈希表内的数据个数,方便之后用于计算填充因子;最后定义了属性 length
设定了哈希表的初始长度为质数7
在文章的开头,我就用霍纳算法讲解了哈希化的过程,因此我们在封装哈希函数时,就也通过霍纳算法的最终化简结构来实现
这里,我放上霍纳算法的化简结果,方便大家观看学习
P(n) = a 0 a_0 a0 + x( a 1 a_1 a1+x( a 2 a_2 a2 +…+ x( a n a_n an − 1 + x a n a_n an) ) )
我们来看一下代码
function HashTable() {
// 属性
// 用于存储数据
this.storage = []
// 统计哈希表内数据个数
this.count = 0
// 设定哈希表初始长度
this.length = 7
//封装哈希函数
HashTable.prototype.hashFunc = function (str, size) {
let hashCode = 0
//取一个很大的数
for (let i = 0; i < str.length; i ++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//取余
return hashCode % size
}
}
这里的霍纳算法中的常数我就随便取了一个质数37,大家也可以随便选别的稍微大一点的质数
哈希函数接收两个参数,第一个参数为 str
,即我们之后传入数据的 key
;第二个参数为 size
,即哈希表的长度,所以可以直接进行调用我们设定的属性 this.length
put()
方法就是向哈希表插入数据或者修改哈希表中某数据。
该方法接收两个参数,第一个参数为 key
;第二个参数为 value
。即相当于传入一个键值对
实现思路:
-
通过哈希函数,将
key
哈希化,获取一个索引index
-
判断哈希表
storage
数组的索引index
上有无数据,若无,则直接在该位置上创建一个空数组arr
,并把我们传入的键值对打包放到一个新的数组中存到arr
中 -
若有数据,则遍历该索引上的数组每个元素,比对每个元素的
key
是否与我们传入的key
相等,若有查询到相等的值,则用我们传入的value
替换查询到的该元素中的value
,这就实现了修改数据的功能 -
若没有查询到相等的值,则直接将我们的键值对打包放到一个新的数组中存储到哈希表
index
索引上的数组中去,此时this.count + 1
为了方便大家理解,我用动图来给大家演示该方法的实现过程
首先是插入数据操作
接下来是修改数据操作
我们来看一下代码
function HashTable() {
// 属性
// 用于存储数据
this.storage = []
// 统计哈希表内数据个数
this.count = 0
// 设定哈希表初始长度
this.length = 7
//封装哈希函数
HashTable.prototype.hashFunc = function (str, size) {
let hashCode = 0
//取一个很大的数
for (let i = 0; i < str.length; i ++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//取余
return hashCode % size
}
// 插入或修改数据
HashTable.prototype.put = function (key, value) {
// 1.哈希化获得下标值
let index = this.hashFunc(key, this.length)
let current = this.storage[index]
// 2.判断该下标值的位置是否有数据
// 2.1无数据
if(!current) {
this.storage[index] = [[key, value]]
this.count ++
return;
}
// 2.2有数据
// 3.遍历对应索引上的数组
for (let i = 0; i < current.length; i ++) {
// 3.1已存在相同数据
if(current[i][0] === key) {
current[i][1] = value
return;
}
}
// 3.2未存在相同数据,直接添加数据
current.push([key, value])
this.count ++
}
}
我们来使用一下该方法
let ht = new HashTable()
// 执行put()方法6次
ht.put(‘abc’, ‘123’)
ht.put(‘hgf’, ‘124’)
ht.put(‘wds’, ‘125’)
ht.put(‘wer’, ‘126’)
ht.put(‘kgl’, ‘127’)
ht.put(‘kmg’, ‘128’)
// 查看哈希表内数据个数
console.log(ht.count) // 6
// 通过storage属性查看一下哈希表的内部结构
console.log(ht.storage)
/* storage打印结果
[
[ [ ‘wds’, ‘125’ ], [ ‘kgl’, ‘127’ ], [ ‘kmg’, ‘128’ ] ],
[ [ ‘wer’, ‘126’ ] ],
<1 empty item>,
[ [ ‘hgf’, ‘124’ ] ],
[ [ ‘abc’, ‘123’ ] ]
]
*/
此时的哈希表内部是这样的
get()
方法是用于查询哈希表中某个数据。该方法直接收一个参数,即用于查询的 key
实现思路:
-
通过哈希函数,将
key
哈希化,获取一个索引index
-
判断哈希表
storage
数组的索引index
上有无数据,若无,则返回false
-
若有数据,则遍历该索引上的数组每个元素,比对每个元素的
key
是否与我们传入的key
相等,若有查询到相等的值,则返回该值的value
-
若无数据,则返回
false
思路和代码都比较简单,我们直接来看代码
function HashTable() {
// 属性
// 用于存储数据
this.storage = []
// 统计哈希表内数据个数
this.count = 0
// 设定哈希表初始长度
this.length = 7
//封装哈希函数
HashTable.prototype.hashFunc = function (str, size) {
let hashCode = 0
//取一个很大的数
for (let i = 0; i < str.length; i ++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//取余
return hashCode % size
}
// 获取数据
HashTable.prototype.get = function (key) {
// 1.获取相应的下标值
let index = this.hashFunc(key, this.length)
let current = this.storage[index]
// 2.判断该下标值的位置是否有数据
// 2.1 若该下标值位置不存在任何数据,则查找失败
if(!current) {
return false
}
// 2.2 该下标值位置有数据
// 3. 进行遍历查找
for(let i in current) {
// 3.1 找到对应数据并返回value
if(current[i][0] === key) {
return current[i][1]
}
}
// 3.2 没有找到对应数据,返回false
return false
}
}
我们来使用以下该方法
let ht = new HashTable()
// 执行put()方法 6次
ht.put(‘abc’, ‘123’)
ht.put(‘hgf’, ‘124’)
ht.put(‘wds’, ‘125’)
ht.put(‘wer’, ‘126’)
ht.put(‘kgl’, ‘127’)
ht.put(‘kmg’, ‘128’)
// 执行get()方法,获取 key为 ‘hgf’ 的值
console.log(ht.get(‘hgf’)) // 124
del()
方法是删除哈希表中某个数据。该方法接收一个参数 key
实现思路:
-
通过哈希函数,将
key
哈希化,获取一个索引index
-
判断哈希表
storage
数组的索引index
上有无数据,若无,则返回false
,表示删除失败 -
若有数据,则遍历该索引上的数组每个元素,比对每个元素的
key
是否与我们传入的key
相等,若有查询到相等的值,则直接删除该值,此时this.count --
,并返回被删除元素的value
值 -
若没有查询到相等的值,则返回
false
,表示删除失败
我们来看一下代码
function HashTable() {
// 属性
// 用于存储数据
this.storage = []
// 统计哈希表内数据个数
this.count = 0
// 设定哈希表初始长度
this.length = 7
//封装哈希函数
HashTable.prototype.hashFunc = function (str, size) {
let hashCode = 0
//取一个很大的数
for (let i = 0; i < str.length; i ++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//取余
return hashCode % size
}
// 删除数据
HashTable.prototype.del = function (key) {
// 1.获取相应的下标值
let index = this.hashFunc(key, this.length)
let current = this.storage[index]
// 2. 判断该索引位置有无数据
// 2.1 该下标值位置没有数据,返回false,删除失败
if(!current) {
return false
}
// 2.2 该下标值位置有数据
// 3. 遍历数组查找对应数据
for (let i in current) {
let inner = current[i]
// 3.1 找到对应数据了,删除该数据
if(inner[0] === key) {
current.splice(i, 1)
this.count –
return inner[1]
}
}
// 3.2 没有找到对应数据,则删除失败,返回false
return false
}
}
我们来使用一下该方法
let ht = new HashTable()
// 执行put()方法 6次
ht.put(‘abc’, ‘123’)
ht.put(‘hgf’, ‘124’)
ht.put(‘wds’, ‘125’)
ht.put(‘wer’, ‘126’)
ht.put(‘kgl’, ‘127’)
ht.put(‘kmg’, ‘128’)
// 删除 key为 'hgf’的元素
ht.del(‘hgf’) // 删除成功,返回 124
// 删除 key为 'ppp’的元素
ht.del(‘ppp’) // 删除失败,返回 false
// 查看哈希表内部结构
console.log(ht.storage)
/* storage打印结果
[
[ [ ‘wds’, ‘125’ ], [ ‘kgl’, ‘127’ ], [ ‘kmg’, ‘128’ ] ],
[ [ ‘wer’, ‘126’ ] ],
<1 empty item>,
[],
[ [ ‘abc’, ‘123’ ] ]
]
*/
此时的哈希表内是这样的
isEmpty()
方法是用于判断哈希表是否为空。该方法无需传参
该方法思路比较简单,直接判断属性 count
是否为 0
即可
我们来看一下代码
function HashTable() {
// 属性
// 用于存储数据
this.storage = []
// 统计哈希表内数据个数
this.count = 0
// 设定哈希表初始长度
this.length = 7
//封装哈希函数
HashTable.prototype.hashFunc = function (str, size) {
let hashCode = 0
//取一个很大的数
for (let i = 0; i < str.length; i ++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//取余
return hashCode % size
}
//判断哈希表是否为空
HashTable.prototype.isEmpty = function () {
return this.count === 0
}
}
我们来用一下该方法
let ht = new HashTable()
console.log(ht.isEmpty()) // false,哈希表为空
ht.put(‘abc’, ‘123’)
console.log(ht.isEmpty()) // true,哈希表不为空
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
TCP协议
- TCP 和 UDP 的区别?
- TCP 三次握手的过程?
- 为什么是三次而不是两次、四次?
- 三次握手过程中可以携带数据么?
- 说说 TCP 四次挥手的过程
- 为什么是四次挥手而不是三次?
- 半连接队列和 SYN Flood 攻击的关系
- 如何应对 SYN Flood 攻击?
- 介绍一下 TCP 报文头部的字段
- TCP 快速打开的原理(TFO)
- 说说TCP报文中时间戳的作用?
- TCP 的超时重传时间是如何计算的?
- TCP 的流量控制
- TCP 的拥塞控制
- 说说 Nagle 算法和延迟确认?
- 如何理解 TCP 的 keep-alive?
CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
浏览器篇
- 浏览器缓存?
- 说一说浏览器的本地存储?各自优劣如何?
- 说一说从输入URL到页面呈现发生了什么?
- 谈谈你对重绘和回流的理解
- XSS攻击
- CSRF攻击
- HTTPS为什么让数据传输更安全?
- 实现事件的防抖和节流?
- 实现图片懒加载?
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-JUrG4sgc-1711809826189)]
[外链图片转存中…(img-R68q0vDn-1711809826190)]
[外链图片转存中…(img-dEgbRLuQ-1711809826190)]
[外链图片转存中…(img-x4zMhvc2-1711809826190)]
[外链图片转存中…(img-RvalfvCC-1711809826191)]
[外链图片转存中…(img-UHsyIaxW-1711809826191)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-WOBnfF3K-1711809826191)]
TCP协议
- TCP 和 UDP 的区别?
- TCP 三次握手的过程?
- 为什么是三次而不是两次、四次?
- 三次握手过程中可以携带数据么?
- 说说 TCP 四次挥手的过程
- 为什么是四次挥手而不是三次?
- 半连接队列和 SYN Flood 攻击的关系
- 如何应对 SYN Flood 攻击?
- 介绍一下 TCP 报文头部的字段
- TCP 快速打开的原理(TFO)
- 说说TCP报文中时间戳的作用?
- TCP 的超时重传时间是如何计算的?
- TCP 的流量控制
- TCP 的拥塞控制
- 说说 Nagle 算法和延迟确认?
- 如何理解 TCP 的 keep-alive?
[外链图片转存中…(img-EW436tMV-1711809826192)]
CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
浏览器篇
- 浏览器缓存?
- 说一说浏览器的本地存储?各自优劣如何?
- 说一说从输入URL到页面呈现发生了什么?
- 谈谈你对重绘和回流的理解
- XSS攻击
- CSRF攻击
- HTTPS为什么让数据传输更安全?
- 实现事件的防抖和节流?
- 实现图片懒加载?