摘要
本文主要介绍了哈希表理论基础,并附带了LeetCode上的几道题目,包括242.有效的字母异位词、349.两个数组的交集、202. 快乐数、1.两数之和的解题思路与示例代码。
1、哈希表理论基础
1.1 概念
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
哈希表(Hash Table)是一种数据结构,它可以用于在常数时间复杂度内(平均情况下)执行插入、删除和查找操作。在Java中,哈希表通常是通过HashMap类实现的,HashMap基于哈希表的原理来存储键-值对。
以下是关于Java中的哈希表(HashMap)的一些关键特点和用法:
- 基本用法:
HashMap是Java集合框架中的一部分,用于存储键-值对。你可以使用put(key, value)方法添加键-值对,使用get(key)方法获取值,使用remove(key)方法删除键-值对。 - 键的唯一性:在一个
HashMap中,键是唯一的。如果你尝试使用相同的键添加多个值,后面的值会覆盖先前的值。 - 性能:
HashMap提供了快速的查找性能,因为它使用了哈希函数将键映射到存储桶中,从而允许在常数时间内查找值。但是,性能可能会受到哈希碰撞的影响。 - 哈希碰撞:哈希碰撞是指不同的键被映射到相同的存储桶中。
HashMap使用链表或红黑树等数据结构来解决碰撞问题,以保证性能。 - 初始容量和负载因子:
HashMap允许你指定初始容量和负载因子。初始容量是哈希表中存储桶的数量,默认为16。负载因子是一个浮点数,它表示当哈希表的大小达到容量乘以负载因子时,将触发哈希表的重新调整大小操作。通常,较低的负载因子可以减少哈希碰撞,但会增加内存开销。 - 线程安全性:
HashMap不是线程安全的,如果在多线程环境下使用,必须采取额外的同步措施或使用ConcurrentHashMap等线程安全的替代品。 - 遍历:你可以使用迭代器或Java 8的Lambda表达式来遍历
HashMap中的键-值对。 - 性能注意事项:在使用
HashMap时,要特别注意选择合适的哈希函数和合适的初始容量和负载因子,以免性能问题。
总之,Java中的哈希表(HashMap)是一种强大的数据结构,适用于许多应用场景,但要注意性能和线程安全性方面的考虑。
1.2 3种常用数据结构
哈希表是一种非常常见的数据结构,它在计算机科学中有着广泛的应用。哈希表通常基于哈希函数将键映射到值的数据结构。以下是哈希表的三种常用数据结构:数组、集合(Set)、映射(Map)。
-
数组:
- 数组是最简单的哈希表数据结构,也是最基本的数据结构之一。
- 哈希表通过哈希函数将键直接映射到数组的索引位置。这种方式非常高效,查找时间复杂度为O(1)。
- 数组只能用于处理键的范围有限且连续的情况,例如用于计数的情况。
-
集合(Set) :
- 集合是一种特殊的哈希表,它存储不重复的元素。
- 集合只关心键(元素)的存在与否,不存储键值对。
- 常见的集合数据结构有HashSet、LinkedHashSet和TreeSet。HashSet是基于哈希表的,查找时间复杂度为O(1)。
-
映射(Map) :
- 映射是一种常见的哈希表,它存储键值对。
- 键值对中的键是唯一的,而值可以重复。
- 常见的映射数据结构有HashMap、LinkedHashMap和TreeMap。HashMap是基于哈希表的,查找时间复杂度为O(1)。
这些哈希表数据结构在不同的场景下有不同的用途。数组通常用于处理简单的计数问题,集合用于存储一组唯一的元素,而映射则用于存储键值对,例如在存储配置信息、缓存数据等方面非常常见。选择适当的哈希表数据结构取决于你的需求和数据特性。
1.3 哈希表的操作
Java中的哈希表主要是通过HashMap类来实现的,它提供了一系列常用的操作。以下是Java中HashMap的一些常见操作:
-
插入键值对:使用
put(key, value)方法向哈希表中插入键值对。如果该键已经存在,新的值将覆盖旧的值。HashMap<String, Integer> hashMap = new HashMap<>(); hashMap.put("apple", 1); hashMap.put("banana", 2); -
获取值:使用
get(key)方法根据键获取值。int value = hashMap.get("apple"); // 获取键"apple"对应的值 -
删除键值对:使用
remove(key)方法根据键删除键值对。hashMap.remove("apple"); // 删除键"apple"对应的键值对 -
判断键是否存在:使用
containsKey(key)方法来检查键是否存在。boolean exists = hashMap.containsKey("banana"); // 检查键"banana"是否存在 -
获取键集合:使用
keySet()方法获取哈希表中所有键的集合。Set<String> keys = hashMap.keySet(); // 获取键的集合 -
获取值集合:使用
values()方法获取哈希表中所有值的集合。Collection<Integer> values = hashMap.values(); // 获取值的集合 -
遍历键值对:可以使用
entrySet()方法获取包含键值对的Set,然后遍历它。for (Map.Entry<String, Integer> entry : hashMap.entrySet()) { String key = entry.getKey(); int value = entry.getValue(); // 处理键值对 } -
获取大小:使用
size()方法获取哈希表中键值对的数量。int size = hashMap.size(); // 获取键值对数量 -
清空哈希表:使用
clear()方法清空哈希表,删除所有键值对。hashMap.clear(); // 清空哈希表 -
默认值:如果键不存在时,可以使用
getOrDefault(key, defaultValue)方法来获取一个默认值。int value = hashMap.getOrDefault("cherry", 0); // 获取键"cherry"对应的值,如果不存在返回默认值0
这些是HashMap类的一些常见操作,可以用于在Java中使用哈希表。根据具体需求,你可以选择不同的操作来管理和操作哈希表中的数据。
1.4 哈希Set的操作
哈希集(HashSet)是Java中的一种常见数据结构,它实现了Set接口,用于存储不重复的元素。以下是哈希集的一些常用操作:
- 添加元素:使用
add(element)方法向哈希集中添加元素。如果元素已存在,添加操作将被忽略,因为哈希集不允许重复元素。
Set<String> hashSet = new HashSet<>();
hashSet.add("apple");
hashSet.add("banana");
- 移除元素:使用
remove(element)方法从哈希集中移除指定元素。
hashSet.remove("banana");
- 检查元素是否存在:使用
contains(element)方法检查哈希集中是否包含指定元素。
boolean contains = hashSet.contains("apple");
- 获取集合大小:使用
size()方法获取哈希集的大小,即包含的不重复元素个数。
int size = hashSet.size();
- 遍历集合:可以使用迭代器或增强型for循环遍历哈希集中的元素。
for (String element : hashSet) {
System.out.println(element);
}
- 清空集合:使用
clear()方法清空哈希集,使其不包含任何元素。
hashSet.clear();
- 判断是否为空:使用
isEmpty()方法检查哈希集是否为空。
boolean isEmpty = hashSet.isEmpty();
哈希集通常用于需要存储一组元素且不允许重复的情况。它的实现基于哈希表,因此添加、查找和移除元素的操作通常具有很快的平均时间复杂度。
2、242.有效的字母异位词
2.1 HashMap
思路
使用hashmap,key表示字母,value表示次数 首先遍历字符串 s存储字母元素并计数 然后遍历字符串 t如果不存在相应字母则返回false,反之给字母相应的计数减1,如果计数为0则删除元素 最后如果map为空则互为字母异位词
代码
public boolean isAnagram(String s, String t) {
Map<Character,Integer> map = new HashMap<>();
for(int i=0; i<s.length(); i++) {
char ch = s.charAt(i);
Integer count = map.getOrDefault(ch, 0);
map.put(ch, ++count);
}
for(int j=0; j<t.length(); j++) {
char ch = t.charAt(j);
if(map.containsKey(ch)) {
Integer count = map.get(ch);
map.put(ch, --count);
if(count == 0) {
map.remove(ch);
}
} else {
return false;
}
}
return map.size() == 0;
}
2.2 数组
思路
使用int数组,数组下标0-25代表a-z,数组的值代表次数 遍历字符串s,int数组中相应的元素 + 1;遍历字符串t,int数组中相应的元素 -1; 最后遍历int数组,如果互为字母异位词,则最后int数组中的元素值都为 0
s.charAt(i) - 'a':并不需要记住字符a的ASCII,只要求出一个相对数值就可以了arr[s.charAt(i) - 'a'] += 1可以优化为arr[s.charAt(i) - 'a']++
代码
public boolean isAnagram(String s, String t) {
int[] arr = new int[26];
for (int i = 0; i < s.length(); i++) {
arr[s.charAt(i) - 'a'] += 1;
}
for (int j = 0; j < t.length(); j++) {
arr[t.charAt(j) - 'a'] -= 1;
}
for(int item : arr) {
if(item != 0) {
return false;
}
}
return true;
}
3、349.两个数组的交集
3.1 思路
遍历num1, 定义一个set1存储nums1;遍历nums2, 定义set2存储交集元素,最后将set2转换为int数组返回
3.2 代码
// 遍历num1, 定义一个set1存储nums1;遍历nums2, 定义set2存储交集元素,最后将set2转换为int数组返回
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new HashSet<>();
for(int num1 : nums1) {
set1.add(num1);
}
for(int num2 : nums2) {
if(set1.contains(num2)) {
set2.add(num2);
}
}
int[] res = new int[set2.size()];
int i = 0;
for(Integer item : set2) {
res[i++] = item;
}
return res;
}
4、202. 快乐数
4.1 思路
1、判断一个数 n 是不是快乐数?
题目中说了会无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
- 定义一个set保存sum,如果无线循环,则set.contains(sum)==true,反之直到结果为1,证明其为快乐数
2、如何计算平方和?
示例:n = 19;
- 从左到右,从低到高、num = (n % 10)、 计算平方和、n = (n /10) ,直至 n = 0,即 while(n > 0)
4.2 代码
// 定义一个set保存sum,如果无线循环,则set.contains(sum)==true,反之直到结果为1,证明其为快乐数
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<>();
while(n != 1) {
// 计算平方和,判断是否重复
int sum = getSum(n);
if(set.contains(sum)) {
return false;
}
// 继续循环,判断是否是快乐数
set.add(sum);
n = sum;
}
return true;
}
// 计算平方和
public int getSum(int n) {
int sum = 0;
while(n > 0) {
int num = n % 10;
sum += num * num;
n = n / 10;
}
return sum;
}
5、1.两数之和
5.1 暴力法
思路
暴力法,双层遍历nums,判断是否存在ums[i]+nums[j]==target; 取值范围:i=[0,len-1], j=[i+1, len]
代码
public int[] twoSum(int[] nums, int target) {
for(int i=0; i<nums.length-1; i++) {
for(int j=i+1; j<nums.length; j++) {
if(nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return new int[]{-1, -1};
}
5.2 HashMap
思路
将遍历过的元素保存到hashmap,key表示nums[i], value表示 i; key表示遍历过的元素,判断是否存在 key + num[i] == target
- 数组中同一个元素在答案里不能重复出现
代码
AC-1
两次遍历:一开始并没有想到一次遍历的方式,因为只操作一个数组,所以只需要遍历一次数组即可
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for(int i=0; i<nums.length; i++) {
map.put(nums[i], i);
}
for(int j=0; j<nums.length; j++) {
int key = target - nums[j];
if(map.containsKey(key)) {
int i = map.get(key);
if(i != j) {
return new int[]{i, j};
}
}
}
return new int[]{-1, -1};
}
AC-2
一次遍历
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for(int i=0; i<nums.length; i++) {
int key = target - nums[i];
if(map.containsKey(key)) {
int j = map.get(key);
return new int[]{i, j};
}
map.put(nums[i], i);
}
return new int[]{-1, -1};
}