Typescript实现散列表数据结构

65 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 12 月更文挑战」的第 6 天,点击查看活动详情

最近在学习 Javascript 数据结构与算法相关知识,数据结构与算法对程序员来说就像是内功心法,只有不断修炼自己的内功,才能从容不迫的应对市场上各种让人应接不暇的框架,达到以不变应万变。学习数据结构与算法的过程中不仅要能看懂更要多写多练,今天就来手写下散列表数据结构。

字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。字典也称作映射、符号表或关联数组。 散列算法的作用是尽可能快地在数据结构中找到一个值。

JavaScript 语言内部就是使用散列表来表示每个对象。此时,对象的每个属性和方法(成员)被存储为 key 对象类型,每个 key 指向对应的对象成员。

散列表的基本操作方法:

  • put(key,value):向散列表增加一个新的项(也能更新散列表)。
  • remove(key):根据键值从散列表中移除值。
  • get(key):返回根据键值检索到的特定的值。
  • getTable():返回散列表引用
  • isEmpty():是否为空
  • size():返回散列表存储的值的个数
  • clear():清除散列表
  • toString():输出散列表

手写散列表

class ValuePair<K, V> {
  constructor(public key: K, public value: V) {}

  toString() {
    return `[#${this.key}: ${this.value}]`;
  }
}
class HashTable<K, V> {
  protected table: { [key: string]: ValuePair<K, V> };
  constructor() {
    this.table = {};
  }
  private toStrFn(item: any): string {
    if (item === null) {
      return "NULL";
    } else if (item === undefined) {
      return "UNDEFINED";
    } else if (typeof item === "string" || item instanceof String) {
      return `${item}`;
    }
    return item.toString();
  }
  // 散列函数
  private djb2HashCode(key: K) {
    const tableKey = this.toStrFn(key);
    let hash = 5381;
    for (let i = 0; i < tableKey.length; i++) {
      hash = hash * 33 + tableKey.charCodeAt(i);
    }
    return hash % 1013;
  }
  hashCode(key: K) {
    return this.djb2HashCode(key);
  }
  put(key: K, value: V) {
    if (key !== null && value !== null) {
      const hash = this.hashCode(key); // hash是索引
      this.table[hash] = new ValuePair(key, value); // 将键值对存入
      return true;
    }
    return false;
  }
  get(key: K) {
    const hash = this.hashCode(key);
    const valuePair = this.table[hash];
    return valuePair == null ? undefined : valuePair.value;
  }
  remove(key: K) {
    const hash = this.hashCode(key);
    const valuePair = this.table[hash];
    if (valuePair !== null) {
      delete this.table[hash];
      return true;
    }
    return false;
  }
  size() {
    return Object.keys(this.table).length;
  }
  getTable() {
    return this.table;
  }
  isEmpty() {
    return this.size() === 0;
  }
  clear() {
    this.table = {};
  }
  toString() {
    if (this.isEmpty()) {
      return "";
    }
    const keys = Object.keys(this.table);
    let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
    for (let i = 1; i < keys.length; i++) {
      objString = `${objString},{${keys[i]} => ${this.table[
        keys[i]
      ].toString()}}`;
    }
    return objString;
  }
}
const hash = new HashTable();
hash.put("Gandalf", "gandalf@email.com");
hash.put("John", "johnsnow@email.com");
hash.put("Tyrion", "tyrion@email.com");
console.log(hash.hashCode("Gandalf") + " - Gandalf");
console.log(hash.hashCode("John") + " - John");
console.log(hash.hashCode("Tyrion") + " - Tyrion");
console.log(hash.get("Gandalf")); // gandalf@email.com
console.log(hash.get("Loiane")); // 不存在于表中 返回undefined
console.log(hash.toString());

我们知道这里散列表之所以能够快速找到存储的值是因为有唯一的键,可是有时候散列函数会存在不同的输入输出相同的结果,即存在散列冲突,解决散列冲突可以使用分离链接和线性探查,这里就不介绍了.

使用场景

leetcode 练习题:3

无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function (s) {
  const map = new Map(); // map数据结构记录字符是否出现过
  const len = s.length;
  let rp = -1, // 初始右指针位置-1
    res = 0; // 用来记录最长子串长度
  for (let i = 0; i < len; i++) {
    if (i !== 0) {
      map.delete(s.charAt(i - 1)); // 左指针向右移动一格,移除一个字符,直到左右指针位置相等 散列表中数据为空
    }
    while (rp + 1 < len && !map.has(s.charAt(rp + 1))) {
      map.set(s.charAt(rp + 1), 1);
      rp += 1; // 右指针向前遍历
    }
    res = Math.max(res, rp - i + 1);
  }
  return res;
};

解题思路
我们使用两个指针表示字符串中的某个子串的左右边界,其中左指针代表着上文中枚举子串的起始位置,而右指针即为上文中的 rp; 在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。 在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;

在枚举结束后,我们找到的最长的子串的长度即为答案。