一般哈希表都是用来快速判断一个元素是否出现集合里
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(log n) | O(log n) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set
底层实现为哈希表std::set
和std::multiset
的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(log n) | O(log n) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::map
和std::multimap
的底层实现是红黑树 同理,std::map
和std::multimap
的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
s[i] - ‘a’
所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。class Solution {
//两种方法 排序 哈希表
public boolean isAnagram(String s, String t) {
int[] record=new int[26];
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i)-'a']++; 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
}
for (int i = 0; i < t.length(); i++) {
record[t.charAt(i)-'a']--;
}
for (int count : record) {
if (count!=0) { //record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
return true;
}
}
学会用字符下标来映射为数组下标这是本题要学会的
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);
}
}
给定两个数组
nums1
和nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
《代码随想录》建议:
本题就开始考虑 什么时候用set 什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和测试用例,添加了
0 <= nums1[i]
,nums2[i] <= 1000
条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用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> resSet=new HashSet<>();//结果集 set集合不可重复
//将nums1遍历存入哈希表结构中
for (int i : nums1) {
set1.add(i);
}
//遍历nums2,判断哈希表中是否有相同的值,并存入
for (int i : nums2) {
if (set1.contains(i)) {
resSet.add(i);
}
}
//方法1:将结果集合转为数组
return resSet.stream().mapToInt(x -> x).toArray();
//将结果集转换为数组
int[] arr=new int[resSet.size()];
int j=0;
for (Integer integer : resSet) {
arr[j++]=integer;
}
return arr;
}
}
时间复杂度是 (m+n),其中 m 和 n 分别是数组 nums1
和 nums2
的长度。
在第一个循环中,我们需要遍历数组
nums1
的所有元素,将其放入set1
中,时间复杂度为 O(m)。在第二个循环中,我们需要遍历数组
nums2
的所有元素,对于每个元素,需要在set1
中进行查找,这个操作的时间复杂度是 O(1),因为使用了哈希表进行存储,所以这个循环的时间复杂度为 O(n)。最后,将
resSet
转换为数组,需要遍历resSet
中的所有元素,时间复杂度为O(k),其中 k 是最终结果集合中元素的个数。因此,时间复杂度为 O(m+n+k),实际上也就是O(m+n)。
空间复杂度O(min(m,n))
空间复杂度主要是由两个哈希表所占用的空间决定的。
首先是
set1
哈希表,其最坏的情况下需要存储所有的nums1
中的元素,所以其空间复杂度是O(m)。然后是
resSet
哈希表,最坏的情况下需要存储所有nums1
和nums2
中的交集元素,所以其空间复杂度是 O(min(m,n))。因此,代码的空间复杂度是 O(min(m,n))。
resSet.stream().mapToInt(x -> x).toArray();
通过调用 resSet 对象的 stream() 方法,将其转换成一个流对象 Stream。在这个流对象上我们调用 mapToInt() 方法,将其中的每个元素都转换为 int 类型,再用 toArray() 方法将该流对象转换为一个 int 类型的数组。
mapToInt()
方法接受一个函数式接口 ToIntFunction
作为参数,该接口定义了一个只接受一个参数并返回一个 int 值的方法。在这里,我们使用Lambda表达式 x -> x
实现了 ToIntFunction
的接口方法,将传入的参数直接返回。
x -> x
是一个 Lambda 表达式,它实现了一个只有单个参数的函数式接口。这个 Lambda 表达式表示的函数mapToInt()的参数是 x
,函数体是 x
。换句话说,这个函数会把接收到的参数原封不动地返回。这种写法称为“恒等函数”(identity function)。
在这个例子中,我们可以使用这个 Lambda 表达式实现一个 ToIntFunction
接口的方法,该方法会将参数本身转换成一个 int
值。这是因为,这个 Lambda 表达式的参数和返回值的类型都是 Integer
,而该类型已经实现了将自己转换成一个 int
值的方法,即 intValue()
。
需要注意的是,x -> x
这种写法其实相当于函数定义 int f(int x){return x;}
。不同的是,Lambda 表达式是一种匿名函数,它没有名称,也没有方法声明。
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> res=new HashSet<>();
int[] hash=new int[1005];
// nums1中出现的字母在hash数组中做记录
for (int i : nums1) {
hash[i]=1;//标记为1
}
for (int i : nums2) {
if (hash[i]==1) {
res.add(i);
}
}
//集合转数组
int[] arr=new int[res.size()];
int j=0;
for (int re : res) {
arr[j++]=re;
}
return arr;
}
}
时间复杂度:O(m+n)
空间复杂度:
空间复杂度主要是由 hash
数组和 Set
集合所占用的空间决定的。hash
数组长度最大为 1005,所以空间复杂度为 O(1)。Set
集合保存了交集元素,所占用的空间取决于 nums1
和 nums2
的交集元素数量,最坏的情况下为 min(m,n),所以空间复杂度为 O(min(m,n))。
编写一个算法来判断一个数
n
是不是快乐数.
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果
n
是 快乐数 就返回true
;不是,则返回false
。
根据探索,猜测会有以下三种可能:
最终会得到 1
最终会进入循环
值会越来越大,最后接近无穷大——>这种永远不会发生,证明:链接
所以共有2种可能
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
当遇到要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
另一个难点就是求和的过程,对取数值各个位上的单数操作。
算法分析,分为两部分:
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
while (n != 1 && !record.contains(n)) {
record.add(n);
n = getNextNumber(n);
}
return n == 1;
}
private int getNextNumber(int n) {
int res = 0;
while (n > 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
}
时间复杂度:O(logn),其中n是输入的数字。
空间复杂度:O(logn)
给定一个整数数组
nums
和一个整数目标值target
,请你在该数组中找出 和为目标值target
的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
1. 为什么想到用哈希法?
将遍历过的元素放到一个集合中,每次遍历新的位置的时候,就判断和目前遍历元素匹配的元素是否出现在这个集合中,如果出现就是遍历过
2. 用什么样的哈希表?
在本题中,我们不仅要知道元素是否被遍历过,还要知道元素所对应的下标,所以使用map
3. 为什么不选用数组或者set呢?
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而这道题目,不仅要判断temp是否存在而且还要记录temp的下标位置,因为要返回对应的下标。所以set 也不能用
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
class Solution {
public int[] twoSum(int[] nums, int target) {
//map集合
HashMap<Integer, Integer> map = new HashMap<>();
//遍历数组
for (int i = 0; i < nums.length; i++) {
int temp = target - nums[i];
if (map.containsKey(temp)) {//在map中寻找是否有nums[i]匹配的key key存元素 value存下标
return new int[]{map.get(temp), i};//返回角标
}
//若不包含,则把数组添加入到map集合
map.put(nums[i], i);
}
return new int[0];
}
}
时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,可以O(1) 地寻找 target - x。
空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。