算法训练--哈希表

172 阅读7分钟

算法训练--哈希表

哈希表基础理论

哈希表/散列表

  • 哈希表也叫散列表:HashTable,一般哈希表都是用来快速判断一个元素是否出现集合里

哈希表是根据关键码的值而直接进行访问的数据结构

哈希函数

  • 哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字

    哈希表2

  • 如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?

    此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了

哈希碰撞

  • 如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞

    哈希表3

    一般哈希碰撞有两种解决方法, 拉链法和线性探测法

    • 拉链法链表结构

      哈希表4

    • 线性探测法:使用线性探测法,一定要保证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值但最多只能有一个null5.Map接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap
    

字母异位词

242. 有效的字母异位词

  • 题目描述

    image.png

  • 题解

    定义一个数组叫做record用来上记录字符串s里字符出现的次数
    需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII26个连续的数值,所以字符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. 字母异位词分组

  • 题目描述

    image.png

  • 题解

    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. 赎金信

  • 题目描述

    image.png

  • 题解

    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. 找到字符串中所有字母异位词

  • 题目描述

    image.png

  • 题解

    /**
    	滑动窗口+哈希表
    */
    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. 两个数组的交集

  • 题目描述

    image.png

  • 题解

    /**
    	每个元素是唯一且不考虑输出顺序,可以想到使用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

  • 题目描述

    image.png

  • 题解

    /**
    	排序+双指针
    */
    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. 快乐数

  • 题目描述

    image.png

  • 题解

    /**
    	题目中说了会 无限循环,那么也就是说求和的过程中,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. 两数之和

  • 题目描述

    image.png

  • 题解

    image.png

    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

  • 题目描述

    image.png

  • 题解

    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. 赎金信

  • 题目描述

    image.png

  • 题解

    image.png

    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. 三数之和

  • 题目描述

    image-20220411223515523.png

  • 题解

    /**
    	双指针法一定要排序
    */
    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. 四数之和

  • 题目描述

    image.png

  • 题解

    image.png

    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;
        }
    }
    

哈希表总结

数组作为哈希表

image.png

Set作为哈希表

image.png

Map作为哈希表

image.png

image.png

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. 无重复字符的最长子串

  • 题目描述

    image.png

  • 题解

    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 的子数组

  • 题目描述

    image.png

  • 题解

    注意:为什么这题不可以用双指针/滑动窗口:因为nums[i]可以小于0,也就是说右指针i向后移1位不能保证区间会增大,左指针j向后移1位也不能保证区间和会减小。给定ji的位置没有二段性

    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. 复制带随机指针的链表

  • 题目描述

    image.png

  • 题解

    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. 最小覆盖子串

  • 题目描述

    image.png

  • 题解

    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. 最长重复子数组

  • 题目描述

    image.png

  • 题解

    记住,子序列默认不连续,子数组默认连续

    动态规划

    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;
        }
    }