JavaScript 手搓 简易 HashTable

374 阅读4分钟

今日要搓什么

HashTable。

Hash 值的计算称之为 Hash Function,鉴于这个不是今天得主要研究目的(之后可能会开坑吧),今天的 Hash Function 就用一个简单的方法,通过输入 String,返回 Number 来实现。

目标方法

这个 HashTable 要实现的有

.setItem(key, value);	// set key-value pair
.getItem(key);	// get value by key
.size();	// return table size
.deleteItem(key);	// remove specific key-value pair by key, if success return true
.containsKey(key);	// return true if the table contains this key
.occupancyRate();	// return how the table is full
.efficiency();	// 100% -> good!, 50% -> =(, 10% -> mange de la merde!

思路

目的是实现一个可以保存键值对的结构,还能够通过 key 来存取 value。那么首先想到的是用 JS 的数组来存,我们要将这个值存到特定的 index 下。这就需要通过一个方法来计算 string 类型的 key,从而得到一个 number 类型的 index。而且每次输入同样的字符串,都要获得同样的返回值才行。并且不同的字符串尽量让返回的 number 平均的在 range 里分布。

函数签名如下:

// convert a string to a integer within the range of [0 - range)
.getStringToNumberHash(string, range);	

但是这样又会带来一个问题,当两个不同的字符串计算后的 Hash 值相等时(这种情况时会发生的),我们无法在同一个 index 下存入多个 value。如此一来我们就需要改变我们只存 value 的现状,将特定 index 下存的数据改为一个数组,这个数组里也不只存 value 了,而是存一个键值对。举个例子,在某个情况下,当 index 1 下面存了两个键值对时,就会有table[1] = [['key1', 'value1'], ['key2', 'value2']]

逐步实现

首先来写最简单的 getStringToNumberHash(), Hash 值得计算方法这里也不赘述(其实是我还没研究太明白)。遵循一切都往上怼质数的原则(为了尽量防止多个字符串计算后的结果是同一个 index。比如这里用 2 来乘,岂不是奇数 index 就没机会了,偶数 index 则又疯狂重复),我们用一个质数作为初始 Hash 值,遍历字符串的每一位,每次自身 × 字符串当前字符的ASCII,最后模传入的 range 参数。

/**
 * convert a string to a integer within the range of [0 - range)
 * @param {string} str
 * @param {number} range
 * @returns
 */
const getStringToNumberHash = (str, range) => {
  let hash = 17;
  for (let i = 0; i < str.length; i++) {
    hash = (13 * hash * str.charCodeAt(i)) % range;
  }
  return hash;
};

好了现在来写主体框架。

class HashTable {
  table = new Array(11);
  keyCount = 0;
  itemCount = 0;

  resize = () => {
    // expand when (item / table length) reached threshold
  };
  
  setItem = (key, value) => {
  };
  
  getItem = (key) => {
  };
  
  size = () => {
  };
  
  deleteItem = (key) => {
  };
  
  containsKey = (key) => {
  };

  occupancyRate = () => {
  };
  
  efficiency = () => {
  };
}

这里面的 table 数组的每一个坑儿用来存储我们之前所说的结构是 [[key, value],...] 的二维数组。itemCount 代表占用了多少个坑位,keyCount 代表已经存了多少个键值对儿进来。

方法 resize() 其实在这里也可以写作 _resize,因为这个按理说应该是个私有方法,目的是当 table 数组中坑位到达一定程度时我们就要在它满了之前,提前扩容数组(类似于上厕所的人的数量到达八成满就扩充坑位)。再说外部 call 这个也没用,因为数组的扩容是在每次插入新的键值对时自动检测,按需扩容的。

倒数第二个方法 occupancyRate() 就是用来返回整个 HashTable 的占有率的。

最后一个 efficiency() 是用 keyCountitemCount 计算得出。因为我们的设计并不是最完美的,可能存在两个及以上的键值对共用一个坑的情况,这时候 keyCount 就会大于 itemCount。所以和 itemCount 相比较时,keyCount 越大说明利用率越低,万人共用一个坑儿的情况在超坏的设计里甚至也成为了可能。

完整代码

class HashTable {
  table = new Array(11);
  keyCount = 0;
  itemCount = 0;

  resize = () => {
    // expand when (item / table length) reached threshold
    if (this.keyCount / this.table.length > 0.8) {
      const newTable = new Array(this.table.length * 2 + 1);
      this.table.forEach((item) => {
        if (item) {
          item.forEach((pair) => {
            const index = getStringToNumberHash(pair[0], newTable.length);
            if (newTable[index]) {
              newTable[index].push([pair[0], pair[1]]);
            } else {
              newTable[index] = [[pair[0], pair[1]]];
            }
          });
        }
      });
      this.table = newTable;
    }
  };

  // if key is already exist, simply replace the value of the pair with the one in parameters
  setItem = (key, value) => {
    this.keyCount++;

    const index = getStringToNumberHash(key, this.table.length);
    if (this.table[index]) {
      let hasKey = false;
      for (let i = 0; i < this.table[index].length; i++) {
        if (this.table[index][i][0] === key) {
          this.table[index][i] = [key, value];
          hasKey = true;
          break;
        }
      }
      if (!hasKey) {
        this.table[index].push([key, value]);
      }
    } else {
      this.itemCount++;
      this.table[index] = [[key, value]];
    }

    this.resize();
  };

  getItem = (key) => {
    const index = getStringToNumberHash(key, this.table.length);
    if (!this.table[index]) {
      return null;
    }

    // O(n)
    return this.table[index].find((x) => x[0] === key)[1];
  };

  size = () => {
    return this.table.length;
  };

  deleteItem = (key) => {
    const index = getStringToNumberHash(key, this.table.length);
    if (!this.table[index]) {
      return false;
    }
    let deleted = false;
    for (let i = 0; i < this.table[index].length; i++) {
      if (this.table[index][i][0] === key) {
        this.table[index].splice(i, 1);
        deleted = true;
        break;
      }
    }
    if (this.table[index].length === 0) {
      this.itemCount--;
    }
    this.keyCount--;
    return deleted;
  };

  containsKey = (key) => {
    const index = getStringToNumberHash(key, this.table.length);
    if (!this.table[index]) {
      return false;
    }

    // if no satisfied element, find() will return undefined
    return this.table[index].find((x) => x[0] === key) === undefined
      ? false
      : true;
  };

  occupancyRate = () => {
    return `${(this.itemCount / this.table.length).toFixed(2) * 100}%`;
  };

  efficiency = () => {
    return `${(this.itemCount / this.keyCount).toFixed(2) * 100}%`;
  };
}

注意事项

这里需要注意的是,当我们删除一个键值对时,可能因为这个被删除的键值对是当前坑儿的二维数组唯一的一个子元素,导致二维跌落成一维。这时候我们就要把 itemCount 相应的减去 1,因为鱼消失了,水洼也干涸了

如有任何错误,烦请指正 =D

如有任何建议,欢迎提出 XD

顺颂码祺