zoukankan      html  css  js  c++  java
  • 【算法】哈希表法四部曲

    哈希表

    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

    给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

    关键码值与地址一一映射

    适用场景

    适用于关键字与某一值一一对应,即 可使用键值对map,而hashmap是键值对中较好的实现类

    关键词:一一对应

    遍历哈希表的四种方式

    public static void main(String[] args) {
      Map<String,String> map=new HashMap<String,String>();
            map.put("1", "value1");
            map.put("2", "value2");
            map.put("3", "value3");
            map.put("4", "value4");
            
            //第一种:普通使用,二次取值
            // 遍历键,取出值
            System.out.println("
    通过Map.keySet遍历key和value:");  
            for(String key:map.keySet()) {
              System.out.println("Key: "+key+" Value: "+map.get(key));
            }
            
            //第二种
            // 使用Map.entrySet()的迭代器
            System.out.println("
    通过Map.entrySet使用iterator遍历key和value: ");  
            Iterator map1it=map.entrySet().iterator();
            while(map1it.hasNext()) {
              Map.Entry<String, String> entry=(Entry<String, String>) map1it.next();
              System.out.println("Key: "+entry.getKey()+" Value: "+entry.getValue());
            }
            
            //第三种:推荐,尤其是容量大时
            // foreach
            System.out.println("
    通过Map.entrySet遍历key和value");  
            for(Map.Entry<String, String> entry: map.entrySet()) {
              System.out.println("Key: "+ entry.getKey()+ " Value: "+entry.getValue());
            }
            
            //第四种  
            // 遍历value
            System.out.println("
    通过Map.values()遍历所有的value,但不能遍历key");  
            for(String v:map.values()) {
              System.out.println("The value is "+v);
            }
     }

    输出结果:

    通过Map.keySet遍历key和value:
    Key: 1 Value: value1
    Key: 2 Value: value2
    Key: 3 Value: value3
    Key: 4 Value: value4
    
    通过Map.entrySet使用iterator遍历key和value: 
    Key: 1 Value: value1
    Key: 2 Value: value2
    Key: 3 Value: value3
    Key: 4 Value: value4
    
    通过Map.entrySet遍历key和value
    Key: 1 Value: value1
    Key: 2 Value: value2
    Key: 3 Value: value3
    Key: 4 Value: value4
    
    通过Map.values()遍历所有的value,但不能遍历key
    The value is value1
    The value is value2
    The value is value3
    The value is value4

    【推荐】使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。
    说明:keySet 其实是遍历了2次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。而entrySet只是遍历了一次就把key和value都放到了entry中,效
    率更高。如果是JDK8,使用Map.foreach方法。
    正例:values()返回的是V值集合,是一个list集合对象; keySet()返回的是K值集合,是一个Set集合对象; entrySet()返回的是K-V值组合集合。

    记录数组中元素出现频数

    遍历nums1,使用哈希表存储关键字,以及他们出现的次数

    方法一:遇到空的就赋初值,非空就+1

    // 1. 遍历nums1,使用哈希表存储关键字,以及他们出现的次数
    Map<Integer, Integer> map = new HashMap<>();
    
    for (int i = 0; i < nums1.length; i++) {
        if (map.get(nums1[i]) != null) {
            map.put(nums1[i], map.get(nums1[i])+1);
        } else {
            map.put(nums1[i], 1);
        }
    }

    方法二:使用getOrDefault()

    Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    
    for (int num : nums1) {
        int count = map.getOrDefault(num, 0) + 1;
        map.put(num, count);
    }

    方法三:如果元素固定,那么我们可以就使用一个一维数组new int[128]来存储他们出现的频数。这也是哈希表法。

    遍历字符串 p,记录字符频数

    // int[] sArr = new int[26];    // 26个字母时
    int[] sArr = new int[128];  // 所有ASCII码字符128个
    
    for (int i = 0; i < p.length(); i++) {
        sArr[s.charAt(i) - 'a']++;
    }

    方法四:如果要求元素只出现一次 或者判断是否有重复元素,那就可以用哈希集合

    Set<Integer, Integer> set = new HashSet<Integer>();
    
    for (int num : nums1) {
        // 添加此元素至 Set,加入失败那就代表有重复
        if(!set.add(num)) {
            return false;
        }
    }

    哈希表 + 列表

    哈希表 = <String, ArrayList>,键值对为<字符串, 列表>

    for (int i = 0; i < p.length(); i++) {
    
        if (!map.containsKey(keyStr)) { // 如果不存在此键
            map.put(keyStr, new ArrayList<>()); // 创建一个列表
        }
        map.get(keyStr).add(s); // 获取此键对应的列表,添加列表元素
    }

    面试题 10.02. 变位词组

    编写一种方法,对字符串数组进行排序,将所有变位词组合在一起。变位词是指字母相同,但排列不同的字符串。

    注意:本题相对原题稍作修改

    示例:

    输入: ["eat", "tea", "tan", "ate", "nat", "bat"],
    输出:
    [
      ["ate","eat","tea"],
      ["nat","tan"],
      ["bat"]
    ]

    答案

    用哈希表来存储列表,使字符串与列表一一对应。

    class Solution {
    
        public List<List<String>> groupAnagrams(String[] strs) {
            //边界条件判断
            if (strs == null || strs.length == 0)
                return new ArrayList<>();
    
            Map<String, List<String>> map = new HashMap<>();
    
            for (String s : strs) {
                
                char[] ca = new char[26];
                // 统计字符串中每个字符串出现的次数
                for (char c : s.toCharArray())
                    ca[c - 'a']++;
    
                // 统计每个字符出现次数的数组转化为字符串
                String keyStr = String.valueOf(ca);
    
                // map.getOrDefault(keyStr, new ArrayList<>()).add(s);
                // 不能用上面那个,因为没有put,我们只是获取到列表然后在列表后面添加而已,没有修改哈希表的值
                
                if (!map.containsKey(keyStr))
                    map.put(keyStr, new ArrayList<>());
                
                map.get(keyStr).add(s);
            }
            return new ArrayList<>(map.values());
        }
    
    }

    实例

    1. 两数之和

    给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。

    你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

    你可以按任意顺序返回答案。

    示例 1:
    输入:nums = [2,7,11,15], target = 9
    输出:[0,1]
    解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

    示例 2:
    输入:nums = [3,2,4], target = 6
    输出:[1,2]

    示例 3:
    输入:nums = [3,3], target = 6
    输出:[0,1]

    答案

    class Solution {
        public int[] twoSum(int[] nums, int target) {
    
            // 哈希表用来存放(关键字,下标)
            Map<Integer, Integer> map = new HashMap<>();
    
            // 遍历数组,每次遇到一个元素判断哈希表里面有没有与其对应的target - nums[i]元素
            // 如果有就返回下标,如果没有就把它的关键字和下标放进去,
            for (int i = 0; i < nums.length; i++) {
    
                Integer index = map.get(target - nums[i]);
                if (index != null) {
                    return new int[]{index, i};
                } else {
                    map.put(nums[i], i);
                }
            }
            return null;
        }
    }

    面试题 16.24. 数对和

    设计一个算法,找出数组中两数之和为指定值的所有整数对。一个数只能属于一个数对。

    示例 1:

    输入: nums = [5,6,5], target = 11
    输出: [[5,6]]

    示例 2:

    输入: nums = [5,6,5,6], target = 11
    输出: [[5,6],[5,6]]

    答案一:两次遍历

    遇到这题的第一反应就是,先哈希表确定每个元素的出现次数,再来找匹配的元素。

    class Solution {
        public List<List<Integer>> pairSums(int[] nums, int target) {
            //key:数组的元素;value:该元素出现的次数
            Map<Integer, Integer> map = new HashMap<>();
            
            List<List<Integer>> res = new ArrayList<>();
    
            for (int i = 0; i < nums.length; i++) {
                int count = map.getOrDefault(nums[i], 0) + 1;
                map.put(nums[i], count);
            }
    
            for (int i = 0; i < nums.length; i++) {
                int count1 = map.getOrDefault(nums[i], 0);
                map.put(nums[i], count1 - 1);
    
                int count2 = map.getOrDefault(target - nums[i], 0);
                map.put(target - nums[i], count2 - 1);
    
                if (count1 > 0 && count2 > 0) {
                    List<Integer> temp = new ArrayList<>();
    
                    temp.add(nums[i]);
                    temp.add(target - nums[i]);
    
                    res.add(temp);
                }
            }
            
            return res;
        }
    }

    答案二:一次遍历

    后来我们开始思考,能不能边遍历边寻找匹配的元素呢?

    其实我们可以在遍历的时候,直接看有没有匹配的

    • 如果有,那就匹配的那个元素-1,当前元素就不+1了,+1-1抵消了
    • 如果没有,那就当前元素+1
    class Solution {
        public List<List<Integer>> pairSums(int[] nums, int target) {
            //key:数组的元素;value:该元素出现的次数
            Map<Integer, Integer> map = new HashMap<>();
            
            List<List<Integer>> res = new ArrayList<>();
            for (int num : nums) {
                // 在遍历的时候,直接看有没有匹配的
                // 如果有,那就匹配的那个元素-1
                // 如果没有,那就当前元素+1
                int count = map.getOrDefault(target - num, 0);
                if (count > 0) {
                    res.add(Arrays.asList(num, target - num));
                    map.put(target - num, --count);
                } else {
                    map.put(num, map.getOrDefault(num, 0) + 1);
                }
            }
            
            return res;
        }
    }

    有效的字母异位词

    给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

    示例 1:

    输入: s = "anagram", t = "nagaram"
    输出: true
    示例 2:

    输入: s = "rat", t = "car"
    输出: false
    说明:
    你可以假设字符串只包含小写字母。

    进阶:
    如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?

    答案

    方法一:排序

    t 是 s 的异位词等价于「两个字符串排序后相等」。因此我们可以对字符串 s 和 t 分别排序,看排序后的字符串是否相等即可判断。此外,如果 s 和 t 的长度不同,t 必然不是 s 的异位词。

    Java

    class Solution {
        public boolean isAnagram(String s, String t) {
            if (s.length() != t.length()) {
                return false;
            }
            char[] str1 = s.toCharArray();
            char[] str2 = t.toCharArray();
            Arrays.sort(str1);
            Arrays.sort(str2);
            return Arrays.equals(str1, str2);
        }
    }

    复杂度分析

    • 时间复杂度:(O(n log n)),其中 n 为 s 的长度。排序的时间复杂度为 (O(nlog n)),比较两个字符串是否相等时间复杂度为 (O(n)),因此总体时间复杂度为 (O(n log n+n)=O(nlog n))

    • 空间复杂度:(O(log n))。排序需要 (O(log n)) 的空间复杂度。注意,在某些语言(比如 Java & JavaScript)中字符串是不可变的,因此我们需要额外的 (O(n)) 的空间来拷贝字符串。但是我们忽略这一复杂度分析,因为:

    这依赖于语言的细节;
    这取决于函数的设计方式,例如,可以将函数参数类型更改为 char[]。

    方法二:哈希表

    前面我们说过了关键码值与地址一一映射,就可以称为哈希表(即 散列表),所以此处的方法也可以称为哈希表法。

    从另一个角度考虑,t 是 s 的异位词等价于「两个字符串中字符出现的种类和次数均相等」。由于字符串只包含 26 个小写字母,因此我们可以维护一个长度为 26 的频次数组 ( extit{table}),先遍历记录字符串 s 中字符出现的频次,然后遍历字符串 t,减去 ( extit{table}) 中对应的频次,如果出现 ( extit{table}[i]<0),则说明 t 包含一个不在 s 中的额外字符,返回 ( ext{false}) 即可。

    Java

    class Solution {
        public boolean isAnagram(String s, String t) {
            if (s.length() != t.length()) {
                return false;
            }
            int[] table = new int[26];
            for (int i = 0; i < s.length(); i++) {
                table[s.charAt(i) - 'a']++;
            }
            for (int i = 0; i < t.length(); i++) {
                table[t.charAt(i) - 'a']--;
                if (table[t.charAt(i) - 'a'] < 0) {
                    return false;
                }
            }
            return true;
        }
    }

    对于进阶问题,( ext{Unicode}) 是为了解决传统字符编码的局限性而产生的方案,它为每个语言中的字符规定了一个唯一的二进制编码。而 ( ext{Unicode}) 中可能存在一个字符对应多个字节的问题,为了让计算机知道多少字节表示一个字符,面向传输的编码方式的 ( ext{UTF}-8)( ext{UTF}-16) 也随之诞生逐渐广泛使用,具体相关的知识读者可以继续查阅相关资料拓展视野,这里不再展开。

    回到本题,进阶问题的核心点在于「字符是离散未知的」,因此我们用哈希表维护对应字符的频次即可。同时读者需要注意 ( ext{Unicode}) 一个字符可能对应多个字节的问题,不同语言对于字符串读取处理的方式是不同的。

    Java

    class Solution {
        public boolean isAnagram(String s, String t) {
            if (s.length() != t.length()) {
                return false;
            }
            Map<Character, Integer> table = new HashMap<Character, Integer>();
            for (int i = 0; i < s.length(); i++) {
                char ch = s.charAt(i);
                table.put(ch, table.getOrDefault(ch, 0) + 1);
            }
            for (int i = 0; i < t.length(); i++) {
                char ch = t.charAt(i);
                table.put(ch, table.getOrDefault(ch, 0) - 1);
                if (table.get(ch) < 0) {
                    return false;
                }
            }
            return true;
        }
    }

    复杂度分析

    • 时间复杂度:(O(n)),其中 n 为 s 的长度。

    • 空间复杂度:(O(S)),其中 S 为字符集大小,此处 (S=26)

    剑指 Offer 61. 扑克牌中的顺子

    从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

    示例 1:

    输入: [1,2,3,4,5]
    输出: True

    示例 2:

    输入: [0,0,1,2,5]
    输出: True

    答案

    class Solution {
        public boolean isStraight(int[] nums) {
            Set<Integer> repeat = new HashSet<>();
            int max = 0, min = 14;
            for(int num : nums) {
                if(num == 0) continue; // 跳过大小王
                max = Math.max(max, num); // 最大牌
                min = Math.min(min, num); // 最小牌
                // 添加此牌至 Set,加入失败那就代表有重复
                if(!repeat.add(num)) return false; // 若有重复,提前返回 false
                 
            }
            return max - min + 1 <= 5; // 最大牌 - 最小牌 + 1 <= 5 则可构成顺子,因为包含有0、0、0、9、11的情况
        }
    }

    面试题 01.04. 回文排列

    给定一个字符串,编写一个函数判定其是否为某个回文串的排列之一。

    回文串是指正反两个方向都一样的单词或短语。排列是指字母的重新排列。

    回文串不一定是字典当中的单词。

    示例1:

    输入:"tactcoa"
    输出:true(排列有"tacocat"、"atcocta",等等)

    答案

    主要利用函数统计出s中所有重复次数为奇数次元素的个数,如果个数为1或0,则该字符串是一个回文串,否则就不是

    class Solution {
        public boolean canPermutePalindrome(String s) {
            char[] map = new char[128]; // 这里需要存储整个ASCII字符,所以128
            int ans = 0;
    
            for (int i = 0; i < s.length(); i++) {
                map[s.charAt(i)]++;
            }
    
            for (int i = 0; i < map.length; i++) {
                if (map[i] % 2 == 1) {
                    ans++;
                }
            }
            return ans <= 1;
        }
    }
    后海有树的院子,夏代有工的玉,此时此刻的云,二十来岁的你。——《可遇不可求的事》

    笔者将不定期更新【考研或就业】的专业相关知识以及自身理解,希望大家能【关注】我。
    如果觉得对您有用,请点击左下角的【点赞】按钮,给我一些鼓励,谢谢!
    如果有更好的理解或建议,请在【评论】中写出,我会及时修改,谢谢啦!
    关注
    评论
    收藏
    Top
  • 相关阅读:
    centos 编码问题 编码转换 cd到对应目录 执行 中文解压
    centos 编码问题 编码转换 cd到对应目录 执行 中文解压
    centos 编码问题 编码转换 cd到对应目录 执行 中文解压
    Android MVP 十分钟入门!
    Android MVP 十分钟入门!
    Android MVP 十分钟入门!
    Android MVP 十分钟入门!
    mysql备份及恢复
    mysql备份及恢复
    mysql备份及恢复
  • 原文地址:https://www.cnblogs.com/blknemo/p/14473441.html
Copyright © 2011-2022 走看看