哈希表本质其实就是一个数组,但在java里面有另外两种哈希的容器:HashMap和HashSet
哈希算法:哈希算法适用于查找一个元素是否在一个集合中。可以设想一个在一个数组里面,你想查找一个值,那时间复杂度就是O(n),哈希表就是用键值对(key,value)的方法,将查找一个值的时间效率提高到O(1)。
哈希表的理解:其实随着我写算法题的增多,哈希表基本上是用来当成一个工具,就不会说有什么题目专门可以用哈希算法来写
下面是用map来做哈希表的情况:
leetcode 1 两数之和
public int[] twoSum(int[] nums, int target) {
Map <Integer,Integer> table = new HashMap<>();
table.put(nums[0],0);
for(int i=1;i<nums.length;++i){
if(table.containsKey(target-nums[i])){
int[] ans = new int[2];
ans[0] = table.get(target-nums[i]);ans[1] = i;
return ans;
}
table.put(nums[i],i);
}
return new int[2];
}
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
这道题也算是一个哈希表入门的经典题目了,这道题目用暴力的方法肯定也能过。
但是用哈希表时间复杂度就能降低成O(n),
仔细想一想哈希表的作用,查找一个数据是否在集合中,那运用到这一题又怎么理解呢?
我们对这个数组进行遍历:当我们遍历到一个数的时候,我们先判断这个(tar-这个数)是否在这个哈希表中,如果不在,那我们就把这个数放到哈希表中,假设这个数它是我们想找的“两数”中的一个,那当我们遍历到这个数的时候,另一个“两数”肯定就已经在哈希表里面了。这不正是哈希表的应用嘛,在一个集合中查找一个元素。
leetcode 454. 四数相加 II
public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
Map<Integer, Integer> countAB = new HashMap<Integer, Integer>();
for (int u : A) {
for (int v : B) {
countAB.put(u + v, countAB.getOrDefault(u + v, 0) + 1);
}
}
int ans = 0;
for (int u : C) {
for (int v : D) {
if (countAB.containsKey(-u - v)) {
ans += countAB.get(-u - v);
}
}
}
return ans;
}
分析:map应该是做哈希的最常用的数据结构了:
map是一种<key, value>的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适
根据map的结构:map中有一对键值对,有一个对应关系。
对第一道题的两数之和分析:这题我们不仅仅要找到这个数,而且我们需要返回这个数组的下标。这里很明显我们就有两个值。
对第二题的四数相加分析:这道题map中的key值很明显,就是数组的和,不过这题的value的值很值得考究:这个key出现的次数,
这是我一开始写得n3的时间复杂度:
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int i,j,k,l;
int n = nums1.length;
int count=0;
Map <Integer,Integer> table = new HashMap<>();
//把nums4数组元素放入哈希表中
for(i=0;i<n;++i){
table.put(nums4[i],table.getOrDefault(nums4[i],0)+1);
}
for(i=0;i<n;++i){
for(j=0;j<n;++j){
for(k=0;k<n;++k){
if(table.containsKey(-(nums1[i]+nums2[j]+nums3[k]))){
count += table.get(-(nums1[i]+nums2[j]+nums3[k]));
}
}
}
}
return count;
}
如果你不去记录这个key出现的次数会发生什么情况呢,就是当你的第四个数组(因为我一开始的想法就是把nums4存到哈希表里去),当nums4数组中有重复的值的时候,这个时候还得分析一下hashmap的特点,里面存储的规则也是,不允许有相同的值,如果你不记录出现的次数,就会导致次数少
举个例子把:比如前三个数组中某三个数的和是-2,然后nums4中有两个2,本来这种情况答案是要+2的,但是你不去记录次数的话,因为hashmap会吃掉一个2,所以导致结果少一个。
下面是用数组做哈希表的情况
leetcode 349两个数组的交集
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash = new int[1001];
Set<Integer> table = new TreeSet<>();
int i;
for (i = 0; i < nums1.length; ++i) {
hash[nums1[i]] = 1;
}
for (i = 0; i < nums2.length; ++i) {
if (hash[nums2[i]] == 1) {
table.add(nums2[i]);
}
}
int[] res = new int[table.size()];
int m = 0;
for(int temp:table){
res[m++] = temp;
}
return res;
}
题目:给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
写着题的题解主要是有两个原因:
1:首先这是一道用数组来当哈希表的题目
2:我在leetcode上注意到这题的数据nums[i]的大小只到1000,所以我就开了一个1001大小的数组,这也算是个小技巧把
leetcode 242:有效的字母异位词
public boolean isAnagram(String s, String t) {
int[] record = new int[26];
int i;
if(s.length()!=t.length()){
return false;
}
for(i=0;i<s.length();++i){
record[s.charAt(i)-'a']++;
}
for(i=0;i<t.length();++i){
record[t.charAt(i)-'a']--;
}
for(i=0;i<26;++i){
if(record[i]!=0){
return false;
}
}
return true;
}
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
分析:这道题也算是个很简单的用数组来当哈希表的题目了,很容易想到字母的个数只有26个,所以我们开一个长度为26的数组record就行了。
leetcode 383. 赎金信
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) {
return false;
}
// 定义一个哈希映射数组
int[] record = new int[26];
// 遍历
for(char c : magazine.toCharArray()){
record[c - 'a'] += 1;
}
for(char c : ransomNote.toCharArray()){
record[c - 'a'] -= 1;
}
// 如果数组中存在负数,说明ransomNote字符串总存在magazine中没有的字符
for(int i : record){
if(i < 0){
return false;
}
}
return true;
}
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
数组做哈希表的总结:
根据数组的特点:长度不可以改变,然后我们可以得出:数组做哈希表的场景主要是对应题目中某些量是定的,比如这两道题,题目所给的都是小写字母,小写字母的个数只有26个,所以,我们开一个26个长度的数组就可以很简单做为这个题目了。
题外话:
至于为什么不用map:因为map的效率比较低,为什么低呢,这个我说实话我现在看别人的理解我有点看不懂,所以等以后我回来再解释把。
下面是用set做哈希表的情况
什么时候要用到set呢,这里就不得不分析set的特点了,set集合是有一个自动去重的功能,所以当你分析题目返回的元素不能重复的时候,就可以考虑用set了。
值得注意的有一道题目:leetcode 三数之和:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
题目中提到了去重,也可以用这样的结构来试一下,虽然这道题用这个结构会超时。
Set<List<Integer>> res = new HashSet<>();
leetcode 217. 存在重复元素
public boolean containsDuplicate(int[] nums) {
Set<Integer> table = new HashSet<>();
for(int i=0;i< nums.length;++i){
if(!table.add(nums[i])){
return true;
}
}
return false;
}
给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。
分析:这道题本身不难,我想把这题记下来的原因是:这道题的哈希表用的是set集合
set集合的add方法返回值是一个boolean类型的值。表示set中如果有这个值就会返回false,没有就会返回true,并且将这个值添加进去。
leetcode 220. 存在重复元素 III
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
int n = nums.length;
TreeSet<Long> set = new TreeSet<Long>();
for (int i = 0; i < n; i++) {
Long ceiling = set.ceiling((long) nums[i] - (long) t);
if (ceiling != null && ceiling <= (long) nums[i] + (long) t) {
return true;
}
set.add((long) nums[i]);
if (i >= k) {
set.remove((long) nums[i - k]);
}
}
return false;
}
给你一个整数数组 nums 和两个整数 indexDiff 和 valueDiff 。
找出满足下述条件的下标对 (i, j):
i != j
,abs(i - j) <= indexDiff
abs(nums[i] - nums[j]) <= valueDiff
如果存在,返回 true
;否则,返回 false
。
这里先补充几个set集合的常用方法:
1:删除指定元素,set集合删除元素的时候是按照元素删的,不是按照下标
set.remove((long) nums[i - k]);
2:
floor(E e) 方法返回在这个集合中小于或者等于给定元素的最大元素,如果不存在这样的元素,返回null.
ceiling(E e) 方法返回在这个集合中大于或者等于给定元素的最小元素,如果不存在这样的元素,返回null.
分析:
这道题题目就是要求我们需要在大小为 k 的滑动窗口所在的「有序集合」中找到与 u 接近的数。
在k个滑动窗口:abs(i - j) <= indexDiff,的有序集合,有序集合的话,只要保证我们一直往前遍历,那i肯定!=j(这个条件我也想到了)
找到与u接近的数字:abs(nums[i] - nums[j]) <= valueDiff 。
这道题根据题目给的大小关系:
abs(nums[i] - nums[j]) <= valueDiff -----> nums[i]+valueDiff<nums[j]
这个时候,我们可以这么理解 我们创建的set集合就是用来存放nums[i],然后i++往前跑的是nums[j],所以我们需要ceil比那两个值的差值大
也就可以得出:Long ceiling = set.ceiling((long) nums[i] - (long) t);
以上是这个题目的第一个注意点:
if (i >= k) {
set.remove((long) nums[i - k]);
}
这一段代码表示什么意思呢:表示我们只需要在set集合中维护k个元素就好,怎么理解呢:
abs(i - j) <= indexDiff,根据题目所给的条件,就算你给我的元素的值满足要求,但是下标不满足要求,我们也不能要。
这就是一个条件的转化,以后碰到类似的情况也能这么去想。
还有第三个需要知道的就是set的treeset集合本质是红黑树。
原地哈希
原地哈希是一种提升空间时间复杂度的方法:
leetcode 41:缺失的第一个正数
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
这道题首先介绍自己的一开始的想法:
写一个哈希表,将这个数组放到哈希表里面去,然后从1遍历到1000001(因为这道题目的范围是这么多)
接着查找这个时候的i是否在这个哈希表中存在,如果不存在,因为i是从小遍历到大的,所以,我们直接返回就行
如果遍历完了都没有,说明,缺失的最小正整数就是这个数组的长度+1;
注意:有可能出现负数的情况,在返回的时候做一个特判。
class Solution {
public int firstMissingPositive(int[] nums) {
Map<Integer,Integer> table = new HashMap<>();
for(int i=0;i<nums.length;++i){
table.put(nums[i],i);
}
int max = Integer.MIN_VALUE;
for(int i=0;i<nums.length;++i){
if(nums[i]>max){
max = nums[i];
}
}
for(int i=1;i<=max;++i){
if(!table.containsKey(i)){
return i;
}
}
return (max<=0?1:max+1);
}
}
ok,下面来介绍原地哈希的办法:这道题要求不能开额外的空间,那我们的想法就是在数组上直接修改值,怎么修改呢,我们可以将nums[i]放在(i-1)上
比如什么呢将num[i] = 2,这个数放在数组(i-1)的位置上,我们对这个数组进行一定的操作之后,我们直接再遍历一次数据,我们从左往右遍历,第一个不符合位置上的数,就是我们要找的缺失的最小正整数。如果遍历到数组的末尾了,我们还是没找到那个正整数,那我们直接把数组的长度+1放回就行。
这里说一下,理论上来说,我们可以把nums[i]放在(i-1)上,但是如果比如说,这个数组的长度是5,然后第一个数字就是7,那这个数字根本没地方放怎么办呢,这个时候,我们就可以忽略这个7这个数字,随便放哪里,反正在我们遍历的时候,我们是根据这个时候的i值来进行比较的。
class Solution {
public int firstMissingPositive(int[] nums) {
int len = nums.length;
for(int i=0;i<len;++i){
while(nums[i]>=1&&nums[i]<=len&&nums[nums[i]-1]!=nums[i]){
swap(nums,i,nums[i]-1);
}
}
for(int i=0;i<len;++i){
if(nums[i]!=i+1){
return (i+1);
}
}
return len+1;
}
public void swap(int[] nums,int a,int b){
int t = nums[a];
nums[a] = nums[b];
nums[b] = t;
}
}
注意点:for循环中套了一个while循环:为什么要来一个while循环呢:比如[3,4,-1,1]这个案例,我们第一次交换完之后变成[-1,4,3,1],
然后我们再交换变成[-1,1,3,4],注意看这个数组,里面3,4是到了正确的位置,可是这个时候的1呢,我们原来的想法是把这个1放到数组的第一个元素上去
可是,如果我们只交换一次(用if)的话,我们可以保证这个4能交换到正确的位置,但我们交换回来的这个1不行,所以,我们还得再交换(while)
把这个数组变成[1,-1,3,4],这样才能获得答案。