数据结构学习笔记-哈希表

243 阅读6分钟

哈希表原理

哈希表是一种非常重要的数据结构,几乎所有的编程语言都有直接或者间接的应用这种数据结构,它通常是基于数组实现的。

相对于数组,它有更多的优势
  • 它可以提供非常快速的插入-删除-查找操作。
  • 哈希表的速度比数组还要快,基本可以瞬间查找到想要的元素 。
  • 哈希表相对于数组来说编码要容易的多。
但是哈希表相对于数组也有一些不足:
  • 哈希表中的数组是没有顺序的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素。
  • 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。

哈希化

将大数字转化成数组范围内下标的过程,我们就称之为哈希化

哈希函数

通常我们会将单词转成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数我们称为哈希函数

哈希表

最终将数据插入到的这个数组,对整个结构的封装,我们就称之为是一个哈希表

哈希化冲突问题

链地址法

链地址法是一种比较常见的解决冲突的方案(也称拉链法)

创建了一个内存为10的数组,现在,需要将一些数字存到数组内部,这些数字哈希化后可能会重复,将下标值相同的数通过链表或者数组链接起来的方法叫做链地址法。当我们要查找某值的时候,就可以先根据其下标找到对应的链表或者数组再在其内部寻找。

开放地址法

开放地址法的主要工作方式是寻找空白的单元格来添加重复的数据。

开放地址法的主要工作方式是寻找空白的单元格来添加重复的数据。如下图所示:

如果有一个数字32,现在要将其插入到数组中,我们的解决方案为:

新插入的32本来应该插入到52的位置,但是该位置已经包含数据, 可以发现3、5、9的位置是没有任何内容的 这个时候就可以寻找对应的空白位置来放这个数据 但是探索这个位置的方式不同,有三种方式:

1、线性探索

即线性的查找空白单元

插入32

经过哈希化得到的index=2,但是在插入的时候,发现该位置已经有52 此时就从index+1的位置开始一点点查找合适的位置来放置32 探测到的第一个空的位置就是该值插入的位置 查询32

首先经过哈希化得到index= 2,比较2的位置结果和查询的数值是否相同,相同则直接返回 不相同则线性查找,从index位置+1查找和32一样的。 需要注意的是:如果32的位置之前没有插入,并不需要将整个哈希表查询一遍来确定该值是否存在,而是如果查询到空位置,就停止。因为32之前不可能跳过空位置去其他的位置。

线性探测也有一个问题就是:如果之前插入的数据是连续插入的,则新插入的数据就需要很长的探测距离。

2、二次探索

二次探索就在线性探索的基础上进行了优化。

线性探测,我们可以看做是步长为1的探测,比如从下标值x开始,从x+1,x+2,x+3依次探测。

二次探测对步长做了优化,比如从下标值x开始,x+1²,x+2²,x+3²依次探测。

但是二次探测依然存在问题:比如我们连续插入的是32-112-82-42-52,那么他们依次累加的时候步长是相同的,也就是这种情况下会造成步长不一的一种聚集,还是会影响效率,怎样解决这个问题呢?来看看再哈希法。

3、再哈希法

再哈希法的做法就是:把关键字用另一个哈希函数在做一次哈希化,用这次哈希化的结果作为步长,对于指定的关键字,步长在整个探测中是不变的,不同的关键字使用不同的步长。

第二次哈希化需要具备以下特点:

和第一个哈希函数不同

不能输出为0(否则,将没有步长,每次叹词都是原地踏步,算法进入死循环)

而计算机专家已经设计好了一种工作很好的哈希函数。

stepSize = constant - (key % constant)

其中constant是质数,且小于数组的容量,key是第一次哈希化得到的值。

例如:stepSize = 5-(key%5),满足需求,并且结果不可能为0。

hashTable的实现

function hashTable() {
    // 属性
    this.storage = []; // 数组中存放的相关元素
    this.count = 0; // 存了多少数据
    this.limit = 7; // 用于标记数组中一共存放了多少元素 
    }

哈希函数

hashTable.prototype.hashFunc = function (str, size) {
  // 1 定义hashcode变量
  let hashCode = 0;
  // 霍纳算法,来计算hashcode的值
  // eg: cats -->   Unicode编码
  for (let i = 0; i < str.length; i++) {
    hashCode = 37 * hashCode + str.charCodeAt(i); // 37用的质数比较多
  }
  // 取余操作
  let index = hashCode % size;
  return index;
};

插入或修改

1根据key获取索引值,将数据插入到相对应的位置

2 根据索引值取出bucket(桶)

  • 2.1 如果桶不存在,创建桶,并放置在该索引的位置

3 判断是新增还是修改

  • 3.1 如果没有值,就新增,如果值存在,则修改

4 新增操作

hashTable.prototype.put = function (key, value) {
  let index = this.hashFunc(key, this.limit); // 根据key找到对应的index
  let bucket = this.storage[index]; // 根据索引值取出bucket(桶)
  // 判断桶是否为null
  if (bucket == null) {
    bucket = [];
    this.storage[index] = bucket;
  }
  // 判断是否是修改数据  根据key查找
  for (let i = 0; i < bucket.length; i++) {
    let tuple = bucket[i];
    // 找到相同的key,赋值value
    if (tuple[0] === key) {
      tuple[1] = value;
      return;
    }
  }
  //  新增
  bucket.push([key, value]);
  this.count += 1;
  // 判断是否需要扩容
  if (this.count > this.limit * 0.75) {
    let newLimit = this.limit * 2; // 扩容,但不是质数  eg: 14
    let primeNumber = this.getPrime(newLimit); // 获取质数
    this.resize(primeNumber);
  }
};

查找

1 根据key获取相对应的index

2根据index获取相对应的bucket

3 判断bucket是否为null

  • 3.1为null return null
  • 3.2 不为null 线性查找bucket中的每一个key 找到烦恼会value,否则返回null
 hashTable.prototype.get = function (key) {
  let index = this.hashFunc(key, this.limit);
  let bucket = this.storage[index];
  if (bucket == null) return null;
  for (let i = 0; i < bucket.length; i++) {
    const tuple = bucket[i];
    if (tuple[0] === key) {
      return tuple[1]; // 返回value
    }
  }
  return null;
};

删除

hashTable.prototype.remove = function (key) {
  let index = this.hashFunc(key, this.limit);
  let bucket = this.storage[index];
  if (bucket == null) return null;
  for (let i = 0; i < bucket.length; i++) {
    const tuple = bucket[i];
    if (tuple[0] === key) {
      bucket.splice(i, 1);
      this.count--;

      // 判断是否缩容
      if (this.limit > 7 && this.count < this.limit * 0.25) {
        this.resize(Math.floor(this.limit / 2));
      }

      return tuple[1]; // 返回删除的value
    }
  }
  return null;
};

扩容

hashTable.prototype.resize = function (newLimit) {
  let oldStorage = this.storage; //备份数据
  // 重置属性
  this.storage = []; // 数组中存放的相关元素
  this.count = 0; // 存了多少数据
  this.limit = newLimit; // 用于标记数组中一共存放了多少元素
  // 扩容后,空间变大,取出原先的数据重新放入
  for (let i = 0; j < oldStorage.length; i++) {
    let bucket = oldStorage[i]; //取桶
    if (oldStorage[i] == null) continue; // 存在桶是空  遍历全部数据,取出全部桶
    for (let j = 0; j < bucket.length; j++) {
      let tuple = bucket[j];
      this.put(tuple[0], tuple[1]);
    }
  }
};

判断是不是质数

hashTable.prototype.isPrime = function (num) {
  let temp = parseInt(Math.sqrt(num)); // num的算术平方根;
  // 判断质数,优化算法,只需要判断 小于等于  这个数的算术平方根 ,只能除够1和它本身
  for (let i = 2; i <= temp; i++) {
    if (num % i == 0) {
      return false; // 能够除够i 说明不是质数
    }
  }
  return true; // (2,temp]  没有一个能够整除,说明是质数
};

获取num后面的质数

hashTable.prototype.getPrime = function (num) {
  while (!this.isPrime(num)) {
    num++;
  }
  return num;
};
let ht = new hashTable();
ht.put("basketball", "姚明");
ht.put("football", "梅西");
ht.put("shooting", "许海峰");
ht.put("diving", "郭晶晶");
ht.put("snooker", "丁俊晖");

ht.put("basketball", "勒布朗詹姆斯");
let value = ht.get("basketball");
console.log(value);

//   let removeValue = ht.remove("snooker");
//   console.log(removeValue);

console.log("ht", ht);
let flag = ht.isPrime(12);
console.log("flag", flag);