前端学算法-哈希表

243 阅读12分钟
介绍

哈希表(Hash Table)是一种基于哈希函数实现的高效数据结构,用于存储键值对(Key-Value)。它通过将键(Key)映射到表中的一个位置(称为桶或槽),实现快速的数据访问、插入和删除操作。理想情况下,这些操作的时间复杂度为 O(1)

基本概念
  • 哈希函数(Hash Function): 将键(Key)转换为一个固定大小的整数(哈希值),用于确定数据在表中的存储位置。 示例

    • "apple" 经过哈希函数后可能映射到索引 3
    • "banana" 可能映射到索引 7
  • 桶(Bucket)或槽(Slot): 哈希表中的存储单元,用于存放键值对。哈希函数的结果决定了键值对存储在哪个桶中。

  • 哈希冲突(Hash Collision): 当不同的键映射到同一个桶时发生。哈希表需要通过冲突处理策略解决这一问题。

基本操作
  • 插入(Insert):

    1. 计算键的哈希值,确定存储位置。
    2. 如果该位置已被占用,处理冲突(如链地址法或开放寻址法)。
    3. 将键值对存入该位置。
  • 查找(Search):

    1. 计算键的哈希值,定位到存储位置。
    2. 处理冲突(如果存在),找到对应的键值对。
    3. 返回对应的值(Value)。
  • 删除(Delete):

    1. 计算键的哈希值,定位到存储位置。
    2. 处理冲突(如果存在),找到对应的键值对。
    3. 从表中移除该键值对。
算法

有个非常经典的一句话: 当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。

有效的字母异位词

力扣题目链接

给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的 字母异位词。

示例 1:

输入: s = "anagram", t = "nagaram"
输出: true

这个题算是简单难度的,如果不考虑时间复杂度啥的,直接字符串排序一把梭哈,比如这样

var isAnagram = function(s, t) {
    let s1 = s.split('').sort().join('')
    let t1 = t.split('').sort().join('')
    return s1 == t1
};

可能这也算是他归于简单难度的原因吧。

学习下通过使用hash表来解答下这个问题

哈希表计数法
function isAnagram(s, t) {
  if (s.length !== t.length) return false;
​
  const count = {};
  for (const char of s) {
    count[char] = (count[char] || 0) + 1;
  }
  for (const char of t) {
    if (!count[char]) return false;
    count[char]--;
  }
​
  return true;
}

这里就定义一个map,开始遍历第一个字符串smap中的key就是每个单独的字符串,值就是出现的次数;然后开始遍历第二个,每遍历到就减1,如果不存在就直接返回false

数组计数法
function isAnagram(s, t) {
    if (s.length !== t.length) {
        return false;
    }
 
    const count = new Array(26).fill(0);
    const aCode = 'a'.charCodeAt(0);
 
    for (const char of s) {
        const index = char.charCodeAt(0) - aCode;
        count[index]++;
    }
 
    for (const char of t) {
        const index = char.charCodeAt(0) - aCode;
        count[index]--;
        if (count[index] < 0) {
            return false;
        }
    }
 
    return true;
}

这个方法和哈希表计数法没啥区别,只是通过数组来统计字母出现的频率,首先初始化一个数组,遍历第一个字符串,统计字母出现的频率,然后开始遍历第二个,将相同的字母挨个减,如果出现负数,就直接返回false

两个数组交集

给定两个数组 nums1nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

暴力解法

这个题算是力扣为数不多我可以称之为简单的,直接两个for循环就可以了

function intersection(nums1, nums2) {
  const set1 = new Set(nums1);
  const set2 = new Set(nums2);
  const result = new Set();
 
  for (const num of set1) {
    if (set2.has(num)) {
      result.add(num);
    }
  }
 
  return Array.from(result);
}
哈希表

使用哈希表来记录数组中的元素,遍历另一个,检查元素是否在哈希表中

function intersection(nums1, nums2) {
  const map = {};
  const result = [];
 
  // 将 nums1 中的元素存入哈希表
  for (const num of nums1) {
    map[num] = true;
  }
 
  // 遍历 nums2,检查元素是否在哈希表中
  for (const num of nums2) {
    if (map[num]) {
      result.push(num);
      delete map[num]; // 确保结果唯一
    }
  }
 
  return result;
}
快乐数

力扣题目链接

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n快乐数 就返回 true ;不是,则返回 false

示例 1:

输入:n = 19
输出:true
解释:
1^2+ 9^2 = 82
8^2 + 2^2 = 68
6^ + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
var getSum = function (n) {
    let sum = 0;
    while (n) {
        sum += (n % 10) ** 2;
        n =  Math.floor(n/10);
    }
    return sum;
}
var isHappy = function(n) {
    let set = new Set();   // Set() 里的数是惟一的
    // 如果在循环中某个值重复出现,说明此时陷入死循环,也就说明这个值不是快乐数
    while (n !== 1 && !set.has(n)) {
        set.add(n);
        n = getSum(n);
    }
    return n === 1;
};

这个题目一开始没思路,看了题解就发现其实也不难。

先定义工具函数getSum,这个就是快乐数求和的过程,核心代码就两行,先取n最后一位的平方和累加到sum上,sum += (n % 10) ** 2;,然后从右向左取下一位n = Math.floor(n/10);,然后再定义一盒set当做hash表,开始循环:如果在循环中某个值重复出现,说明此时陷入死循环,也就说明这个值不是快乐数。

两数之和

力扣题目链接

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

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]
var twoSum = function (nums, target) {
  let hash = {};
  for (let i = 0; i < nums.length; i++) {  // 遍历当前元素,并在map中寻找是否有匹配的key
    if (hash[target - nums[i]] !== undefined) {
      return [i, hash[target - nums[i]]];
    }
    hash[nums[i]] = i;   // 如果没找到匹配对,就把访问过的元素和下标加入到map中
  }
  return [];
};

这里先定义一个map,key就是数组中的项,value就是数组index,因为后续返回的就是数组下标。然后每次遍历时判断下 hash[target - nums[i],看定义的map中是否有值,如果有值说明找到了,如果没有就添加。

做这种题目有一个关键的思想就是取反,题目说是数组求和,常规做法就是遍历两次数组,求和同target比较。这种hash表的题目,一般都是将值存到hash表里,判断表里有没有值,比如这个题目,将值存放到hash表里,然后判断hash[target - nums[i]] !== undefined,这里没有求和,而是取差值。

四数之和

力扣题目链接

给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

这个题刚一看还是有点懵的,看完题解就恍然大悟,先计算nums1nums2,两个for循环,将计算的结果当做key,value就是出现的次数,然后再遍历nums3nums4,同样两个for循环,找差值,因为要相加等于0,找到如果有twoSumMap.get(0 - sum)就可以了。

var fourSumCount = function(nums1, nums2, nums3, nums4) {
    // 创建一个哈希表来存储nums1和nums2中元素和的出现次数
    const twoSumMap = new Map();
    let count = 0; // 初始化计数器
    
    // 统计nums1和nums2数组元素之和,和出现的次数,放到map中
    for(const n1 of nums1) {
        for(const n2 of nums2) {
            const sum = n1 + n2;
            // 如果sum已存在,增加计数;否则初始化为1
            twoSumMap.set(sum, (twoSumMap.get(sum) || 0) + 1)
        }
    }
    
    // 遍历nums3和nums4,查找互补的和
    for(const n3 of nums3) {
        for(const n4 of nums4) {
            const sum = n3 + n4;
            // 查找0 - (n3 + n4)在twoSumMap中的出现次数
            // 如果存在则累加,否则加0
            count += (twoSumMap.get(0 - sum) || 0)
        }
    }
 
    return count; // 返回满足条件的四元组总数
};
赎金信

力扣题目链接

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

输入:ransomNote = "aa", magazine = "aab"
输出:true

题干有点花里胡哨的,换成好理解的就是:给两个字符串ransomNotemagazine,判断 ransomNote 能不能由 magazine 里面的字符构成

这个也算是比较简单,首先的想法:遍历两个字符串,取出所有的不重复字符串,看看前者是不是后者的子集,如果是就返回true,不然就返回false。

var canConstruct = function(ransomNote, magazine) {
    const ransomSet = new Set(ransomNote);
    const magazineSet = new Set(magazine);
 
    // 检查 ransomNote 中的所有字符是否都在 magazine 中
    for (const char of ransomSet) {
        if (!magazineSet.has(char)) {
            return false;
        }
    }
 
    return true;
};

发现测试不通过。低估了这个题目,有个关键点magazine 中的每个字符只能在 ransomNote 中使用一次。

老老实实看题解:

var canConstruct = function(ransomNote, magazine) {
    const magCount = {};
 
    // 统计 magazine 中每个字符的数量
    for (let i = 0; i < magazine.length; i++) {
        const char = magazine[i];
        magCount[char] = (magCount[char] || 0) + 1;
    }
 
    // 检查 ransomNote 中的每个字符是否在 magazine 中有足够的数量
    for (let j = 0; j < ransomNote.length; j++) {
        const char = ransomNote[j];
        if (!magCount[char] || magCount[char] <= 0) {
            return false;
        }
        magCount[char]--;
    }
 
    return true;
};

首先遍历magazine的字符串,构建一个hash表,key是字符,value就是出现的次数。再次遍历ransomNote,然后从hash表中取值,出现一次减1,直到不存在或者小于0。

三数之和

力扣题目链接

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

首先思路:暴力解法,固定一个长度为2的窗口开始遍历整个数组,得到和,再数组遍历剩下的,看能不能找到差值,试一下:

var threeSum = function (nums) {
  let left = 0;
  let right = left + 1;
  let right_1 = right + 1;
  let result = [];
  while (right < nums.length - 1) {
    let sum = nums[left] + nums[right];
    for (let i = right_1; i < nums.length; i++) {
      if (nums[i] + sum === 0) {
        result.push([nums[left], nums[right], nums[i]]);
      }
    }
    left++;
    right++;
  }
  return result;
};

发现有重复的,再给数组加个去重(小思考:既然有重复的,看看有没有办法将数据保存在hash表中,来根据hash表避免添加重复的)

function removeDuplicates(arr) {
    const seen = new Set();
    const result = [];
 
    for (const subArray of arr) {
        // 对子数组进行排序,以便顺序无关的比较
        const sortedSubArray = [...subArray].sort((a, b) => a - b);
        // 将排序后的子数组转换为字符串,以便用作 Set 的键
        const key = JSON.stringify(sortedSubArray);
 
        if (!seen.has(key)) {
            seen.add(key);
            result.push(subArray); // 保留原始顺序
        }
    }
 
    return result;
}

提交后发现这种方式会有数组遗漏,唉,菜鸡如我,

这个方式明显不可以,换个三层for循环,挨个遍历,这样就不会出现遗漏了

var threeSum = function (nums) {
    const result = [];
    const n = nums.length;
 
    // 首先对数组进行排序
    nums.sort((a, b) => a - b);
 
    for (let i = 0; i < n - 2; i++) {
        // 跳过重复的元素
        if (i > 0 && nums[i] === nums[i - 1]) continue;
 
        for (let j = i + 1; j < n - 1; j++) {
            // 跳过重复的元素
            if (j > i + 1 && nums[j] === nums[j - 1]) continue;
 
            for (let k = j + 1; k < n; k++) {
                // 跳过重复的元素
                if (k > j + 1 && nums[k] === nums[k - 1]) continue;
 
                if (nums[i] + nums[j] + nums[k] === 0) {
                    result.push([nums[i], nums[j], nums[k]]);
                }
            }
        }
    }
 
    return result;
};

这样提交后超时了,只剩下一个办法了,看题解

function threeSum(nums) {
    nums.sort((a, b) => a - b); // 排序数组
    const result = [];
    const n = nums.length;
 
    for (let i = 0; i < n - 2; i++) {
        // 跳过重复的 nums[i]
        if (i > 0 && nums[i] === nums[i - 1]) {
            continue;
        }
 
        let left = i + 1;
        let right = n - 1;
 
        while (left < right) {
            const total = nums[i] + nums[left] + nums[right];
            if (total < 0) {
                left++;
            } else if (total > 0) {
                right--;
            } else {
                result.push([nums[i], nums[left], nums[right]]);
                // 跳过重复的 nums[left] 和 nums[right]
                while (left < right && nums[left] === nums[left + 1]) {
                    left++;
                }
                while (left < right && nums[right] === nums[right - 1]) {
                    right--;
                }
                left++;
                right--;
            }
        }
    }
 
    return result;
}

这里先对数组做一个排序,方便后续指针的移动。然后就开始遍历数组,遍历的约束条件是i < n - 2,因为后续还有两个指针,要给他们留位置;紧接着判断如果有重复的直接continue,下面就是双指针的核心了:

  • 初始化两个指针:left 指向 i + 1right 指向数组的末尾。

  • while (left < right):在 leftright 指针之间进行操作,直到它们相遇。

  • 计算total = nums[i] + nums[left] + nums[right]

    • 如果 total < 0,则说明当前和太小,需要增大,因此将 left 指针右移。
    • 如果 total > 0,则说明当前和太大,需要减小,因此将 right 指针左移。
    • 如果 total === 0,则找到一个有效的三元组,将其加入 result
  • 在找到一个有效的三元组后,移动指针以跳过重复的元素:

    • while (left < right && nums[left] === nums[left + 1]) left++;:跳过重复的 nums[left]
    • while (left < right && nums[right] === nums[right - 1]) right--;:跳过重复的 nums[right]
  • 然后移动 leftright 指针,继续寻找其他可能的三元组。

四数之和

力扣题目链接

题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意: 答案中不可以包含重复的四元组。

示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]

var fourSum = function(nums, target) {
    const len = nums.length;
    if(len < 4) return [];
    nums.sort((a, b) => a - b);
    const res = [];
    for(let i = 0; i < len - 3; i++) {
        // 去重i
        if(i > 0 && nums[i] === nums[i - 1]) continue;
        for(let j = i + 1; j < len - 2; j++) {
            // 去重j
            if(j > i + 1 && nums[j] === nums[j - 1]) continue;
            let l = j + 1, r = len - 1;
            while(l < r) {
                const sum = nums[i] + nums[j] + nums[l] + nums[r];
                if(sum < target) { l++; continue}
                if(sum > target) { r--; continue}
                res.push([nums[i], nums[j], nums[l], nums[r]]);
        
        // 对nums[left]和nums[right]去重
                while(l < r && nums[l] === nums[++l]);
                while(l < r && nums[r] === nums[--r]);
            }
        } 
    }
    return res;
};