【算法】哈希表法四部曲

哈希表

散列表(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
原文地址:https://www.cnblogs.com/blknemo/p/14473441.html