公众号:自学编程村,关注后,回复“书籍”,领取1000多本计算机书籍。涵盖语言语法、数据结构算法、AI相关书籍、开发相关等等各种书籍,应有尽有,你总会用到。回复“C语言”,领取原创C语言九讲PDF教程(数万字、白图)。回复学习路线,领取C++和Java学习路线和资源推荐总总结。
关注方式见主页或文章底部。想要加群可从公众号中加我wx~~~
那么从今天开始到未来的几个月的时间里,我将会为大家带来LeetCode中算法题的总结和思考。也算是对于自身算法学习的一种记录,同时分享给大家。我们先从LeetCode Hot100开始。
本次分享,主要带来的是LeetCode Hot100中的哈希表。由于哈希的内容比较简单,我们可以一次性就说完。
序
看完本节,你将收获:
1、LeetCode三道题的AC。
2、哈希表的一般使用思路和总结。
OK,我们废话少说,直接来看题。
下面是这三道题的原链接,大家可以看一下原题:
1、两数之和
2、字母异位词分组
3、最长连续序列
由于哈希表比较简单,所以我的行文思路就是先快速过一下这几个题,然后在做题的过程中,为大家分享出 我自己总结的规律。
1、两数之和
对于两数之和这道题,我们很容易想到的是两次循环遍历(即两个for循环),然后求得对应的两个数组的下标。
但是这么解肯定太low了。它对应的时间复杂度为O(N^2)。
我们就想能不能用空间换时间的方法,将其优化一下,把时间复杂度优化呢?这个时候,就用到了哈希表。为什么想到用哈希表呢?这里的思路可以再来拆分一下。
就是以样例为例的话,如下图,我想在数组中找到两个数等于target的数的时候,我想要记住某个数的同时,还要记住它的下标。比如下面是,我一开始用到了2,但是我并不确定2是不是最后target的组成成分,所以我不能确定0是否需要记录下来。
那这个时候,最简单的办法,就是建立起数组和下标的映射。那么建立起这种映射的方法,哈希表就可以(另外红黑树map也可以)。另一方面,之前说到过,我们最简单的办法是两层for循环。第一层for循环是用来遍历整个数组的,而第二层循环实际上是用来查找target - nums[i] 这个数在不在数组中(i表示第一层for循环从0到nums.size()的结果)。那通过遍历查找的时间复杂度为O(N),但是通过哈希表查找,就可以把O(N)变成O(1)了。这样以后,时间复杂度就从原本的O(N)*O(N) = O(N^2)变成了O(N)*O(1) = O(N)。
因此,总结来说,建立起哈希表可以有以下用途,换句话说,当我们想要用到以下需求功能的时候,可以想起来使用哈希表:
1、需要数据和下标建立起映射。
2、在数组中查找某个数,让其时间复杂度变成O(1)。
那按照刚刚那样分析,实际上本题就已经很简单了。我们直接给出代码和注释:
class Solution {
public:
vector<int> v;
vector<int> twoSum(vector<int>& nums, int target) {
// 建立起一个哈希表
unordered_map<int, int> map;
//这个for循环就是外部for循环,用于遍历整个数组
for(int i = 0; i < nums.size(); i++){
// 首先查找 target-nums[i] 这个元素在不在
auto a = map.find(target-nums[i]);
if(a != map.end()){ //如果在的话,那就可以直接返回结果了
return {a->second, i};
}
//否则就把这个数放进哈希表
map[nums[i]] = i;
}
return {0};
}
};
2、 字母异位词分组
对于这道题,也是一道比较简单的题。我们讲解的目的也是从简单的题中抽象出思路和规律。其实这道题也可以转化为和第一题类似的方法,因为字符串,我们就可以利用自己定义的哈希,将其转化为数字,类似于ascii码一样。通俗一点来说,举个例子,假如我字符串的长度就是一个字符,那我是不是可以直接把每个字母都减去一个'a',那这样的话,每个字母就可以和每个数字一一对应起来。那这道题就可以转化为“在数组中找到所有数字相等的数”。那照我们在第一题所说的(哈希表的两个用途),我们可以很快地想到使用哈希表来做。
那现在问题就是它不是一个字母,而是一个字符串。那我就想办法将每一个字符串都转换成一个独立的数字就好了。那怎么转换呢?我的做法是将字符串中的每个字母三次方。我试了一下,这样子是不会溢出的,并且可以AC。如果数据量更大的话可以继续调整。这个很灵活的,看大家自己的做法也行。
那么说到此,本题的做法也就水到渠成了。
两点:
1、使用自己的哈希函数,将每个字符串转化为一个独立的数字;
2、题目就转化为了在一个数组中找到相同的数字。使用哈希表即可。
class Solution {
public:
vector<vector<string>> vvs;
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<long, int> mp;
int k = 0;
for(int i = 0; i < strs.size(); i++){
//去将字符串数组转化为整型数组
long number = strs[i].size() * 100;
for(int j = 0; j < strs[i].length(); j++){
number += strs[i][j]*strs[i][j]*strs[i][j]*strs[i][j];
}
auto a = mp.find(number);
if( a != mp.end()){
//找到了
//那就需要将其放到对应的数组内
//cout << a->second << endl;
vvs[a->second].push_back(strs[i]);
continue;
}
//没找到
//那么就存储到哈希表内,将第k个新的数组作为值,哈希存储值作为key
mp[number] = k++;
vector<string> vp;
vp.push_back(strs[i]);
vvs.push_back(vp);
}
return vvs;
}
};
3、最长连续子序列
我们在乍一看到这道题的时候,看到它的时间复杂度要求为O(N),然后又要求去找序列(就是让你判断这个数的前面一个数在不在这个数组里,这个数的后面一个数在不在数组里)。按照我们平时暴力的做法(也是最先想到的做法),遍历一个O(N),然后判断相邻元素(就是小1的、小2的、小3的...)在不在又是O(N),然后有可能会有N个元素(因为最大长度是N,最多到小N的 ),这样N* N *N最坏的情况就都有可能是O(N^3)了。要我们找序列,又要我们O(N),这不扯淡么。
因为所谓O(N)的复杂度,就是要让你一遍过,或者你能说出来几遍过(也即常数个O(N),即O(kN),k为常数),所以,如果按照上面说的,要常数遍过就有些扯淡了。
这个时候,我们就可以想到用时间换空间的思想。并且,用哈希表的存储这样一个空间,来换遍历查找的时间,还是我们刚刚上面说到的哈希表作用的第二点。
但现在,问题又来了:
我是用哈希来去降了一次,但是我似乎遍历一遍数组需要O(N),但还需要去递归地去找 比每个元素小1的、小2的、小3的...小n的 在不在里面,这不是还是有个O(N)吗,然后合在一块不还是O(N^2)?
在第一次做这题的时候,我想着,前面的第一次O(N)是不可避免的(因为你总要遍历整个数组嘛)。但是后面的那个O(N),可不可以用unordered_map里的 <key, value>来去标记我每个元素有没有遍历过呢?如果说,我在某次找比某个元素小1的、小2的...小n的时候遍历过了,那么我在第一个遍历数组的那个O(N)循环里就不需要再去遍历它了,但是这就有可能造成新的问题,就是我怎么知道谁是最长的呢?比如对于样例:
[100, 4, 200, 1, 3, 2, 5]
(问题解释:)如果在外层第一层循环遍历到4的时候,我依靠哈希在内层找到了后面的1,2,3,并且标记它们已经被遍历过。那后面第一层循环再遍历到数组1,2,3的时候就直接跳过。但是如果我后面的5来了,我该怎么办呢?而且我这个时候4已经遍历过了。
如果大家能够明白我上面想表达的意思了,那么大家可以先不着急往下看,自己想一想该如何做。
其实做法有很多啦。在这里呢,就说两种。
1. 就是刚刚说的,是采用计数的方法。我们刚刚不是用了unordered_map嘛,那就可以在value中来去存储长度的相关信息。这样,在5来了之后,将它现在已有的长度加上4中的value中存储的长度,作为5的长度来使用,但是怎么样去实现,大家可以自己来思考。
2. 这个做法就更加简单粗暴了。就是如果这个数不是边界,就别理它,只有当它是边界的时候我们再去给它计数。什么意思呢?就是说,假如我们在内循环中,往比每个数小的那个方向去找(就是对于每个元素,我们统一找比它小1的、小2的、小3的...,而不是大1的,大2的,大3的...),那么如果我们能在该数组中找到有比它大1的元素存在,我们就直接跳过它(就是说对于那个元素而言,还会有比它小的元素),就不理它。直到我们在数组中不能够找到有比它大的元素存在(说明此时已经到达了属于它自己连续序列的右边界),说明它就是右边界了,这时我们才开始计数往比它小的方向找(即从它开始,在内层循环中一直找比它小1的、小2的、小3的...,一直找到左边界,然后输出长度)。最终,比较出一个最大的长度即可。
上面文字不知道会不会有些乱,如果觉得乱,可以只看红色的部分。
这里笔者就对第二种思路进行展开分析了哈。可以确立以下思路:
1)先把数组中所有的数都放到哈希表里面,直接放。
2)然后用几个 if 和 else 就搞定了。
伪代码:
if 比它大1:
if 它的比它大1没有在这数组里面(也就是不存在):
then 它是最后一个元素 从它开始计数;再开始判断比它小1的数是否存在
【
if 比它小1的数在这里面 :
then 继续往前找(找小2的、小3的...),并且找的那个元素的value值赋值false,这样在外层“对于每一个元素中”看到它就会直接跳过了。
else:
就不管,跳过
】
else if 比它大1的数在这里面:
then 它不是最后一个元素,别理它,直接跳过
最终代码:
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int Max = 0;
unordered_set<int> mp;
//建立哈希表
for(int i = 0;i < nums.size();i++){
mp.insert(nums[i]);
}
//遍历每一个元素
for(int i = 0;i < nums.size();i++){
if(mp.find(nums[i] + 1) == mp.end()){
//如果比它大一的数不存在了,就代表是右边界了,开始往比它小的方向找
int length = 1;
while(mp.find(nums[i] - length) != mp.end()){
length++;
}
Max = max(Max, length);
}
//否则就不是右边界,就继续找。
else{
continue;
}
}
return Max;
}
};
最后,可以来稍微总结一下,这道题实际上就是哈希的变形了哈。想到用哈希来用空间换时间,然后对于遍历,可以设置一个value值来去判断有没有遍历过,或者是一直找到一个边界再去遍历(这个稍稍随机应变一下应该就好)。然后这道题的思路也就出来了。
OK,以上就是对于这三道题的全部分享。总结一下就是,哈希表,这样一个空间换时间的做法,主要可以带来以下好处:
1、数组中的数据元素和数组下标建立起映射。
2、在数组中查找某个数,让其时间复杂度变成O(1)。
OK,本期分享到此结束,关注我,就可以第一时间看到下一期的分享喽。下一期要分享的是大名鼎鼎的双指针!!!
同时,也欢迎大家关注自学编程村公众号,为大家准备了一系列的资源在公众号上,回复私信就可以领取。