1.哈希表基础
哈希表也称为散列表,是根据键(Key)而直接访问在内存存储位置的数据结构。
哈希表其实就是一个数组。
哈希表的关键码就是数组的索引下标,通过数组下标直接访问数组元素。
通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度,存放这些记录的数组称为哈希表
哈希表可以解决的问题:
一般哈希表用于快速判断一个元素是否出现在某个集合中
。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1) 就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了哈希函数。
哈希函数:
把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下表快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下表的位置。就会涉及到哈希碰撞。
哈希碰撞
多个元素映射到同一个索引下标的位置上,这个现象称为哈希碰撞。
解决哈希碰撞的两种方法:
拉链法:
多个元素映射在同一个索引下标位置,发生了冲突,将发生冲突的元素存储在链表
中。可以通过索引找到这几个元素。
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
2.有效的字母异位词
leetcode
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = “anagram”, t = “nagaram” 输出: true
示例 2: 输入: s = “rat”, t = “car” 输出: false
思路
暴力解法:双重for循环
哈希表:
首先两个字符串长度不同则肯定不是字母异位词。
使用数组记录字符串中每个字符出现的次数,因为每个字符串都是由小写字母组成的,所有可以使用大小为26的数组record来记录,将数组record初始化为0,从字符a到z的ASCII码为26个连续的数值,可以通过ASCII将字符映射到哈希表的索引下,将a映射到下标为0的位置上,将z映射到下标为25的位置上。将字符的ASCII码数值减去a的ASCII数值即正好为a到z的索引下标。
分别记录一个字符串字符出现的次数,出现一次则在字符对应的索引下标的值加1。就统计出第一个字符串中各个字符出现的次数。
将其余第二个字符串进行对比,第二个字符串上每出现一个字符,就将哈希表上字符对应下标位置上的值减1,最后检查哈希表,若哈希表上存在元素不为0,则说明两个字符串的字符类型和个数不相同,不是字母异位词。
function isAnagram(s,t){
if(s.length!=t.length) return false; //长度不等直接判断为false
var resSet=Array(26).fill(0); //新建一个长度为26元素都为0的数组
var base=a.charCodeAt(); //a对应的ASCII码
for(const i of s){ //遍历字符串字符
resSet[i.charCodeAt()-base]++ //存在则在其索引对应值加1
}
for(const i of t){
resSet[i.charCodeAt()-base]-- //存在字符则减1
}
for(const i of resSet){ //遍历数组
if(resSet[i]!=0) //存在不为0的项,则证明字符类型和数量不同
return false;
}
return true;
}
3.两个数组的交集
leetcode
给定两个数组,编写一个函数来计算它们的交集。
示例一:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例二:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
说明:输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序。
思路:
题目说明输出的结果的每一个元素时唯一的,则就需要进行去重,可以使用set集合作为哈希表。
注意,使用数组来做哈希的题目,是因为题目都限制了数值的大小(比如上题限制可能的元素为26个,可以使用数组),而该题无法确定各数组中可以重叠的数字的多少,没有大小的限制,因此不能使用数组作为哈希表。
而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
可以使用数组的时候尽量使用数组哈希表而不是set哈希表,因为数组中存放的是元素值,而哈希表中存放的是键值对,set占用的空间相对较大,而且速度较慢。
将一个数组转换为set集合,就会将数组中的元素进行去重,新建一个空set集合,遍历另一个数组,若该数组中的元素存在于第一个set集合中,则将该元素存放在空set集合中,保证输出的每个元素都是唯一的。
function intersection(nums1,nums2){
var nums1Set=new Set(nums1); //将数组设置为set集合,进行去重操作
var resSet=new Set(); //新建一个空白set集合
for(const n of nums2){ //for of进行迭代,遍历数组中的元素
if(nums1Set.has[n]) //若set集合中存在nums2中的元素
resSet.add(n); //将两个数组中都存在的元素则添加进新的set集合
}
return Array.from(resSet); //将set集合转换为数组
}
function intersection(nums1,nums2){
var nums1Set=new Set(nums1);
var resSet=new Set();
for(var i=0;i<nums2.length;i++){ //使用循环进行遍历
nums1Set.has[n] && resSet.add(n)
}
return Array.from(resSet);
}
4.快乐数
leetcode
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:
输入:19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
6.两数之和
leetcode
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例1:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
7.四数相加II
leetcode
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。
例如:
输入: A = [ 1, 2] B = [-2,-1] C = [-1, 2] D = [ 0, 2] 输出: 2 解释: 两个元组如下:
(0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
(1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0