算法训练--哈希表
哈希表基础理论
哈希表/散列表
- 哈希表也叫散列表:HashTable,一般哈希表都是用来快速判断一个元素是否出现集合里
哈希表是根据关键码的值而直接进行访问的数据结构
哈希函数
-
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字
-
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了
哈希碰撞
-
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞
一般哈希碰撞有两种解决方法, 拉链法和线性探测法
-
拉链法:链表结构
-
线性探测法:使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了
-
常见的三种哈希结构
- 数组
- set(集合)
- map(映射)
List、Set、Map的区别
-
List
1.可以允许重复的对象 2.可以插入多个null元素 3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序 4.常用的实现类有 ArrayList、LinkedList 和 Vector ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元 素的场合更为合适 -
Set
1.不允许重复对象 2.无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序 3.只允许一个null元素 4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器 -
Map
1.Map不是collection的子接口或者实现类,Map是一个接口 2.Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的 3.TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序 4.Map里你可以拥有随意个null值但最多只能有一个null键 5.Map接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap
字母异位词
242. 有效的字母异位词
-
题目描述
-
题解
定义一个数组叫做record用来上记录字符串s里字符出现的次数 需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25 再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了 那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作 那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false 最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。 时间复杂度为$O(n)$,空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为$O(1)$class Solution { public boolean isAnagram(String s, String t) { int[] record=new int[26]; for(char c:s.toCharArray()){ record[c-'a']+=1; } for(char c:t.toCharArray()){ record[c-'a']-=1; } for(int i:record){ if(i!=0){ return false; } } return true; } } /** class Solution { public boolean isAnagram(String s, String t) { char[] sArr=s.toCharArray(); char[] tArr=t.toCharArray(); Arrays.sort(sArr); Arrays.sort(tArr); return Arrays.equals(sArr,tArr); } } */
49. 字母异位词分组
-
题目描述
-
题解
class Solution { public List<List<String>> groupAnagrams(String[] strs) { Map<String,List<String>> map=new HashMap(); List<String> list=new ArrayList(); for(String s :strs){ char[] sArr=s.toCharArray(); Arrays.sort(sArr); String key=String.valueOf(sArr); if(!map.containsKey(key)){ map.put(key,new ArrayList()); } //将同组元素加入 map.get(key).add(s); } return new ArrayList(map.values()); } }
383. 赎金信
-
题目描述
-
题解
class Solution { public boolean canConstruct(String ransomNote, String magazine) { int[] record=new int[26]; for(char c:magazine.toCharArray()){ record[c-'a']+=1; } //区别有效的字母异位词,这里会存在多余的字符 //因此不能通过判断record是否为空来返回 for(char c:ransomNote.toCharArray()){ if(record[c-'a']>0){ record[c-'a']--; }else{ return false; } } return true; } }
438. 找到字符串中所有字母异位词
-
题目描述
-
题解
/** 滑动窗口+哈希表 */ class Solution { public List<Integer> findAnagrams(String s, String p) { List<Integer> res=new ArrayList<>(); Map<Character,Integer> need=new HashMap<>(); Map<Character,Integer> window=new HashMap<>(); for(char c:p.toCharArray()){ need.put(c,need.getOrDefault(c,0)+1); } int left=0,right=0,vaild=0; int len=s.length(); char[] sArr=s.toCharArray(); while(right<len){ char cRight=sArr[right]; right++; if(need.containsKey(cRight)){ window.put(cRight,window.getOrDefault(cRight,0)+1); if(need.get(cRight).equals(window.get(cRight))) vaild++; } while(need.size()==vaild){ if(right-left==p.length()){ res.add(left); } char cLeft=sArr[left]; left++; if(need.containsKey(cLeft)){ if(need.get(cLeft).equals(window.get(cLeft))) vaild--; window.put(cLeft,window.getOrDefault(cLeft,0)-1); } } } return res; } }
相关题目练习
349. 两个数组的交集
-
题目描述
-
题解
/** 每个元素是唯一且不考虑输出顺序,可以想到使用Set来解决 */ class Solution { public int[] intersection(int[] nums1, int[] nums2) { if(nums1==null || nums1.length==0 || nums2==null || nums2.length==0){ return new int[0]; } Set<Integer> set=new HashSet<>(); Set<Integer> reset=new HashSet<>(); //遍历数组1 for(int i:nums1){ set.add(i); } //遍历数组2的过程中判断哈希表中是否存在该元素 for(int i:nums2){ if(set.contains(i)){ reset.add(i); } } //将结果转为数组 int[] res=new int[reset.size()]; int index=0; for(int i:reset){ res[index++]=i; } return res; } }
350. 两个数组的交集 II
-
题目描述
-
题解
/** 排序+双指针 */ class Solution { public int[] intersect(int[] nums1, int[] nums2) { Arrays.sort(nums1); Arrays.sort(nums2); int l1=0,l2=0,index=0; int len1=nums1.length,len2=nums2.length; int[] res=new int[Math.min(len1,len2)]; while(l1<len1 && l2<len2){ if(nums1[l1]==nums2[l2]){ res[index]=nums1[l1]; l1++; l2++; index++; }else if(nums1[l1]>nums2[l2]){ l2++; }else{ l1++; } } //复制指定范围的数组 return Arrays.copyOfRange(res,0,index); } }
202. 快乐数
-
题目描述
-
题解
/** 题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要! 所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止 还有一个难点就是求和的过程,如果对取数值各个位上的单数操作不熟悉的话,做这道题也会比较艰难 */ class Solution { public boolean isHappy(int n) { Set<Integer> set=new HashSet<>(); //用于判断是否重复出现 //如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false while(n!=1 && !set.contains(n)){ set.add(n); //取数值各个位上的单数之和 n=getNextNum(n); } return n==1; } public Integer getNextNum(Integer n){ int res=0; while(n>0){ int temp=n%10; res+=temp*temp; n/=10; } return res; } }
1. 两数之和
-
题目描述
-
题解
class Solution { public int[] twoSum(int[] nums, int target) { Map<Integer,Integer> map=new HashMap<>(); for(int i=0;i<nums.length;i++){ int num=target-nums[i]; if(!map.containsKey(num)){ map.put(nums[i],i); }else{ return new int[]{i,map.get(num)}; } } return null; } }
454. 四数相加 II
-
题目描述
-
题解
class Solution { public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) { int temp=0,res=0; Map<Integer,Integer> map=new HashMap<>(); for(int i=0;i<nums1.length;i++){ for(int j=0;j<nums2.length;j++){ temp=nums1[i]+nums2[j]; map.put(temp,map.getOrDefault(temp,0)+1); } } for(int i=0;i<nums3.length;i++){ for(int j=0;j<nums4.length;j++){ temp=0-(nums3[i]+nums4[j]); if(map.containsKey(temp)){ res+=map.get(temp); } } } return res; } }
383. 赎金信
-
题目描述
-
题解
class Solution { public boolean canConstruct(String ransomNote, String magazine) { int[] record=new int[26]; for(char c:magazine.toCharArray()){ record[c-'a']+=1; } for(char c:ransomNote.toCharArray()){ if(record[c-'a']>0){ record[c-'a']-=1; }else{ return false; } } return true; } }
15. 三数之和
-
题目描述
-
题解
/** 双指针法一定要排序 */ class Solution { public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> res=new ArrayList<>(); if(nums==null || nums.length<3){ return res; } //排序排序排序 Arrays.sort(nums); for(int i=0;i<nums.length;i++){ int j=i+1,k=nums.length-1; if(nums[i]>0) break; if(i>0 && nums[i]==nums[i-1]) continue; while(j<k){ int sum=nums[i]+nums[k]+nums[j]; if(sum==0){ res.add(Arrays.asList(nums[i],nums[j],nums[k])); while(j<k && nums[j]==nums[j+1]) j++; while(j<k && nums[k]==nums[k-1]) k--; j++; k--; }else if(sum>0){ k--; }else{ j++; } } } return res; } }
18. 四数之和
-
题目描述
-
题解
class Solution { public List<List<Integer>> fourSum(int[] nums, int target) { List<List<Integer>> res=new ArrayList<>(); //排序排序排序 Arrays.sort(nums); if(nums.length<4){ return res; } for(int i=0;i<nums.length;i++){ // 去重 if(i>0 && nums[i]==nums[i-1]) continue; //比三数之和多一重循环 for(int j=i+1;j<nums.length;j++){ if(j>i+1 && nums[j]==nums[j-1]) continue; int left=j+1,right=nums.length-1; while(left<right){ int sum=nums[i]+nums[j]+nums[left]+nums[right]; if(sum>target){ right--; }else if(sum<target){ left++; }else{ res.add(Arrays.asList(nums[i],nums[j],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 res; } }
哈希表总结
数组作为哈希表
Set作为哈希表
Map作为哈希表
CodeTop系列
94. 二叉树的中序遍历
-
递归
class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res=new ArrayList<>(); inorder(root,res); return res; } public void inorder(TreeNode root,List<Integer> res){ if(root==null){ return; } inorder(root.left,res); res.add(root.val); inorder(root.right,res); } } -
统一迭代法
class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); if(root==null) return res; Stack<TreeNode> stack = new Stack<>(); stack.push(root); while (!stack.isEmpty()) { TreeNode node=stack.peek(); if(node!=null){ stack.pop(); if(node.right!=null) stack.push(node.right); stack.push(node); stack.push(null); if(node.left!=null) stack.push(node.left); }else{ stack.pop(); node=stack.peek(); res.add(node.val); stack.pop(); } } return res; } }
3. 无重复字符的最长子串
-
题目描述
-
题解
class Solution { public int lengthOfLongestSubstring(String s) { if(s.length()==0) return 0; Map<Character,Integer> map=new HashMap<>(); int left=0; int maxLen=Integer.MIN_VALUE; for(int right=0;right<s.length();right++){ char ch=s.charAt(right); map.put(ch,map.getOrDefault(ch,0)+1); while(map.get(ch)>1){ char cLeft=s.charAt(left); map.put(cLeft,map.get(cLeft)-1); left++; } maxLen=Math.max(maxLen,right-left+1); } return maxLen; } }
560. 和为 K 的子数组
-
题目描述
-
题解
注意:为什么这题不可以用双指针/滑动窗口:因为
nums[i]可以小于0,也就是说右指针i向后移1位不能保证区间会增大,左指针j向后移1位也不能保证区间和会减小。给定j,i的位置没有二段性class Solution { public int subarraySum(int[] nums, int k) { //扫描一遍数组,使用map记录同样的和的次数 //对每个i计算累加sum并判断map内是否有sum-k Map<Integer,Integer> map=new HashMap<>(); map.put(0,1); int sum=0,res=0; for(int i=0;i<nums.length;i++){ sum+=nums[i]; if(map.containsKey(sum-k)){ res+=map.get(sum-k); } map.put(sum,map.getOrDefault(sum,0)+1); } return res; } }
138. 复制带随机指针的链表
-
题目描述
-
题解
class Solution { public Node copyRandomList(Node head) { if(head == null) return head; // map方法,空间复杂度O(n) Node node = head; // 使用hash表存储旧结点和新结点的映射 Map<Node,Node> map = new HashMap<>(); while(node != null){ Node clone = new Node(node.val,null,null); map.put(node,clone); node = node.next; } node = head; while(node != null){ map.get(node).next = map.get(node.next); map.get(node).random = map.get(node.random); node = node.next; } return map.get(head); } }
76. 最小覆盖子串
-
题目描述
-
题解
class Solution { public String minWindow(String s, String t) { if(s.length()==0) return ""; if(t.length()>s.length()) return ""; char[] sArr=s.toCharArray(); char[] tArr=t.toCharArray(); Map<Character,Integer> needMap=new HashMap<>(); Map<Character,Integer> map=new HashMap<>(); int left=0,maxNum=0,start=0,minLen=Integer.MAX_VALUE; for(char c:tArr){ needMap.put(c,needMap.getOrDefault(c,0)+1); } for(int right=0;right<sArr.length;right++){ char ch=sArr[right]; map.put(ch,map.getOrDefault(ch,0)+1); if(map.get(ch).equals(needMap.get(ch))){ //满足条件的字符数 maxNum++; } //满足条件时缩小窗口 while(needMap.size()==maxNum){ //记录当前窗口 if(right-left<minLen){ start=left; minLen=right-left+1; } //缩小左边界 char cleft=sArr[left]; if(map.get(cleft).equals(needMap.get(cleft))){ maxNum--; } map.put(cleft,map.get(cleft)-1); left++; } } return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen); } }
718. 最长重复子数组
-
题目描述
-
题解
记住,子序列默认不连续,子数组默认连续
动态规划
class Solution { public int findLength(int[] A, int[] B) { int n = A.length, m = B.length; int[][] dp = new int[n + 1][m + 1]; int ans = 0; for (int i = n - 1; i >= 0; i--) { for (int j = m - 1; j >= 0; j--) { dp[i][j] = A[i] == B[j] ? dp[i + 1][j + 1] + 1 : 0; ans = Math.max(ans, dp[i][j]); } } return ans; } }滑动窗口
class Solution { public int findLength(int[] A, int[] B) { int n = A.length, m = B.length; int ret = 0; for (int i = 0; i < n; i++) { int len = Math.min(m, n - i); int maxlen = maxLength(A, B, i, 0, len); ret = Math.max(ret, maxlen); } for (int i = 0; i < m; i++) { int len = Math.min(n, m - i); int maxlen = maxLength(A, B, 0, i, len); ret = Math.max(ret, maxlen); } return ret; } public int maxLength(int[] A, int[] B, int addA, int addB, int len) { int ret = 0, k = 0; for (int i = 0; i < len; i++) { if (A[addA + i] == B[addB + i]) { k++; } else { k = 0; } ret = Math.max(ret, k); } return ret; } }