哈希表
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
哈希表是根据关键码的值而直接进行访问的数据结构。
这么官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
哈希函数
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样我们就保证了学生姓名一定可以映射到哈希表上了。
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
这里数组就没啥可说的了,我们来看一下set。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set和std::multiset 的底层实现基于红黑树而非哈希表,它们通过红黑树来索引和存储数据。不过给我们的使用方式,还是哈希法的使用方式,即依靠键(key)来访问值(value)。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。std::map也是一样的道理。
这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?
实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
摘录于代码随想录
242.有效的字母异位词
题目:给定两个字符串 s
和 t
,编写一个函数来判断 t
是否是 s
的 字母异位词
正确代码
class Solution {
public boolean isAnagram(String s, String t) {
int [] hash=new int[26];
for(int i=0;i<s.length();i++){
hash[s.charAt(i)-'a']++;
}
for(int i=0;i<t.length();i++){
hash[t.charAt(i)-'a']--;
}
for(int i=0;i<26;i++){
if(hash[i]!=0)
return false;
}
return true;
}
}
手稿
我遇到的问题
1.怎么把一个数组的数据值都为0
int[ ] intArray = new int[5]; // 元素初始化为0
2.s.length()和s.length有什么区别
在Java编程语言中,s.length()
和 s.length
这两个表达式有着本质的区别,主要体现在它们的应用对象和数据类型上。
s.length()
:- 这个方法主要用于
String
类型的对象。 - 它返回的是一个
int
类型的值,代表字符串s
的长度,即字符串中字符的总数。 - 值得注意的是,
length()
方法末尾有一个括号,这明确表明它是一个方法调用。
- 这个方法主要用于
s.length
:- 这个属性或方法(取决于上下文)并不适用于
String
类型,但在其他场合可能出现。 - 在Java的数组中,
length
是一个属性,用于获取数组的长度,它同样返回一个int
类型的值。 - 例如,若
int[] arr = {1, 2, 3};
,则arr.length
会返回3,表示数组arr
中有3个元素。 - 对于
String
类型的对象,使用s.length
是不合法的,因为String
类中并没有定义这样的属性或不带参数的方法。
- 这个属性或方法(取决于上下文)并不适用于
简而言之,当你处理String
类型的对象时,应使用s.length()
来获取字符串的长度;而当你需要了解数组的长度时,则应使用array.length
(这里的array
代表任意类型的数组)。这两者在Java中有着不同的用途和语法规则。如果你尝试在String
对象上使用s.length
,编译器会报错,提示你找不到该方法或属性。
3.为什么 hash[s[i]-'a']++;报错
将s[i]
和t[i]
替换为s.charAt(i)
和t.charAt(i)
。因为String
在内部使用char[]
来存储字符),使用charAt()
方法更符合Java的惯用法。
349. 两个数组的交集
题意:给定两个数组,编写一个函数来计算它们的交集。
使用数组来做哈希的题目,是因为题目都限制了数值的大小。
而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
本题后面 力扣改了 题目描述 和 后台测试数据,增添了 数值范围:
- 1 <= nums1.length, nums2.length <= 1000
- 0 <= nums1[i], nums2[i] <= 1000
所以就可以 使用数组来做哈希表了, 因为数组都是 1000以内的。
std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
正确代码
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
return new int[0];
}
Set <Integer> set1=new HashSet<>();
Set <Integer> set2=new HashSet<>()
for(int i:nums1){
set1.add(i);
}
for(int i:nums2){
if(set1.contains(i)){
set2.add(i);
}
}
/*return set2.stream()
.mapToInt(Integer::intValue) // 使用 Integer::intValue 方法来转换
.toArray(); // 这将调用 IntStream 的无参 toArray 方法,返回 int[]*/
//申请一个数组存放
int[] arr=new int [set2.size()];
int j=0;
for(int i :set2){
arr[j++]=i;
} return arr }}
手稿
我遇到的问题
1.怎么创建两个HashSet
类型的集合:set1
和set2
Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new HashSet<>();
2.怎么将结果集合转为数组,想要返回一个 int[]
数组
return set2.stream() .mapToInt(Integer::intValue) .toArray();
// 使用 Integer::intValue 方法来转换
Integer::intValue
是一个方法引用,它指向 Integer
类的 intValue
方法。这个方法是从 Integer
对象中提取其原始 int
值的标准方式。当你有一个 Integer
对象,但需要一个原始的 int
类型值时,你会调用这个方法。
// 这将调用 IntStream 的无参 toArray 方法,返回 int[]
3.或者说,怎么申请一个数组存放set2中的元素,然后返回数组
//方法2:另外申请一个数组存放setRes中的元素,最后返回数组
4.size和length有什么区别
- size:
- 在Java中,
size
通常用于表示集合(如ArrayList
,HashSet
,HashMap
等)中元素的数量。 - 它是通过调用集合类中的
size()
方法来获取的。 - 例如,
ArrayList<String> list = new ArrayList<>();
你可以使用list.size()
来获取列表中元素的数量。
- 在Java中,
- length:
length
通常用于表示数组的长度(即数组中元素的数量)。- 对于一维数组,你可以直接使用
.length
属性来获取数组的长度。 - 例如,
int[] numbers = {1, 2, 3, 4, 5};
你可以使用numbers.length
来获取数组的长度。 - 需要注意的是,
length
是一个属性,而不是一个方法,因此它后面不需要加括号。 length
也可以用于表示字符串的长度,因为字符串在Java中是通过字符数组来实现的。- 例如,
String str = "Hello";
你可以使用str.length()
来获取字符串的长度(注意这里是方法,因为字符串类是对象化的,所以长度是通过方法获取的,尽管这个方法没有参数,并且看起来像属性)。
1. 两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
我遇到的问题
containsKey(balance)这是什么
containsKey(Object key)
是 Map
接口中的一个方法。这个方法用于检查调用它的 Map
对象(如 HashMap
, TreeMap
, LinkedHashMap
等)是否包含一个特定的键(key
)。
这里的 balance
看起来是传递给 containsKey
方法的参数,意味着代码正在检查某个 Map
对象是否有一个键等于 balance
的条目。如果 Map
包含指定的键,则此方法返回 true
;如果不包含,则返回 false
。
正确代码
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> indexMap =new HashMap<>();
for(int i=0;i<nums.length;i++){
int s=target-nums[i];
if(indexMap.containsKey(s))
{
return new int[]{i,indexMap.get(s)};}
else{indexMap.put(nums[i],i);}
}return null;}
}
手稿