代码随想录一刷笔记_哈希表
60+在408的算法考察难度和深度上还是收手了👍)
哈希表理论基础
哈希表用于快速判断一个元素是否出现在集合里,是一种用空间换时间的方法。
当数据通过函数映射到同一位置时,会出现哈希碰撞的现象,一般有线性探测法和拉链法两种办法应对。
数组、set、map三种数据结构的适用情况:
- 数组:适用于数据量小,数据集中的情况
- set:适用于数据量大、数据离散的情况
- map:适用于数据量大,需要存储键值对的情况
有效的字母异位词(242)
这一道题只定义一个数组即可解决。
①遍历两串字母,将遇到的字母填入对应的哈希表序列
②遍历数组,查找是否有异位的字母。
- tips:相关题目找到字符串中所有字母异位词(438)中,需要考虑避免下标越界的问题。(似乎本题还可以使用滑动窗口的思想)
两个数组的交集(349)
这一题主要是科普了set这类数据结构 & 范围for循环的语法。
set
适合使用set的情况:数据很多、数值很分散的情况。(PS:如果需求对重复数据有特别要求需慎重考虑 unordered_set!)
用set这种数据结构存储数据可以实现去重的效果。
1
2
3
4
5// 定义一个 unordered_set 类型数据结构
unordered_set<int> result_set;
// 将num1容器中的数据存入 nums_set 中时,能够实现去重 & 排序 功能
unordered_set<int> nums_set(num1.begin(), num1.end());
范围for循环
提供了一种简洁的方式遍历容器(数组,vector,list)中的元素
局限:不能提供索引值
1
2
3for (declaration: range) {
....
}
快乐数(202)
由于不知道输入的数据大小,所以如果使用数组的话需要留很大的冗余。故,这类题目更适合使用set这种数据结构回答。
set提供了一个成员函数find(),用于在容器中查找特定的元素,返回值取决于查找操作的结果:
如果找到了指定的元素,find()函数会返回一个指向该元素的迭代器;
如果没有找到指定的元素,find()函数会返回容器的end()迭代器。
end()迭代器是一个哨兵值,其不指向容器中的任何元素,而是作为一个标记,表示容器的末尾。
在遍历容器时,通常使用end()迭代器来表示何时停止遍历。
1
2// 判断计算得到的数据是否已经出现过
set.find(sum) != set.end();
两数之和(1)
这一题的需求是需要存储考虑存储数组index以及数据,所以适用map这种数据结构。
auto关键字:
auto是在c++11标准中引入的,用于自动推导变量的类型。
1
2
3// 在这一行代码中,iter的类型应该是 std::map<Key, T>::iterator
// 在这一行代码中,map.find() 方法寻找的是键为 target - nums[i] 的元素。(关键字是唯一的!)
auto iter = map.find(target - nums[i]);unordered_map
访问map中value的值
1
2
3
4
5
6
7// 在这里,iter表示指向map的一个有效元素
// iter->first 表示访问迭代器iter所指向元素的键部分
// iter->second 表示访问迭代器iter所指向的元素的值部分
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}insert方法
1
2
3
4// insert方法
// pair<int, int>(a, b) 创建了一个临时的 std::pair 对象
// insert需要一个pair类型的参数,而不是两个单独的值
map.insert(pair<int, int>(a + b, c));
四数相加Ⅱ(454)
这一题如果使用暴力解法的话是需要四层for循环,时间复杂度是O(n^4)。故,我们需要对其进行降重操作(采用nums1和nums2,nums3和nums4两两相加的方式)。
接下来的问题是采用哪一种数据结构:
- 首先在定义里,数组中的值的大小达到了2^28,存在数据量过大以及数据过于分散的问题,故排除使用数组;
- 然后我们需要计算所有和为0的情况,涉及重复数据的计算,所以需要额外的一个变量来记录有几个重复的值。
故,这一题使用map更合理一些,一个变量用来存储两两相加的结果,另一个变量用来存储这个结果出现的次数。
赎金信(383)
这一题只涉及26个字母,数据量不大,但是涉及到对重复数据的计数,故,可以使用数组和map两种数据结构。
1 |
|
三数之和(15)
这一题有两个需要注意的点:
- 降低时间复杂度。使用暴力枚举的话需要n^3的时间复杂度,修改思路是降成两重循环 + 查找需要的第三个数是否存在
- 去重。数组中是可能存在重复的数字的,所以需要考虑如何去除重复的数据。
①暴力方法:三重循环
(使用这种方法,就没有想过能通过力ToT。)
时间复杂度先不谈,使用这种方法主要考虑如何去重。 => 先对数组进行排序,然后在遍历时,如果遍历到的数据(除了当前循环的第一个数据)和前一个数据相同,就跳过处理。
②使用模板进行降重
由暴力方法,已经成功实现了去重的工作,接下来需要考虑的就是如何降低复杂度。 => 使用set和map进行处理。
在我实现的方法中,使用了双重循环 & map 方法。对于map中存储的第三个数需要注意这一个点:
- 第三个数的index是否是在前两个之后(之前已排序)。
针对这一个点,需要判定:
map中是否能够查询到需要的第三个数
map中该值的value值是否为0
- 如果第三个数和第二个数相同,value >1
用时有点高,仅击败5.01%….
在讲解的方法中,对更多的细节进行了判断,降低了时间。
- 因为数组已排序,如果第一个数大于0,则表明已经g了
- 使用了 set 方法,具体操作是
- 两层for循环遍历的是第一个数和第三个数,将第二个数据存在set中,进行遍历,若使用过则删除
- 需要考虑去除重复的三元组
- 这种方法效率也不是很高
③使用双指针(nb👈(゚ヮ゚👈))
题解推荐使用双指针的方法,将三重for循环降到了一重,并且需要考虑的条件也少了许多。具体如下:
- 首先,也是需要对数组进行排序操作。
- 遍历的是最左边的数(就是三元组中最小的数,序号为i),并且令left指针指向i+1,right指针指向nums.size() - 1。
- 如果三个数相加和>0,则对right指针进行操作;如果三个数相加小于0,则对left指针进行操作;直到left=right。
- 在这个过程中,也需要考虑指针指到的数值是否存在重复的情况。
四数之和(18)
处理n数之和的问题,解决方法大体是进行 n - 2 重 for 循环,然后最后两个数据使用双指针法进行遍历。
遇到的一些问题:
剪枝处理方法:(令第一个数为i,第二个数为j)
1
2
3
4
5
6
7
8
9// 第一重剪枝
// 确保nums[i] 以及 nums[i] + nums[j] 满足大于等于0是因为
// 如果是负数,会导致单个数大于target,但是最终相加结果可能等于target的情况
// ! 两个负数相加,可能会变得更小!!
if (nums[i] > target && nums[i] >= 0) break;
// 第二重剪枝
// 其中,nums[i] + nums[j] >= 0 的条件可以优化为 => nums[j] >= 0
if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;越界问题:
这题中有些用例的数值是超过了int的界限的,所以需要用long类型的数据接收四个数的和。
此处,需要注意类型转化的时机和方式:
1
2
3
4
5
6
7
8
9// 方式1
// 由于四个数为int类型,所以等式右边的求和类型为int类型的,然后将这个值转化为long类型的数据
// 显然,右边求和的结果存在越界的情况,需要注意一下
long sum = nums[i] + nums[j] + nums[left] + nums[right];
// 方式2
// 这里使用了显式类型转换 (long),将 nums[i] 转换为 long 类型
// 由于 nums[i] 已经被转换为 long 类型,后续的加法操作也会在 long 类型上进行,因此整个表达式的结果会被自动提升为 long 类型
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];