数据结构,哈希表(笔记)

1. 哈希表

​ 为什么会有哈希表?

举个栗子:

一个公司里有上千个员工,我想找一个员工的信息一般通过什么方式

通过数组,每一个员工对应一个工号,我可以直接查找工号就可以获取对应的信息

但是我不想通过工号去获取,我想通过员工的名字去获取,这个时候就会想到链表,但是问题来了,链表是一个信息连着一个信息去查找,效率非常的慢

这个时候就可以用到哈希表了,可以把每个员工的名字转成对应的数字,数字对应名字,这样我直接通过数字就可以获取员工的信息了

1.1 哈希化

​ 将大数字转化为数组范围内的下标(这里的范围自己去定义)过程,比如0~100取余10就得到0-9的下标值(方式有多种,比如java采用的位运算),这个过程我们就称之为哈希化

假如有5000个单词,可能会定义一个长度为5000的数组,但是在实际情况中,我们不能保证这些单词对应的大数完全不相同,所以需要两倍的大小10000,虽然数组的长度已经足够了,但是通过哈希化后的下标还是可能会重复,这个时候就需要去解决冲突(这里的冲突是指大数哈希化后的,而不是大数)

1.2 哈希函数

​ 通常我们会将单词转成大数字(幂的连乘),大数字在进行哈希化后的代码,通过一个函数进行处理得到想要的数据,这个函数就是哈希函数

为什么要把单词转成大数字:因为我们需要,把对应的单词通过合适的方式转换为一个非常庞大的数字,这样就不会和其他的单词重复,确保了每个单词都有对应的数字(但是数据过于庞大就需要哈希化)

1.3 哈希表

​ 最终将数据都插入到一个数组中,我们就称之为是一个哈希表

1.4 解决冲突
  • 什么是冲突,冲突就是哈希化后会有相同的下标值,解决冲突有两种方式,链地址法开发地址法

  • 链地址法

在这里插入图片描述

  • 链地址法解决冲突的办法,就是每一个数组中存储的数据不再是单个数据,而是一个链表或数组

  • 当查询时想,先根据哈希化后的下标找到对应的位置,再在对应位置中进行线性查找

  • 问题来了,到底是选择链表还是数组呢,其实都差不多,因为冲突的情况很少(但是我们必要也要解决),很少就表示了,它后面的链表或者数组的长度都不会很长,都是线性查抄效率其实都差不多

  • 选择链表或数组还是看需求,链表和数组都有自己的优缺点,数组适合查抄修改,链表适合插入删除

  • 开放地址法

在这里插入图片描述

  • 开发地址法就是寻找空白的位置来存放冲突的数据,因为经过处理,存放数组的长度是足够存放这些数据的(长度不够会扩容),但是通过哈希化后存放会有冲突的情况,开发地址法的效率要比链地址法的效率要高一些,开发地址法寻找有三种方式,线性探测二次探测再哈希法

  • 线性探测

    插入:假设下标为2项中有一个为22的数据,然后我想插入32的数据,这个时候就需要线性探测,线性探测就是线性的查抄空白的单元,假如下标为3项中没有内容,就可以通过线探测让index++(目前指的是2),当加到3时发现有空位,就把32插进去,线性探测只会从对应的下标开始探测,不是从头开始

    查询:查询的思路和插入一样,如果还有值62,72等也是按照这样的思路,还有就是查询index=2的值,比如我要查有没有102,线性查找当遇到了空的位置还没有找到,那么就停止,因为在插入的时候会排除掉所有的空位置,都已经找到空位置还没有找到,就已经能够表示没有该值了

    删除:删除和查询插入的思路也是一样,但是有一点要注意,在删除后不能将这个下标的内容设置为null,因为设置为null在插入查询和删除时,会找到为空的项,那么它就不会在向后探测了,那我后面还存的62,72呢他就找不到了,所以删除后的需要做特殊处理(比如设置为-1),表示他是被删除的,后面还可能有相同类型(这里类型指结尾都是2,通过哈希化后分辨的),然后再次插入时可以放到这个-1的位置

    线性探测的问题:线性探测有一个比较严重的问题,就是聚集,比如在我们没有任何数据的时候,我们插入一些数据22-23-24-25-26-27,那就意味着2-3-4-5-6-7的位置都有元素,这种一连串的数据就叫做聚集,聚集会影响哈希表的性能,无论是插入、删除、查询都会影响,比如我们插入一个32,会发现从2-7都有数据,但是他还是会一位一位的去探测,特别消耗性能,这个时候就需要二次探测了,二次探测可以解决一部分这个问题

  • 二次探测

    二次探测其实就是线性探测的改良版:线性探测是 x+1、x+2、x+3这种一步一步探测的,而二次探测是 x+1(平方)、x+2(平方)、x+3(平方)这种成倍探测的,其他的操作和线性探测都是一样的,就是步长不一样

    二次探测的问题:如果我们插入的是32-112-42-92-2这种数据呢,这种情况也会造成一种,步长不一样的聚焦,这个时候就需要再哈希法来根本的解决问题了

  • 再哈希法

    再哈希法:线性探测和二次探测都会有对应的聚焦问题,为了解决这些问题,就需要在哈希法,再哈希法是根据关键字来进行探测,不同的关键字即使映射到相同的数组下标(冲突),也可以使用不同的探测序列(步长),不同的关键字使用不同的步长,第二次哈希化和第一个哈希函数不同,也不能输出为0,因为输出为0就代表没有步长,算法就进入了死循环

    新的哈希函数stepSize= constant - (key % constant),其中key是数据的长度,constant是质数,且小于数组的容量,例如stepSize= 5- (key % 5),结果无乱如何都不可能为0

1.5 哈希化的效率
  • 如果没有发生冲突,那么效率就会非常的高
  • 如果发生冲突,存取时间就依赖于后来的探测长度,平均探测长度以及平均存取时间,取决于装填因子,随着装填因子变大,探测长度也越来越长
  • 装填因子 = 总数据项 / 哈希表长度
  • 开放地址法的装填因子最大只能是1,因为它必须寻找到空白的单元才能将元素放入
  • 链地址法的装填因子可以大于1,因为链地址法的拉链可以无限的延伸下去(到后面效率越来越低)
1.6 优秀的哈希函数
  • 快速的计算

    • 在计算机中使用乘法是非常消耗性能的

    • 把对应数据转为大数,使用的就是幂的连乘,这里会用到非常多的乘法,比如

      cats = 3 * 27^3 + 1*27^2 + 20 * 27^1 + 17 * 27^0 = 60337

      说明:27自己选择的幂的底数,^表示幂,3、1、20、17表示对应单词的编码

      每个单词都是这样转换的,可以用 n 来表示 经过一系列化简运算乘法次数变为:n(n+1)/2次,加法次数为:n次

    • 霍纳法则:为了解决这类求值问题的高效算法,在中国霍纳法则也被称为秦九韶算法

      通过霍纳法则变化后,乘法次数变为:n次,加法次数为:n次

      如果使用大O表示时间复杂度的话,直接从O(N^2)降到了O(N)

  • 均匀的分布

    • 在哈希表中,无论是链地址法还是开放地址法,当多个元素映射到同一个位置时,都会影响效率

    • 所以,优秀的哈希函数应该尽可能将元素映射到不同的位置,当元素在哈希表中均匀的分布

    • 为了均匀的分布,在使用常量的时候尽量使用质数

      哈希表的长度,幂的底数,这些都使用质数,为了让哈希表分布更均匀,再哈希法也更容易查找

      举个栗子:

      如果容器长度为15(下标值0~14),通过再哈希法的函数计算要查找的步长:5 - (15 % 5) = 5(步长)

      探测顺序:0 - 5 - 10 - 0 - 5 - 10……如果0、5、10这些里面都有数据那么程序就会变成死循环

      如果容器长度为13(下标值0~12),查找的步长:5 - (13 % 5) = 2(步长)

      探测顺序:0 - 2 - 4 - 6 - 8 - 10 - 12 - 1 - 3 - 5 - 7 - 9 - 11,会把每一个位置都探测到

      在试一个,容器长度为7(下标值0~6),查找的步长:5 - (7 % 5) = 3(步长)

      探测顺序:0 - 3 - 6 - 2 - 5 - 1 - 4,也会把每一个位置都探测到

      这样不仅不会产生循环,而且可以让数据在哈希表中更加均匀的分布(这里面设计到数学的知识)

1.7 哈希表扩容
  • 在链地址法中,装填因子是可以大于1的
  • 但是随着数据量的增多,每一个index对应的bucket(桶)也会越来越长,这样一来效率就会降低
  • 所以在这个时候就需要对数组进行扩容
  • 比较常见的情况是,装填因子 > 0.75 的时候进行扩容,在这个时候进行扩容效率最高
  • 扩容之后要把之前数组中的数据清空,在重新添加到新的数组中
  • 有了扩容当然也会有对应的缩容,缩容的实现原理和扩容其实是一样的
  • 当 装填因子 < 0.25 的时候进行缩容
  • 因为如果没有缩容,只有扩容的话,我先一直添加数据,然后数组会不断的扩容,然后我在一直删除数据,这个时候数组中就会有很多空的位置,就浪费掉了很多不必要的空间,所以也需要用到缩容
1.8 哈希表的封装(链地址法)
  • 哈希函数

    // 封装哈希函数
    // str:要转为大数的数据
    // size:数组的长度
    function hashFn(str, size) {
      // 存储大数
      let hashCode = 0
    
      // 霍纳法则:高效的计算
      // ((...(((anx + an - 1)x + an - 2)x + an - 3)...)x + a1)x + a0
      for (let i = 0; i < str.length; i++) {
        // charCodeAt --> 转Unicode编码
        // 37 自己选择的质数,选择37的比较多
        hashCode = hashCode * 37 + str.charCodeAt(i)
      }
    
      // 数组的长度取模(哈希化),方便存放不同对应的位置
      let index = hashCode % size
      return index
    }
    
  • 高效的判断质数

    function isPrime(num) {
      // sqrt(n) --> 指的是n的开方
      // 一个数n如果可以进行因式分解,那么分解的得到的两个数
      // 一定是一个小于等于sqrt(n),一个大于等于sqrt(n)
      let temp = parseInt(Math.sqrt(num))
      
      for (let i = 2; i <= temp; i++) {
        if (num % i == 0) {
          return false
        }
      }
      return true
    }
    
  • 哈希表

    // 哈希表的增、删、改、查思路是一样的
    // 1.通过哈希函数转换,取出对应的下标,也就是要存放的数据的桶
    // 2.查看该桶是不是空的
    //  (1)添加操作中,如果桶空就创建一个桶(这里使用的是数组,数组和链表的效率差不多)
    //  (2)在哈希表中,添加和修改是同一个方法,因为是通过key去添加或修改数据,
    //      如果没有key就添加,等同于添加操作,有key的话就修改value,就等同于修改操作
    //  (3)在查询和删除中,如果没有该桶就返回操作失败对应的操作
    // 3.遍历该桶,在进行增、删、改、查,第二点说的是桶,第三点说的是桶中的数据,思路都一样
    // 4.添加和删除才会存在的操作
    //  (1)添加,内容长度加1,然后查看是否需要扩容
    //  (2)删除,内容长度减1,删除查看是否需要缩容
    //  (3)扩容或缩容,后的数据也必须是质数
    
    // 哈希表类
    class HashTable {
      constructor() {
        // 最外层大数组(哈希表)
        this.storage = []
        // 数据量(数据内容的总长度)
        this.count = 0
        // 哈希表长度
        this.limit = 7
      }
    
      // 封装哈希函数
      // str:要转为大数的数据
      // size:数组的长度
      hashFn(str, size) {
        // 存储大数
        let hashCode = 0
    
        // 霍纳法则:高效的计算
        // ((...(((anx + an - 1)x + an - 2)x + an - 3)...)x + a1)x + a0
        for (let i = 0; i < str.length; i++) {
          // charCodeAt --> 转Unicode编码
          // 37 自己选择的质数,选择37的比较多
          hashCode = hashCode * 37 + str.charCodeAt(i)
        }
    
        // 数组的长度取模(哈希化),方便存放不同对应的位置
        let index = hashCode % size
        return index
      }
    
      // 1.put(key, value):插入和修改数据
      put(key, value) {
        // 取出对应要放入的下标
        let index = this.hashFn(key, this.limit)
    
        // 桶:因为选择的链地址法,每一个对应的下标都要存放一个数组(桶)
        let bucket = this.storage[index]
    
        // 如果这个桶不存在表示第一次插入值,需要创建这个桶,并放在对应下标的位置
        if (bucket == null) {
          bucket = []
          this.storage[index] = bucket
        }
    
        // 如果是第一次添加数据循环不会进来(bucket.length == 0)
        for (let i = 0; i < bucket.length; i++) {
          // 每一项数据存放的也是一个数组,[key, value]
          let data = bucket[i]
          // 发现有相同的key,那么就做修改操作
          if (data[0] == key) {
            data[1] = value
            return
          }
        }
        // 没有就添加
        bucket.push([key, value])
    
        // 添加数据内容加一
        this.count++
        // 当装填因子 > 0.75 时,对数组哈希表进行扩容
        if (this.count > this.limit * 0.75) {
          // 查看这个数是不是质数,是就返回不是就变为质数
          let prime = this.setPrime(this.limit * 2)
          this.resize(prime)
        }
      }
    
      // 2.get(key):获取数据
      get(key) {
        // 取出对应要查找的下标
        let index = this.hashFn(key, this.limit)
    
        // 取出当前桶
        let bucket = this.storage[index]
    
        // 如果桶为空直接返回
        if (bucket == null) {
          console.log("桶为空");
          return
        }
    
        for (let i = 0; i < bucket.length; i++) {
          let data = bucket[i]
          // 查找key相等就返回value
          if (data[0] == key) {
            return data[1]
          }
        }
    
        return "没有该数据"
      }
    
      // 3.remove(key):删除数据,并返回该数据
      remove(key) {
        let index = this.hashFn(key, this.limit)
    
        let bucket = this.storage[index]
    
        if (bucket == null) {
          console.log("桶为空");
          return
        }
    
        for (let i = 0; i < bucket.length; i++) {
          let data = bucket[i]
          if (data[0] == key) {
            // 匹配到了就删除当前项
            bucket.splice(i, 1)
            this.count--
    
            // 哈希表最小长度为7(因为太小了没必要)
            // 当装填因子 < 0.25 时,缩容
            if (this.limit > 7 && this.count < this.limit * 0.25) {
              let prime = this.setPrime(Math.floor(this.limit / 2))
              this.resize(prime)
            }
    
            return data[1]
          }
        }
        return "没有该数据"
      }
    
      // 4.isEmpty():查看哈希表是否为空,为空返回true,不为空返回false
      isEmpty() {
        return this.count == 0
      }
    
      // 5.size():获取哈希表中的元素个数
      size() {
        return this.count
      }
    
      // resize(newLimit):哈希表扩容/缩容
      resize(newLimit) {
        // 先保存原数据
        let oldStorage = this.storage
    
        // 初始化,然后改变哈希表长度
        this.storage = []
        this.count = 0
        this.limit = newLimit
    
        // 
        for (let i = 0; i < oldStorage.length; i++) {
          let bucket = oldStorage[i]
    
          // 如果为空就跳过
          if (bucket == null) {
            continue
          }
    
          // 如果有值就全部在添加,到新的哈希表长度中
          for (let i = 0; i < bucket.length; i++) {
            let data = bucket[i]
            this.put(data[0], data[1])
          }
        }
      }
    
      // isPrime(num):判断一个数是否为质数,是true,不是false
      isPrime(num) {
        // sqrt(n) --> 指的是n的开方
        // 一个数n如果可以进行因式分解,那么分解的得到的两个数
        // 一定是一个小于等于sqrt(n),一个大于等于sqrt(n)
        let temp = parseInt(Math.sqrt(num))
        
        for (let i = 2; i <= temp; i++) {
          if (num % i == 0) {
            return false
          }
        }
        return true
      }
    
      // setPrime(num):把一个数变为质数
      setPrime(num) {
        while (!this.isPrime(num)) {
          num++
        }
        return num
      }
    }
    
    // 测试
    let hashTable = new HashTable()
    
    hashTable.put("小红", "1")
    hashTable.put("小白", "12")
    hashTable.put("小蓝", "123")
    hashTable.put("小绿", "1234")
    hashTable.put("小黑", "12345")
    hashTable.put("小天", "123456")
    hashTable.put("小黄", "1234567")
    
    // console.log(hashTable.get("大狗"));
    // hashTable.get("小黑")
    
    console.log(hashTable.remove("小红"));
    console.log(hashTable.remove("小白"));
    console.log(hashTable.remove("小绿"));
    console.log(hashTable.remove("小黑"));
    
    // console.log(hashTable.isEmpty());
    
    // console.log(hashTable.size());
    
    console.log(hashTable);
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
尚硅谷数据结构韩顺平老师在讲解数据结构时也提到了哈希表哈希表是一种常用的数据结构,它通过哈希函数将关键字映射到一个数组中的位置,以实现快速的查找和插入操作。哈希表的特点是可以在常数时间内进行查找、插入和删除操作,因此在实际应用中被广泛使用。 然而,哈希表也有一些不足之处。首先,哈希表中的数据是没有顺序的,所以不能以一种固定的方式来遍历其中的元素。其次,通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。 尽管哈希表存在一些不足之处,但是它在实际应用中的性能优势仍然是非常显著的。在处理大量数据时,哈希表可以提供较高的查找效率,使得我们能够更快地找到所需的数据。 因此,尚硅谷数据结构韩顺平老师在讲解数据结构时也强调了哈希表的重要性,帮助学习者了解并掌握这一常用的数据结构。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [韩顺平老师尚硅谷Java数据结构与算法194集笔记](https://download.csdn.net/download/weixin_52184392/32076811)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [【尚硅谷|韩顺平】数据结构和算法](https://blog.csdn.net/ZEZHEN0222/article/details/128624496)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [数据结构与算法-哈希表 | 尚硅谷韩顺平](https://blog.csdn.net/weixin_54232666/article/details/127043618)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值