介绍
哈希表(Hash Table)是一种基于哈希函数实现的高效数据结构,用于存储键值对(Key-Value)。它通过将键(Key)映射到表中的一个位置(称为桶或槽),实现快速的数据访问、插入和删除操作。理想情况下,这些操作的时间复杂度为 O(1) 。
基本概念
-
哈希函数(Hash Function): 将键(Key)转换为一个固定大小的整数(哈希值),用于确定数据在表中的存储位置。 示例:
- 键
"apple"经过哈希函数后可能映射到索引3。 - 键
"banana"可能映射到索引7。
- 键
-
桶(Bucket)或槽(Slot): 哈希表中的存储单元,用于存放键值对。哈希函数的结果决定了键值对存储在哪个桶中。
-
哈希冲突(Hash Collision): 当不同的键映射到同一个桶时发生。哈希表需要通过冲突处理策略解决这一问题。
基本操作
-
插入(Insert):
- 计算键的哈希值,确定存储位置。
- 如果该位置已被占用,处理冲突(如链地址法或开放寻址法)。
- 将键值对存入该位置。
-
查找(Search):
- 计算键的哈希值,定位到存储位置。
- 处理冲突(如果存在),找到对应的键值对。
- 返回对应的值(Value)。
-
删除(Delete):
- 计算键的哈希值,定位到存储位置。
- 处理冲突(如果存在),找到对应的键值对。
- 从表中移除该键值对。
算法
有个非常经典的一句话: 当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 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,开始遍历第一个字符串s,map中的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
两个数组交集
给定两个数组 nums1 和 nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序
暴力解法
这个题算是力扣为数不多我可以称之为简单的,直接两个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,这里没有求和,而是取差值。
四数之和
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < nnums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1
这个题刚一看还是有点懵的,看完题解就恍然大悟,先计算nums1、nums2,两个for循环,将计算的结果当做key,value就是出现的次数,然后再遍历nums3 和 nums4,同样两个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; // 返回满足条件的四元组总数
};
赎金信
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
输入:ransomNote = "aa", magazine = "aab"
输出:true
题干有点花里胡哨的,换成好理解的就是:给两个字符串ransomNote和magazine,判断 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 + 1,right指向数组的末尾。 -
while (left < right):在left和right指针之间进行操作,直到它们相遇。 -
计算
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]。
-
然后移动
left和right指针,继续寻找其他可能的三元组。
四数之和
题意:给定一个包含 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;
};