算法随笔-数据结构(哈希表)

144 阅读6分钟

算法随笔-数据结构(哈希表)

本文主要介绍数据结构中哈希表在生活中的应用、主要特点、ES6如何实现HashTable类及一些leetcode真题解析。供自己以后查漏补缺,也欢迎同道朋友交流学习。

引言

哈希表(Hash Table),也称为散列表,是一种通过哈希函数将键(Key)映射到表中一个位置以便快速访问记录的数据结构。

哈希表听起来我们可能稍微陌生一点,但在生活中超市收银扫的商品条形码、图书馆的索引、手机通讯录都是哈希表的应用之一,它的本质就是在某个存储空间中存储数据,通过键值对来定位数据

对于前端来说,简单的对象或者ES6MapSet等数据结构都可以认为是哈希表的一种应用,其实就是keyvalue的键值对。

所以,上一章介绍的字典就是哈希表的一种应用。

可以详看我写的算法随笔-数据结构(字典)

主要特点

  • 键值对存储:存储数据的方式是键值对(Key-Value),其中键是唯一的,用于通过哈希函数计算存储位置。
  • 快速访问:哈希表通过键(Key)映射到表中的一个位置,访问速度非常快,平均时间复杂度为O(1)
  • 解决冲突:由于哈希函数可能将不同的键映射到同一个位置,哈希表需要有机制来解决这种冲突。常见的方法包括链地址法(Chaining)开放寻址法(Open Addressing)
  • 动态调整大小:为了保持操作的效率,哈希表可以根据负载因子(已存储元素数量与表大小的比率)动态地调整其大小。
  • 非顺序存储:与数组或链表不同,哈希表不保持元素的任何特定顺序。这意味着它不适合需要按顺序处理元素的应用。
  • 空间效率:哈希表通常需要额外的空间来存储哈希桶(Buckets)链表,这可能会增加存储成本,但为了快速访问数据,这种成本通常是值得的。

ES6实现HashTable类

借助 ES6 的 Class 实现一个更接近传统哈希表的自定义HashTable类:

class HashTable {
  constructor() {
    this.table = [];
  }

  hashCode ( key ) {
    let hash = 0;
    for (let i = 0; i < key.length; i++) {
      hash = (hash + key.charCodeAt(i) * i) % 37;
      // key.charCodeAt(i): 这部分获取键字符串中第i个字符的Unicode编码

      // * i:将字符的Unicode编码乘以它在字符串中的位置i。
      // 这样做可以增加不同字符位置的权重,使得相同字符在不同位置对最终哈希值的贡献不同。

      // hash + ...:将当前字符的贡献累加到哈希值上。初始时,hash可能被设置为0。

      // % 37:取模运算确保哈希值在0到36之间,这是因为哈希表的大小被设定为37(这是一个素数,通常选择素数作为哈希表的大小,因为它们在哈希分布上通常表现得更好)。
      // 取模运算的结果将哈希值限制在哈希表的索引范围内。
    }

    return hash;
  }

  set( key, val ) {
    let hashKey = this.hashCode(key);
    this.table[hashKey] = val;
  }

  get(key) {
    let hashKey = this.hashCode(key);
    return this.table[hashKey];
  }

  delete(key) {
    let hashKey = this.hashCode(key);
    if (this.table[hashKey]) {
      delete this.table[hashKey];
      return true;
    }
    return false;
  }
}

let myHashTable = new HashTable();
myHashTable.set('key1', 'value1');
myHashTable.get('key1'); // "value1"
myHashTable.delete('key1'); // true
myHashTable.delete('key2'); // false

leetcode真题解析

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出为目标值 target 的那两个整数,并返回它们的数组下标

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

示例 1

输入:nums = [2,7,11,15], target = 9

输出:[0,1]

解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2

输入:nums = [3,2,4], target = 6

输出:[1,2]

示例 3

输入:nums = [3,3], target = 6

输出:[0,1]

题解1

利用 JS 的对象存储下标:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
  // 利用obj存储值的index下标
  const obj = {}
  
  for (let i = 0; i < nums.length; i++) {
    // 值
    const number = nums[i];
    // 差值
    const curNumber = target - number;
    // 判断在hashMap里有没有值
    if (obj[curNumber] !== undefined) {
      return [ obj[curNumber], i];
    } else {
      // 没有就存obj
      obj[number] = i
    }
  }
};

题解2

利用 ES6Map 存储下标:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
  // 利用hashMap存储值的index下标
  const hashMap = new Map()
  
  for (let i = 0; i < nums.length; i++) {
    // 值
    const number = nums[i];
    // 差值
    const curNumber = target - number;
    // 判断在hashMap里有没有值
    if (hashMap.has(curNumber)) {
      return [ hashMap.get(curNumber), i];
    } else {
      // 没有就存hashMap
      hashMap.set(number, i)
    }
  }
};

217. 存在重复元素

给你一个整数数组 nums 。如果任一值在数组中出现至少两次 ,返回true;如果数组中每个元素互不相同,返回false

示例 1

输入:nums = [1,2,3,1]

输出:true

解释:元素 1 在下标 0 和 3 出现。

示例 2

输入:nums = [1,2,3,4]

输出:false

解释:所有元素都不同。

示例 3

输入:nums = [1,1,1,3,3,4,3,2,4,2]

输出:true

题解

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var containsDuplicate = function(nums) {
  let hashMap = new Map();
  for (let item of nums) {
    if (hashMap.has(item)) {
      return true;
    }
    hashMap.set(item)
  }
  return false
};

3. 无重复字符的最长子串

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

示例 1

输入:s = "abcabcbb"

输出:3

解释:因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2

输入:s = "bbbbb"

输出:1

解释:因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3

输入:s = "pwwkew"

输出:3

解释:因为无重复字符的最长子串是 "wke",所以其长度为 3。

题解

思路:利用滑动窗口去找最长子串,用一个滑动窗口来维护一个子串,窗口的左边界和右边界。

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
  let start = 0; // 窗口的左边界
  let maxLen = 0; // 最长子串的长度
  let charMap = {}; // 存储字符及其索引的映射
  
  for (let i = 0; i < s.length; i++) {
    const char = s[i];
    // 如果字符已经在映射中,并且它的索引在当前窗口内(即比开始索引大),则移动开始索引
    if (charMap[char] !== undefined && charMap[char] >= start) {
      start = charMap[char] + 1;
    }
    // 更新字符的索引
    charMap[char] = i;
    console.log('@@@@ charMap', charMap)
    // 更新最长子串的长度
    maxLen = Math.max(maxLen, i - start + 1);
  }

  return maxLen;
};