哈希表基础
哈希表Hash Table是一种存储键值对的数据结构,最简单的例子就是数组,我们通过数组的index索引来直接访问数组中存储的元素。哈希表的主要功能就是用了快速判断一个元素是否出现在集合里。
哈希函数Hash Function一个将键映射为数组索引的函数。
哈希表的大小是有限的,键值组合可能有很多,因此不同的键可能通过哈希函数会映射到同一个位置,这叫做哈希碰撞(Collision)。碰撞处理主要分为拉链法(Separate Chaining)即为哈希表的冲突位置建立一个链表,将碰撞元素都储存在链表中;以及开放地址法(Open Addressing)当发生碰撞时,通过某种策略(如线性探测、二次探测、双重哈希)寻找下一个空闲的槽来存储数据。
常见的哈希结构:
- 数组 Array:将键映射到数组的索引位置。数组中的每个位置称为一个槽
- 集合 Set / HashSet:用于存储不重复的元素,但是不能保证集合中的元素顺序
- 映射 Map / HashMap:用于存储键值对,每一个键都是唯一的并且与一个值相关连,但也不能保证键的顺序
LeetCode - 242. Valid Anagram 有效字母的异位词
输入两个英文字符串,只要两者所包含的英文字母一样(可能顺序不一样)即为异位词。这道题比较适用的哈希结构是数组。因为对于英文单词一共就26个,对于这种数据量小的情况可以直接生成一个长度为26的数组。因为字母的位置是可以直接确定的,我妈可以通过数组对应的索引直接查找是否出现过该字母。在遍历第一个单词时,对应的字母顺序在数组中的位置+1,遍历第二个单词时-1。如果最后所有元素都为0那么就证明两者为异位词。
class Solution {
public boolean isAnagram(String s, String t) {
int[] letterHash = new int[26];
for (int i=0; i < s.length(); i++) {
letterHash[s.charAt(i) - 'a']++;
}
for (int j=0; j < t.length(); j++) {
letterHash[t.charAt(j) - 'a']--;
}
for (int c : letterHash) {
if (c != 0) return false;
}
return true;
}
}
但同时也需要注意,因为我们是按数组的索引为基础(0~26),每个字母的ASCII码值需要减去第一个字母'a'才能转换为0~26字母在数组中所对应的可用的数值。
LeetCode - 349. Intersection of Two Arrays 两个数组的交集
这题中数组的交集是不重复的。因为数组里的数值大小我们是无法确认的,这时候做哈希映射的时候用数组就不太合适了,因为确定不了一个固定的数组长度(leetcode上更新了数字大小的上限,其实是可以做的)。
而这题的本质也就是判断元素是否出现过,并且数值可能很大或是很分散,这种情况下就适合用Set。思路就是先把其中一个数组的元素记录到HashSet中,再遍历第二个数组中过的元素是否存在于HashSet,如果存在则加入result数组。
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> hs = new HashSet<>();
Set<Integer> res = new HashSet<>();
for (int i : nums1) {
hs.add(i);
}
for (int j : nums2) {
if (hs.contains(j)) {
res.add(j);
}
}
//convert to array
int[] resArr = new int[res.size()];
int idx = 0;
for (int a : res) {
resArr[idx++] = a;
}
return resArr;
}
}
这里注意因为我们想要的结果是去重的,result应该先存为HashSet结构自动去重并且最终再转换为数组结构
LeetCode - 202. Happy Number 快乐数
快乐数的定义是对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到最后的数字变为 1。这个题有可能会出现一直找不到的情况,即无限循环,因此这时候就要借助Set来判断是否出现过。注意while循环的条件,首先要判断不包含在set中的数字才进行循环,否则就代表重复了也就是出现无限循环的情况。
刚开始没有想到如何拆分数字中每位数字进行计算,看了卡尔的题解学习到了这个巧妙的方法。用余数来得到最后一位数字以及每次都除以10来去掉末尾数字,这样就可以用循环来算出总平方和。这里的num>0的循环条件并不会形成死循环,按照逻辑处理完所有数位后n会变成小数,但因为num定义为int,Java的除法机制自动丢弃小数部分,因此处理完之后num就是等于0的,退出循环。
class Solution {
public boolean isHappy(int n) {
Set<Integer> hs = new HashSet<>();
while (n != 1 && !hs.contains(n)) {
hs.add(n);
n = getNextNum(n);
}
return n == 1;
}
//helper function
private int getNextNum(int num) {
int sum = 0;
while (num > 0) {
int dec = num % 10;
sum += dec * dec;
num /= 10; //get next digit
}
return sum;
}
}
LeetCode - 1. Two Sum 两数之和
这题同样是查找一个元素是否遍历过,因为在遍历每个数字的过程中,需要往前找是否可以相加等于target的数字。但是题目中最终要求的是返回两个数字对应的index,这时候光找到数字是不够的,因此需要存储键值对才能达到目的。注意这里键值对中key是数值而不是index,因为我们在查找的时候是根据差值diff来查找的,因此key为数组中的数值,value为index。
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
HashMap<Integer, Integer> hm = new HashMap<>();
for (int i=0; i < nums.length; i++) {
int diff = target - nums[i];
if (hm.containsKey(diff)) {
res[0] = hm.get(diff);
res[1] = i;
}
hm.put(nums[i], i);
}
return res;
}
}