哈希表
散列表(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("\n通过Map.keySet遍历key和value:");
for(String key:map.keySet()) {
System.out.println("Key: "+key+" Value: "+map.get(key));
}
//第二种
// 使用Map.entrySet()的迭代器
System.out.println("\n通过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("\n通过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("\n通过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);
}
方法三:如果元素固定,那么我们可以就使用一个一维数组来存储他们出现的频数。这也是哈希表法。
遍历字符串 p,记录字符频数
int[] sArr = new int[26];
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;
}
}
实例
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;
}
}
有效的字母异位词
给定两个字符串 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(n\log n)\),比较两个字符串是否相等时间复杂度为 \(O(n)\),因此总体时间复杂度为 \(O(n \log n+n)=O(n\log n)\)。
空间复杂度:\(O(\log n)\)。排序需要 \(O(\log n)\) 的空间复杂度。注意,在某些语言(比如 Java & JavaScript)中字符串是不可变的,因此我们需要额外的 \(O(n)\) 的空间来拷贝字符串。但是我们忽略这一复杂度分析,因为:
这依赖于语言的细节;
这取决于函数的设计方式,例如,可以将函数参数类型更改为 char[]。
方法二:哈希表
从另一个角度考虑,t 是 s 的异位词等价于「两个字符串中字符出现的种类和次数均相等」。由于字符串只包含 26 个小写字母,因此我们可以维护一个长度为 26 的频次数组 \(\textit{table}\),先遍历记录字符串 s 中字符出现的频次,然后遍历字符串 t,减去 \(\textit{table}\) 中对应的频次,如果出现 \(\textit{table}[i]<0\),则说明 t 包含一个不在 s 中的额外字符,返回 \(\text{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;
}
}
对于进阶问题,\(\text{Unicode}\) 是为了解决传统字符编码的局限性而产生的方案,它为每个语言中的字符规定了一个唯一的二进制编码。而 \(\text{Unicode}\) 中可能存在一个字符对应多个字节的问题,为了让计算机知道多少字节表示一个字符,面向传输的编码方式的 \(\text{UTF}-8\) 和 \(\text{UTF}-16\) 也随之诞生逐渐广泛使用,具体相关的知识读者可以继续查阅相关资料拓展视野,这里不再展开。
回到本题,进阶问题的核心点在于「字符是离散未知的」,因此我们用哈希表维护对应字符的频次即可。同时读者需要注意 \(\text{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的情况
}
}