LeetCode Hot100实战之哈希表

公众号:自学编程村关注后,回复“书籍”,领取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,本期分享到此结束,关注我,就可以第一时间看到下一期的分享喽。下一期要分享的是大名鼎鼎的双指针!!!

同时,也欢迎大家关注自学编程村公众号,为大家准备了一系列的资源在公众号上,回复私信就可以领取。

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jxwd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值