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
  • 相关阅读:
    ASP.NET
    JS、JQury
    ASP.NET
    ASP.NET
    ASP.NET
    C#
    C#
    C#
    基础算法-树:预备知识
    开发环境配置--Ubuntu+Qt4+OpenCV(二)
  • 原文地址:https://www.cnblogs.com/blknemo/p/14473441.html
Copyright © 2011-2022 走看看