摘要
本文是参加算法学习的日常记录,分析总结了哈希表基础理论的一些要点,详细阐述了四道基础的哈希表leetcode算法题目(有效字母异位词、两个数组的交集、快乐数、两数之和)的解题思路,并给出了对应的Java代码实现。
哈希表基础理论
哈希表的理论思维其实就在于将Key
作为数组下标进行搜索。
我们知道数组的物理空间在内存中是连续的,因而访问任意数组下标,Java虚拟机仅需计算数组起始地址+索引*数组内元素占用空间大小
,就可以获得需要访问的索引的物理地址,这个过程显然是非常快的,可以达到O(1)
的时间复杂度。
而哈希表的一般实现方式是数组+链表
,其中链表是为了解决哈希碰撞而使用的内层数据结构。因而,哈希表的随机存取时间复杂度也可以达到O(1)
。
哈希表的核心,当然是哈希算法,它接收任何一组任意长度的输入信息,通过哈希算法变换成固定长度的数据指纹输出形式,如字母和数字的组合,该输出就是“哈希值”。这个哈希值通过一些计算之后,就能作为数组的下标。
Java中最常见的哈希表实现是HashMap
,它的每一个元素都是一个key
-value
键值对Entry
,存储在一个Entry
数组当中,存入之前先通过以下算式计算其应该存入的数组位置:
(h = key.hashCode()) ^ (h >>> 16)
这样计算出的数组下标出现重复,也就是哈希碰撞,的概率是极低的。只有当存入元素的增多,特别是变态多(甚至超过数组下标范围的元素个数),其存取效率才会变低。
当然,因为底层是数组实现的原因,哈希表实际上是用了空间换时间,在空间复杂度方面有一定的牺牲。
在实际的算法学习中,能用哈希法实现的算法题目,还是要具体问题具体分析的。如果key
的取值范围有限的,或许定义一个定长的数组作为哈希表即可;如果key
的取值范围特别大的,只能直接用HashMap
或者HashSet
这种库数据结构。
另外一方面,我们需要清楚,哈希表的一个重要特性,那就是无序,如果算法要求有序,那么就不要使用哈希表,否则只会浪费扣脑袋的时间。另一个重要特性是,不重复,key
在哈希表中是不能重复出现的,会被覆盖。
242、有效的字母异位词
思路分析
这道题显然有两种解法。
一种是排序法。如果是字母异位词,排序后将相等。所以对两个字符串进行排序,逐字符进行比较即可。
另一种是计数法。更为直观,要求每个字符出现次数一样,所以对一个字符串中各字符的出现次数进行统计,然后去比较另一个字符串中各个字符的出现字数即可。
排序法,最快的排序法时间复杂度为O(2nlogn)
,加上之后的字符遍历比较O(n)
,是O(nlogn)
级别。由于没有用额外的空间,空间复杂度是O(1)
。
计数法,需要一个哈希表来存储字符出现次数,空间复杂度是O(n)
;时间复杂度方面,需要遍历两次字符串,一次是累加计数O(n)
,一次是累减计数O(n)
,最后还要遍历一次哈希表最快O(26)
,因此时间复杂度是O(n)
。
而这道题哈希表的key
范围是固定的,就是小写字母集,因此不需要HashMap
,仅需一个长度为26
的字符数组作为哈希表即可。
代码实现
//计数法
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
int[] map = new int[26];
for (int i = 0; i < s.length(); i++) {
map[s.charAt(i) - 97]--;//s字符次数计数
}
for (int i = 0; i < t.length(); i++) {
int i1 = t.charAt(i) - 97;
if (map[i1] == 0) {//剪枝操作
return false;
}
map[i1]++;//t字符次数反计数
}
for (int i : map) {
if (i != 0) return false;
}
return true;
}
//排序法
public boolean isAnagram1(String s, String t) {
if (s.length() != t.length()) {
return false;
}
char[] sCharArray = s.toCharArray();
char[] tCharArray = t.toCharArray();
Arrays.sort(sCharArray);
Arrays.sort(tCharArray);
for (int i = 0; i < sCharArray.length; i++) {
if (sCharArray[i] != tCharArray[i]) {
return false;
}
}
return true;
}
349、两个数组的交集
思路分析
根据这道题的题目要求,显然只要一个数字在nums1
、nums2
中都出现了,就可以直接加入输出答案中。因此,我们需要一个额外空间来统计nums1
中各元素的出现情况。由于要求要去重,因此我们不需要计数,只需要关心是否出现过就行了。
所以,可以采用一个哈希表,key
存储nums1
中的数字,value
存储对应的key
是否出现过。另一方面,我们注意到条件1 <= nums1.length, nums2.length <= 1000
,这意味着我们可以采用一个长度为1001
的布尔数组map
即可。
因此,第一步是将map
中下标为nums1
中的元素的位置设为真
。
第二步是遍历nums2
中的元素,查询map
中对应下标的位置是否为真
。如果是,就将该元素加入输出答案中,并且将map
对应位置设为假
。
代码实现
public int[] intersection(int[] nums1, int[] nums2) {
ArrayList<Integer> ans = new ArrayList<>();
boolean[] map = new boolean[1001];
for (int i : nums1) {
map[i] = true;//记录nums1[i]出现过
}
for (int i : nums2) {
if (map[i]) {//nums2[i]在nums1中出现过
ans.add(i);
map[i] = false;//记录nums2[i]加过了
}
}
int[] res = new int[ans.size()];//转化为int[]
for (int i = 0; i < res.length; i++) {
res[i] = ans.get(i);
}
return res;
}
202、快乐数
思路分析
这道题首先要实现的就是将给的数n
按照个位十位百位分解,并且计算每一位的平方之和n1
。
接着是将n1
进行相同的分解操作得到n2
···,直到得到的nm
为1
。
那么无限循环始终不到1的情况是什么样呢?如果将n
、n1
、n2
···nm
看作一条“链表”,那么无限循环就是“链表”成环了,也就是说出现过的nm
,又被计算得到了。如果不循环,这条“链表”的尾部节点就是1
。
因此,我们仍然可以使用哈希法来做这道题,将出现过的元素记录一下,每次将计算得到nm
时比较是否被记录过,如果是,则不是快乐数字,否则继续计算直到1出现。
最后,我们发现这道题的条件是1 <= n <= 2^31- 1
,因此无法直接使用数组作为哈希表,只能用HashMap
或者HashSet
。
代码实现
public boolean isHappy(int n) {
Map<Integer, Boolean> map = new HashMap<>();
int count = 0;
while (map.getOrDefault(n, true)) {
map.put(n, false);
n = fact(n);
count++;
if (n == 1) {
return true;
}
}
return false;
}
private int fact(int n) {
int sum = 0;
while (n != 0) {
int tmp = n / 10;
int num = n - 10 * tmp;
sum += num * num;
n = tmp;
}
return sum;
}
1、两数之和
思路分析
这道题虽然放在第1题,而且是easy
,但是如果不清楚哈希法这种做法,还是很难想到的。
哈希法解这道题的精髓在于,从哈希表map
中搜索target - nums[i]
,如果不存在这个下标就将nums[i]
加入其中,相对于暴力搜索法的O(n^2)
,这种哈希表搜索法的时间复杂度O(1)
是碾压性的。
对于第一次刷leetcode算法题的初学者来说,这道题在算法思维的塑造上有重大意义,不愧是梦开始的地方!
代码实现
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])) {
return new int[]{map.get(target - nums[i]), i};
} else {
map.put(nums[i], i);
}
}
return new int[2];
}
总结和思考
哈希表的三大特性:无序、无重复、O(1)。这三点,是限制也是优势,只要合理运用,就可以降低我们的算法复杂度。
哈希法在计数等方面有很明显的时间优势,用空间换时间,现代的计算机技术已经使得空间不再那么有限、宝贵,对时间的需求远大于对空间的需求,因此哈希表的广泛应用符合计算机发展规律。