TypeScript 基础数据结构哈希表 HashTable教程

目录
  • 前言
  • 1. 哈希表介绍和特性
  • 2. 哈希表的一些概念
  • 3. 地址冲突解决方案
    • 3.1 方案一:链地址法
    • 3.2 方案二:开放地址法
  • 4. 哈希函数代码实现
  • 5. 哈希表封装
    • 5.1 整体框架 v1 版
    • 5.2 添加 put 方法 v2 版
    • 5.3 添加 get 方法 v3 版
    • 5.4 添加 delete 方法 v4 版
  • 6. 哈希表的自动扩容

前言

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

很多学习编程的人一直搞不懂哈希表到底是如何实现的,在这一章中,我们就一点点来实现一个自己的哈希表。通过实现来理解哈希表背后的原理和它的优势。

1. 哈希表介绍和特性

哈希表通常是基于数组进行实现的,相对于数组,他有很多优势:

  • 他可以提供非常快速的 插入-删除-查找 操作
  • 无论多少数据,插入和删除都接近常量的时间:即 O(1) 的时间复杂度
  • 哈希表的速度比 树还要快,基本可以瞬间查找到想要的元素
  • 哈希表相对于树来说编码要容易很多

PS: 树也是一种基础的数据结构,js 中没有树,我们下一章就会讲树

哈希表相对于数组的一些不足:

  • 哈希表中的数据没有顺序,所以不能以一种固定的方式来遍历其中的元素
  • 通常情况下,哈希表中的 key 是不允许重复的。

那么哈希表到底是什么?

我们上面说了哈希表的优势和不足,似乎还是没说它到底长什么样子?

这也是哈希表不好理解的地方,它不像数组、链表和树等可通过图形的形式表示其结构和原理。

哈希表结构就是数组,但它神奇之处在于对下标值的一种变换,这种变换我们可以称之为哈希函数,通过哈希函数可以获取HashCode。

2. 哈希表的一些概念

  • 哈希化:将大数字转化成数组范围内下标的过程,称之为哈希化;
  • 哈希函数:我们通常会将单词转化成大数字,把大数字进行哈希化的代码实现放在一个函数中,该函数就称为哈希函数;
  • 哈希表:对最终数据插入的数组进行整个结构的封装,得到的就是哈希表。

仍然需要解决的问题:

  • 哈希化过后的下标依然可能重复,如何解决这个问题呢?这种情况称为冲突,冲突是不可避免的,我们只能解决冲突。

3. 地址冲突解决方案

解决冲突常见的两种方案:

3.1 方案一:链地址法

如下图所示:

  • 链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一个链条
  • 这个链条使用什么数据结构呢?常见的是数组或者链条
  • 比如链表,也就是每个数组单元中存储着一个链表。一旦发现重复,将重复的元素插入到链表的首端或者末端即可。
  • 当查询时,先根据哈希化后的下标值找到对应的位置,再取出链表,依次查询要寻找的数据

数组还是链表?

  • 数组或者链表在这其实都可以,效率上也差不多
  • 因为根据哈希化的 index 找出这个数组或者链表时,通常就会使用线性查找,这个时候数组和链表的效率是差不多的。
  • 当然在某些实现中,会将新插入的数据放在数组或者链表的最前面,因为觉得新插入的数据用于取出的可能性更大
  • 这种情况最好采用链表,因为数组在首位插入数据是需要所有其他项后移的,链表就没有这样的问题

3.2 方案二:开放地址法

开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。(了解即可,现在很少用到了)

据探测空白单元格位置方式的不同,可分为三种方法:

  • 线性探测:如果发现要插入的位置已经被占据,则 +1,直到探测到空白位置
  • 二次探测:二次探测就是在线性探测的基础上把步长修改了(+1 +4 +9
  • 再哈希法:如果发现要插入的位置已经被占据,通过算法将这个位置再次哈希化,直到得到新的位置没被占据

4. 哈希函数代码实现

好的哈希函数应该尽可能的让计算的过程变得简单,提高计算的效率。

  • 哈希表的主要优点是它的速度,所以在速度上不能满足,那么就打不到设计的目的了。
  • 提高速度的一个办法就是让哈希函数中尽量少的有乘法和除法。因为它们的性能是比较低的。

设计好的哈希函数应该具有的优点:

下面我们就来实现一个哈希函数:

/**
 * 哈希函数,将 key 映射成 index
 * @param key 要转换的 key
 * @param max 数组的长度(最大的数值)
 * @returns
 */
function hashFunc(key: string, max: number): number {
  // 1. 计算 hashCode cats => 60337 (27为底的时候)
  let hashCode = 0;
  const length = key.length;
  for (let i = 0; i < length; i++) {
    // 霍纳法则计算 hashCode
    hashCode = 31 * hashCode + key.charCodeAt(i);
  }
  // 2. 求出索引值
  const index = hashCode % max;
  return index;
}

5. 哈希表封装

经过前面这么多内容的铺垫,我们现在终于可以真正实现自己的哈希表了

5.1 整体框架 v1 版

class HashTable<T = any> {
  // 创建一个数组,用来存放链地址法中的链
  storage: [string, T][][] = [];
  // 定义数组的长度
  private length: number = 7;
  // 记录已经存放元素的个数
  private count: number = 0;
}

在上面代码中,我们定义了三个属性

  • storage:创建一个数组,用来存放链地址法中的链
  • length:定义数组的长度
  • count:记录已经存放元素的个数

5.2 添加 put 方法 v2 版

put 方法的作用是插入&修改数据

在哈希表中,插入和修改操作是同一个函数。

  • 因为,当使用者传入一个<key,value>
  • 如果原来不存在该 key,那么就是插入操作
  • 如果已经存在该 key,那么就是修改操作
put(key: string, value: T) {
  // 1.根据key 获取数组中对应的索引值
  const index = this.hashFunc(key, this.length);
  // 2. 取出索引值对应位置的数组
  let bucket = this.storage[index];
  // 3. 判断 bucket 是否有值
  if (!bucket) {
    bucket = [];
    this.storage[index] = bucket;
  }
  // 4. 确定已经有一个数组了,但是数组中是否已经存在 key 时不确定的
  let isUpdate = false;
  for (let i = 0; i < bucket.length; i++) {
    const tuple = bucket[i];
    const tupleKey = tuple[0];
    if (tupleKey === key) {
      // 修改/更新的操作
      tuple[1] = value;
      isUpdate = true;
    }
  }
  // 5. 如果上面的代码没有进行覆盖,那么在该位置进行添加
  if (!isUpdate) {
    bucket.push([key, value]);
    this.count++
  }
}

测试:

const hashTable = new HashTable();
hashTable.put("aaa", 200);
hashTable.put("bbb", 300);
hashTable.put("ccc", 400);

上面 hashTable 的结构可以用下图来表示:(aaabbbccc 可能在一个 bucket 中,也可能不在,具体要看哈希函数得到的 index

5.3 添加 get 方法 v3 版

get 方法的作用是根据 key 获取对应的值

  get(key: string): T | undefined {
    // 1. 根据 key 获取索引值 index
    const index = this.hashFunc(key, this.length);
    // 2. 获取 bucket(桶)
    const bucket = this.storage[index];
    if (!bucket) return undefined;
    // 3. 对 bucket 进行遍历
    for (let i = 0; i < bucket.length; i++) {
      const tuple = bucket[i];
      const tupleKey = tuple[0];
      const tupleValue = tuple[1];
      if (tupleKey === key) {
        return tupleValue;
      }
    }
    return undefined;
  }

测试:

  get(key: string): T | undefined {
    // 1. 根据 key 获取索引值 index
    const index = this.hashFunc(key, this.length);
    // 2. 获取 bucket(桶)
    const bucket = this.storage[index];
    if (!bucket) return undefined;
    // 3. 对 bucket 进行遍历
    for (let i = 0; i < bucket.length; i++) {
      const tuple = bucket[i];
      const tupleKey = tuple[0];
      const tupleValue = tuple[1];
      if (tupleKey === key) {
        return tupleValue;
      }
    }
    return undefined;
  }

5.4 添加 delete 方法 v4 版

delete 方法的作用是根据 key 删除对应的值

  delete(key: string): T | undefined {
    // 1. 获取索引值的位置
    const index = this.hashFunc(key, this.length);
    // 2. 获取 bucket(桶)
    const bucket = this.storage[index];
    if (!bucket) return undefined;
    // 3. 对 bucket 进行遍历
    for (let i = 0; i < bucket.length; i++) {
      const tuple = bucket[i];
      const tupleKey = tuple[0];
      const tupleValue = tuple[1];
      if (tupleKey === key) {
        bucket.splice(i, 1);
        this.count--;
        return tupleValue;
      }
    }
    return undefined;
  }

测试:

const hashTable = new HashTable();
hashTable.put("aaa", 200);
hashTable.put("bbb", 300);
hashTable.put("ccc", 400);
hashTable.delete('aaa')
console.log(hashTable.get("aaa")); // undefined
console.log(hashTable.get("bbb")); // 300
console.log(hashTable.get("ccc")); // 400

6. 哈希表的自动扩容

目前,我们是将所有的数据项放在长度为 7 的数组中,因为我们使用的是链地址法,loadFactor 可以大于 1,所以这个哈希表是可以无限制的插入新数据的。

但是,随着数据量的增多,每一个 index 对应的 bucket 会越来越长,也就造成效率的降低,所以在合适的情况对数组进行扩容是很有必要的。

loadFactor 译为装载因子。装载因子用来衡量 HashMap 满的程度

如何进行扩容?

扩容可以简单的将容量增大两倍,但是这种情况下,所有的数据项一定要同时进行修改(重新调用哈希函数)

1. 调整代码,添加 resize 方法

private resize(newLength) {
  // 设置新的长度
  this.length = newLength;
  // 获取原来所有的数据,并且重新放入到新的容量数组中
  // 1. 对数据进行的初始化操作
  const oldStorage = this.storage;
  this.storage = [];
  this.count = 0;
  // 2. 获取原来数据,放入新的数组中
  oldStorage.forEach((bucket) => {
    if (!bucket) return;
    for (let i = 0; i < bucket.length; i++) {
      const tuple = bucket[i];
      this.put(tuple[0], tuple[1]);
    }
  });
}

上面的 resize 方法的作用就对 哈希表进行扩容,但是我们应该如何使用 resize 方法呢?

答案就是,我们可以在 putdelete 方法的最后判断一下当前哈希表的 loadFactor,比如:

  • loadFactor 大于 0.75 时,对哈希表进行两倍的扩容
  • loadFactor 小于 0.25 时,对哈希表进行两倍的缩容

2. 修改 put 方法

  put(key: string, value: T) {
    // 1.根据key 获取数组中对应的索引值
    const index = this.hashFunc(key, this.length);
    console.log({ index });
    // 2. 取出索引值对应位置的数组
    let bucket = this.storage[index];
    // 3. 判断 bucket 是否有值
    if (!bucket) {
      bucket = [];
      this.storage[index] = bucket;
    }
    // 4. 确定已经有一个数组了,但是数组中是否已经存在 key 时不确定的
    let isUpdate = false;
    for (let i = 0; i < bucket.length; i++) {
      const tuple = bucket[i];
      const tupleKey = tuple[0];
      if (tupleKey === key) {
        // 修改/更新的操作
        tuple[1] = value;
        isUpdate = true;
      }
    }
    // 5. 如果上面的代码没有进行覆盖,那么在该位置进行添加
    if (!isUpdate) {
      bucket.push([key, value]);
      this.count++;
+     // 发现 loadFactor 比例已经大于 0.75,那么就直接扩容
+     const loadFactor = this.count / this.length;
+     if (loadFactor > 0.75) {
+       this.resize(this.length * 2);
+     }
    }
  }

3. 修改 delete 方法

  delete(key: string): T | undefined {
    // 1. 获取索引值的位置
    const index = this.hashFunc(key, this.length);
    // 2. 获取 bucket(桶)
    const bucket = this.storage[index];
    if (!bucket) return undefined;
    // 3. 对 bucket 进行遍历
    for (let i = 0; i < bucket.length; i++) {
      const tuple = bucket[i];
      const tupleKey = tuple[0];
      const tupleValue = tuple[1];
      if (tupleKey === key) {
        bucket.splice(i, 1);
        this.count--;
+       // 发现 loadFactor 比例已经小于 0.75,那么就直接缩容
+       const loadFactor = this.count / this.length;
+       if (loadFactor < 0.25 && this.length > 7) {
+         this.resize(Math.floor(this.length / 2));
+       }
        return tupleValue;
      }
    }
    return undefined;
  }

测试:

const hashTable = new HashTable();
hashTable.put("aaa", 200);
hashTable.put("bbb", 300);
hashTable.put("ccc", 400);
hashTable.put("ddd", 500);
hashTable.put("eee", 600);
console.log(hashTable.storage.length); // 7
hashTable.put("fff", 600);
console.log(hashTable.storage.length); // 14
hashTable.delete("fff");
hashTable.delete("eee");
console.log(hashTable.storage.length); // 14
hashTable.delete("ddd");
console.log(hashTable.storage.length); // 7

以上就是TypeScript 基础数据结构哈希表 HashTable教程的详细内容,更多关于TypeScript 数据结构HashTable的资料请关注我们其它相关文章!

(0)

相关推荐

  • 数据结构TypeScript之链表实现详解

    目录 链表结构特点 面向对象方法封装链表 构造函数 基本单元:链表节点 主体:链表 查找节点 增加节点 删除节点 链表结构特点 链表是线性表的其中一种,用于存储有固定顺序的元素.而元素之间会通过”链“连接在一起. 链表存储的元素称为节点.每个节点内部存在两个值.如下: this.element:链表需要存储的单个元素 this.next:指向下一个节点,也就是链表中的”链“,将节点连接在一起. 尝试手动构建链表结构.过程如下: class LinkedListNode { constructor

  • 数据结构TypeScript之二叉查找树实现详解

    目录 树的结构特点 面向对象方法封装二叉查找树(迭代版) 二叉查找树的定义 构造函数 基本单元:二叉查找树节点 主体:二叉查找树 增加节点 查找节点 删除节点 二叉树的遍历 树的结构特点 树是一种有层次的数据结构,通常用于存储具有层次性的数据.比如上下级的关系图,动物的分类图等.树的类型分好几种,无序树.有序树和二叉树等等.但最常应用的还是二叉树,其特点为每个节点最多含有两个子树. 尝试手动构建一颗二叉树. 过程如下: class BinaryTreeNode { constructor(ele

  • 数据结构TypeScript之邻接表实现示例详解

    目录 图的结构特点 图的分类 图的表示 面向对象方法封装邻接表 构造函数 增加顶点和边 删除顶点和边 图的遍历 颜色标记 广度优先搜索(队列) 深度优先搜索(栈) 图的结构特点 图由顶点和顶点之间的边构成,记为G(V, E).其中V是顶点集合,E是边集合.作为一种非线性的数据结构,顶点之间存在多对多关系. 图的分类 无向图:两个顶点之间有两条互相关联的边.A和B之间为双向互通. 有向图:两个顶点之间有一条或两条关联的边.从A到B或者从B到A,只能单向通过. 带权无向图:在无向图的基础上增加一个权

  • TypeScript数据结构链表结构 LinkedList教程及面试

    目录 1. 认识链表 2. 实现链表结构的封装 2.1 基础框架 v1 版 2.2 添加 append 方法 v2 版 2.3 添加 traverse 方法 v3 版 2.4 添加 insert 方法 v4 版 2.5 添加 removeAt 方法 v5 版 2.6 添加 get 方法 v6 版 2.7 添加 getNode 方法 v7 版 2.8 添加 update 方法 v8 版 2.9 添加 indexOf 方法 v9 版 2.10 添加 remove 方法 v10 版 2.11 添加方法

  • TypeScript数据结构栈结构Stack教程示例

    目录 1. 认识栈结构 2. 实现栈结构的封装 2.1 基于数组 v1 版 2.2 使用泛型重构 v2 版 3. 实战一:有效的括号 3.1 题目描述 3.2 题目分析 3.3 解一:栈 4. 实战二:下一个更大元素 I 4.1 题目描述 4.2 解一:暴力 4.3 解二:单调栈 1. 认识栈结构 栈是一种 后进先出(LIFO) 的数据结构 在 js 中没有栈,但我们可以用 数组或链表 实现栈的所有功能 栈的常用操作: push(入栈) pop(出栈) peek(返回栈顶元素) isEmpty(

  • TypeScript数据结构之队列结构Queue教程示例

    目录 1. 认识队列结构 2. 实现队列结构封装 3. 实战一:最近的请求次数 3.1 题目描述 3.2 解一:队列 4. 实战二:无法吃午餐的学生数量 4.1 题目描述 4.2 解一:队列 5. 实战三:字符串中的第一个唯一字符 5.1 题目描述 5.2 解一:哈希表 5.3 解二:队列 1. 认识队列结构 队列是一个 先进先出(FIFO) 的数据结构 js 中没有队列,但我们可以用 数组或链表 实现队列的所有功能 队列的常用操作: enqueue(element):向队列尾部添加一个(多个)

  • TypeScript 基础数据结构哈希表 HashTable教程

    目录 前言 1. 哈希表介绍和特性 2. 哈希表的一些概念 3. 地址冲突解决方案 3.1 方案一:链地址法 3.2 方案二:开放地址法 4. 哈希函数代码实现 5. 哈希表封装 5.1 整体框架 v1 版 5.2 添加 put 方法 v2 版 5.3 添加 get 方法 v3 版 5.4 添加 delete 方法 v4 版 6. 哈希表的自动扩容 前言 哈希表是一种 非常重要的数据结构,几乎所有的编程语言都有 直接或者间接 的应用这种数据结构. 很多学习编程的人一直搞不懂哈希表到底是如何实现的

  • C#中哈希表(HashTable)用法实例详解(添加/移除/判断/遍历/排序等)

    本文实例讲述了C#中哈希表(HashTable)用法.分享给大家供大家参考,具体如下: 1.  哈希表(HashTable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似keyvalue的键值对,其中key通常可用来快速查找,同时key是区分大小写:value用于存储对应于key的值.Hashtable中keyvalue键值对均为object类型,所以Hashtable可以支持任何类型的keyvalue键

  • C语言数据结构哈希表详解

    /* * 程序名:hash.c,此程序演示哈希表的实现,数据元素单链表带头结点. * */ #include <stdio.h> #include <stdlib.h> #include <string.h> // 哈希表中数据元素的结构体. typedef struct Element { unsigned int key; // 关键字. int value; // 数据元素其它数据项,可以是任意数据类型. // char value[1001]; // 数据元素其

  • javascript 哈希表(hashtable)的简单实现

    首先简单的介绍关于属性的一些方法: 属性的枚举: for/in循环是遍历对象属性的方法.如 复制代码 代码如下: var obj = { name : 'obj1', age : 20, height : '176cm' } var str = ''; for(var name in obj) { str += name + ':' + obj[name] + '\n'; } alert(str); 输出为:name:obj1 age:20 height:176cm 检查属性是否存在: in运算

  • C++数据结构哈希表详解

    目录 实现 散列函数 开散列方法 闭散列方法(开地址方法) 删除* 实现 哈希表,即散列表,可以快速地存储和查询记录.理想哈希表的存储和查询时间都是 O(1). 本<资料>中哈希表分以下几部分:散列函数.存储和查找时的元素定位.存储.查找.删除操作因为不常用,所以只给出思想,不给出代码. 根据实际情况,可选择不同的散列方法. 以下代码假设哈希表不会溢出. // N表示哈希表长度,是一个素数,M表示额外空间的大小,empty代表"没有元素". const int N=9997

  • Redis之常用数据结构哈希表

    目录 1.哈希冲突 2.链式哈希 3.rehash 4.渐进式 rehash 5.rehash 触发条件 哈希表是一种保存键值对(key-value)的数据结构 哈希表优点在于,它能以 O(1) 的复杂度快速查询数据. 怎么做到的呢? 将 key 通过 Hash 函数的计算,就能定位数据在表中的位置,因为哈希表实际上是数组,所以可以通过索引值快速查询到数据. 在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高. Redis 采用了**「链式哈希」**来解决哈希冲突,在不扩容

  • C#中哈希表(Hashtable)的介绍及简单用法

    key通常可用来快速查找,同时key是区分大小写:value用于存储对应于key的值.Hashtable中key/value键值对均为object类型,所以Hashtable可以支持任何类型的key/value键值对 <BR><BR><BR>在哈希表中添加一个key/value键值对:HashtableObject.Add(key,value); 在哈希表中去除某个key/value键值对:HashtableObject.Remove(key); 从哈希表中移除所有元素

  • 简单讲解哈希表

    目录 一.哈希表的概念 1.查找算法 2.哈希表 3.哈希数组 4.关键字 5.哈希函数 6.哈希冲突 7.哈希地址 二.常用哈希函数 1.直接定址法 2.平方取中法 3.折叠法 4.除留余数法 5.位与法 三.常见哈希冲突解决方案 1.开放定址法 1)原理讲解 2)动画演示 2.再散列函数法 1)原理讲解 2)动画演示 3.链地址法 1)原理讲解 2)动画演示 4.公共溢出区法 1)原理讲解 2)动画演示 四.哈希表的实现 1.数据结构定义 2.哈希表初始化 3.哈希函数计算 4.哈希表查找

  • php内核解析:PHP中的哈希表

    PHP中使用最为频繁的数据类型非字符串和数组莫属,PHP比较容易上手也得益于非常灵活的数组类型. 在开始详细介绍这些数据类型之前有必要介绍一下哈希表(HashTable). 哈希表是PHP实现中尤为关键的数据结构. 哈希表在实践中使用的非常广泛,例如编译器通常会维护的一个符号表来保存标记,很多高级语言中也显式的支持哈希表. 哈希表通常提供查找(Search),插入(Insert),删除(Delete)等操作,这些操作在最坏的情况下和链表的性能一样为O(n). 不过通常并不会这么坏,合理设计的哈希

  • JS模拟实现哈希表及应用详解

    本文实例讲述了JS模拟实现哈希表及应用.分享给大家供大家参考,具体如下: 在算法中,尤其是有关数组的算法中,哈希表的使用可以很好的解决问题,所以这篇文章会记录一些有关js实现哈希表并给出解决实际问题的例子. 说明: 这篇文章所写并不是真正意义的哈希表,只是与哈希表的使用有相似之处. 第一部分:相关知识点 属性的枚举: var person = { name: "zzw", sex: "Male", age: 21 }; for (var prop in person

随机推荐