算法训练营 第六天

算法训练营 第六天

解锁新章节 哈希表day01

哈希表理论基础

1、哈希表的妙用

哈希表主要是能够实现快速查找,查找复杂度为o(1),和数组的查找复杂度一样,那为什么不用数组,还需要哈希表呢,

  1. 增删操作,哈希更快

因为数组每次增删数据,都需要重新在内存中查找合适的内存空间,然后搬过去,但哈希表就不用,哈希表类似在内存中开辟了一块空间,足够用一段时间,等到不够用了,或者产生哈希碰撞了,就在碰撞的地方生成链表存储(拉链法),或者向下查找有没有空位,给他放进去(线性探测法),

  1. 哈希表适用于根据部分信息查找的情况

在实际应用中经常只知道部分信息,用部分的信息查找全部的信息,比如已知用户id,要查找用户的所有信息,数组只能通过全部信息来查找,用部分信息来查找,它的时间复杂度为o(n),而哈希表是key-value的键值形式,将用户id设为key时,通过特定的哈希函数将key转换为一个哈希值,存储在哈希表相应的位置,因此它的查找复杂度为o(1)。

2、哈希表怎么实现时间复杂度为o(1)的

当知道数组的下标时,查找该下标对应的值,时间复杂度是o(1)。哈希表也类似,key可以通过哈希函数,转化为一个哈希值,这个哈希值基本都是一个int类型的数(如果不对,还请大佬指正),也就是起到数组下标的作用,这样在已知key时,就能直接查找到下标对应的值了。因此哈希表和数组一样,是一段连续的空间,注意当发生哈希碰撞,用链表来延申的时候,那段空间不连续,同时查找时间复杂度也不是o(1)了。

242.有效的字母异位词

这道题我的思路是:
1、按需建立哈希表,如果哈希表里有字符a,就hash[a]++,如果没有,就插入键值对(a,1)

for (auto a : s) {
            if (hash.find(a) != hash.end())
                hash[a]++;
            else
                hash.insert(make_pair(a, 1));
        }

2、然后处理字符串t,遍历字符串t,如果哈希表里没有字符a,则返回false,如果有就hash[a]–。如果hash[a]<0了就返回false。这里就有小伙伴要疑惑了,那如果字符串s里有两个字符b,字符串t里有一个字符b,那hash[‘b’]==1,不会小于0,这种应该怎么办呢,这个时候,我们需要在函数最开始判断一下,字符串s和字符串t的大小是否相等,如果字符串大小相等,且字符串t里不存在字符串s里没有的元素,这样的话,如果s和t不是字母异位词,那就一定会有字符hash[a]<0。

for (auto a : t) {
            //如果t里存在s没有的字符,则直接返回false
            if (hash.find(a) == hash.end())
                return false;
            hash[a]--;
            if (hash[a] < 0)
                return false;
        }

源码:

class Solution {
public:
    bool isAnagram(string s, string t) {
        unordered_map<char, int> hash;
        if (s.size() != t.size())
            return false;
        for (auto a : s) {
            if (hash.find(a) != hash.end())
                hash[a]++;
            else
                hash.insert(make_pair(a, 1));
        }
        for (auto a : t) {
            //如果t里存在s没有的字符,则直接返回false
            if (hash.find(a) == hash.end())
                return false;
            hash[a]--;
            if (hash[a] < 0)
                return false;
        }
        return true;
        

    }
};

参考答案(代码随想录)

class Solution {
public:
    bool isAnagram(string s, string t) {
        int record[26] = {0};
        for (int i = 0; i < s.size(); i++) {
            // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
            record[s[i] - 'a']++;
        }
        for (int i = 0; i < t.size(); i++) {
            record[t[i] - 'a']--;
        }
        for (int i = 0; i < 26; i++) {
            if (record[i] != 0) {
                // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
                return false;
            }
        }
        // record数组所有元素都为零0,说明字符串s和t是字母异位词
        return true;
    }
};

代码随想录中的参考答案没有使用哈希表,而是直接创建了空间大小为26的数组,然后用a-z来表示数组的下标。按理说我是按需创建哈希,内存使用会更小,且不用结尾再遍历一遍数组,时间复杂度也会更小,但是结果显示内存占用会更多,我猜是因为哈希表创建时,就已经分配了一些内存,这个内存比26个大小更大,因此内存占用更大。

349. 两个数组的交集

这道题首先想到用哈希,我用的unorder_map,第一个值是int,第二个值(value值)设置为布尔值。

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_map<int, bool>hash;
        vector<int> res;
        for (auto a : nums1) {
            if (hash.find(a) == hash.end())
                hash.insert(make_pair(a, true));
        }
        for (auto a : nums2){
            if (hash.find(a) != hash.end() && hash[a] == true) {
                res.push_back(a);
                hash[a] = false;
            }
        }
        return res;
    }
};

代码随想录中的参考答案用的是unorder_set,用set确实合理,但这样的话就没法保证结果数组的去重,因此它结果数组也是用的unorder_set,实现了去重的操作,但是内存占用会相对更多一些。
时间复杂度和空间复杂度上,我的答案都更优一些。

202. 快乐数

这道题的思路比较明晰,如果这个数不是快乐数,那可能显然死循环,也就是有些数会重复出现,因此用unorder_set记录之前出现过的数,如果中间计算的数有重复出现的,则该数一定不是快乐数。
然后如何得到每个位置上的数字的平方和,也是这道题的一个重点
我的源码

class Solution {
public:
    bool isHappy(int n) {
        unordered_set<int> hash_set;
        while (true) {
            int tmp = 0;
            while (n) {
                tmp += (n % 10) * (n % 10);
                n /= 10;
            }
            n = tmp;
            if (n == 1)
                return true;
            if (hash_set.find(n) != hash_set.end())
                return false;
            hash_set.insert(n);
        }
    }
};

参考答案

class Solution {
public:
    // 取数值各个位上的单数之和
    int getSum(int n) {
        int sum = 0;
        while (n) {
            sum += (n % 10) * (n % 10);
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        unordered_set<int> set;
        while(1) {
            int sum = getSum(n);
            if (sum == 1) {
                return true;
            }
            // 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
            if (set.find(sum) != set.end()) {
                return false;
            } else {
                set.insert(sum);
            }
            n = sum;
        }
    }
};

1. 两数之和

经典两数之和,经典到我看到他的第一个想法就是暴力解法,就是两层for循环遍历。遍历就是为了先确定一个数,然后再判断另一个数在不在数组里,这,不就是经典哈希判断元素是否存在的问题吗?因此,用哈希!
因为要考虑重复元素的问题,所以我们用multimap(比如题目给的数组为{3,3},那么这两个数都要存到表里,需要用可以重复键的结构),key里存数组里的值,value存数组下标。将数组遍历存进multimap里,然后再遍历哈希表判断。
这里重复值的处理有点别扭,以输入:vector = {3,3} value = 6为例,multimap存储内容为{[3,0],[3,1]},用迭代器遍历,iter->first ==3,分两种情况
1、value-iter->first==iter->first时(即6-3=3)
那么先判断3在map里有几个,用hash.count(),如果只有一个,那个continue;如果不止一个,则返回iter->second和(++iter)->second(因为multimap是有顺序的,所以下一个3一定在iter的后一个)
2、value-iter->first!=iter->first时
那么正常查找value-iter->first在不在map里
源码

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        multimap<int, int>hash;
        for (int i = 0; i < nums.size(); i++) {
            hash.insert(make_pair(nums[i],  i ));
        }
        for (auto iter = hash.begin(); iter != hash.end(); iter++) {
            int tmp = target - iter->first;
            if (tmp == iter->first) {
                if (hash.count(tmp) != 1) {
                    return{ iter->second, (++iter)->second };
                }
                    
                else
                    continue;
            } 
            if (hash.find(tmp) != hash.end())
                return {iter->second, (hash.find(tmp))->second};

        }
        return { 0,0 };
    }
};

这道题参考答案的方法更优,它最开始把数组存到map的过程中,边存边查。还是以{3,3},6为例,先把第一个3存进去,然后存第二个3时先判断value-vec[1]这个值是否在map里,在的话就直接返回了,不在,再把第二个3存进去,这样就解决了我之前碰到的因为有重复值而不得不使用multimap的问题,而是可以用unordered_map啦,unordered_map的底层实现是哈希表,查找效率是o(1),multimap的底层实现是红黑树,查找效率是o(logn),所以能用unordered_map就用unordered_map。

参考答案

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        std::unordered_map <int,int> map;
        for(int i = 0; i < nums.size(); i++) {
            // 遍历当前元素,并在map中寻找是否有匹配的key
            auto iter = map.find(target - nums[i]); 
            if(iter != map.end()) {
                return {iter->second, i};
            }
            // 如果没找到匹配对,就把访问过的元素和下标加入到map中
            map.insert(pair<int, int>(nums[i], i)); 
        }
        return {};
    }
};

结束!今天花的时间最长的就是这个两数之和,没想到用边存边查的方法来避免重复值的情况。今天花了大概3个小时吧,但是因为上午拉肚子老跑厕所,没有在上午弄完,时间他不等人啊呜呜,明天继续加油

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值