day6 | 242.有效的字母异位词、 349. 两个数组的交集、202. 快乐数、 1. 两数之和

目录:

链接

题目链接:

https://leetcode.cn/problems/valid-anagram/

https://leetcode.cn/problems/intersection-of-two-arrays/

哈希表基础

参考:https://programmercarl.com/哈希表理论基础.html

哈希表是根据关键码的值而直接进行访问的数据结构。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FUtMeIfG-1685441418954)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f8474c84-4a3c-4139-ab89-fd8685ccf5ee/Untitled.png)]

一般哈希表都是用来快速判断一个元素是否出现集合里。

例如要查询一个名字是否在这所学校里。

要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。

我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。

将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数

****哈希函数:****哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。

****哈希碰撞:****如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wmwtptHG-1685441418955)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7dea0bc5-1fe4-461d-af23-ba4626d68ecd/Untitled.png)]

一般哈希碰撞有两种解决方法, 拉链法和线性探测法

常见的三种哈希结构:

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map(映射)

set

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JyfrsBUf-1685441418958)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0e246ef6-6525-40f5-aab9-822153e6612a/Untitled.png)]

std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。

总结

总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。

如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!

大体判断:数值有限可控用数组,如果数值很大,用set,如果key对应value的话,就用map。

如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费!

解题及思路学习

242. 有效的字母异位词

给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的字母异位词。

**注意:**若 st 中每个字符出现的次数都相同,则称 st 互为字母异位词。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DWOnQfSo-1685441418959)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8f2eeaad-763e-4a1f-a657-43d8dd679a66/Untitled.png)]

自己思路:用一个哈希表,存放s中每个字母出现的次数。之后用另一个哈希表统计t中字母出现的次数,若相等,则为true。

代码随想录思路:由于字母a到z,长度有限且连续,可以用数组当作哈希表。记录每一个字母出现的次数。之后遍历另一个字符串的时候,直接做减法(比我想的少一个哈希表)。最后判断是否都为0.

定义一个数组叫做record用来上记录字符串s里字符出现的次数。

需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。

再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。

那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。

那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。

最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,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;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

如果用哈希表,先考虑用数组,因为这是最简单的结构。

s[i]-‘a’ 这种相对数值计算很妙!

不是想着设置两个表,而是直接做减法。这样的思路更简单,节省内存。

349. 两个数组的交集

给定两个数组 nums1nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kdT0h27f-1685441418961)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8208912c-c9a6-4999-9e05-6b605d391134/Untitled.png)]

自己思路:找到两个数组交集,也就是重复的某些元素。因为输出元素是唯一的,所以需要对数组中的元素进行去重操作。去重之后将一个数组中的所有元素放入哈希表。set了解到不深,没相关思路,但是map可以做。直接看思路吧。

代码随想录:这道题目,主要要学会使用一种哈希数据结构:unordered_set,这个数据结构可以解决很多类似的问题。

注意题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序

如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。

C++ 给提供了如下三种可用的数据结构:

  • std::set
  • std::multiset
  • std::unordered_set

std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。

思路如图所示:

(用unordered_set 建立集合并去重,之后再比较结果。)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mnjg0hSz-1685441418961)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3109ba72-e34b-4ea7-9a86-2fdcd97022c4/Untitled.png)]

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
        unordered_set<int> nums_set(nums1.begin(), nums1.end());
        for (int num : nums2) {
            // 发现nums2的元素 在nums_set里又出现过
            if (nums_set.find(num) != nums_set.end()) {
                result_set.insert(num);
            }
        }
        return vector<int>(result_set.begin(), result_set.end());
    }
};
  • 时间复杂度: O(mn)
  • 空间复杂度: O(n)

后记

本题后面 力扣改了 题目描述 和 后台测试数据,增添了 数值范围:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 1000

所以就可以 使用数组来做哈希表了, 因为数组都是 1000以内的。

对应C++代码如下:

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
        int hash[1005] = {0}; // 默认数值为0
        for (int num : nums1) { // nums1中出现的字母在hash数组中做记录
            hash[num] = 1;
        }
        for (int num : nums2) { // nums2中出现话,result记录
            if (hash[num] == 1) {
                result_set.insert(num);
            }
        }
        return vector<int>(result_set.begin(), result_set.end());
    }
};
  • 时间复杂度: O(m + n)
  • 空间复杂度: O(n)

for (int num : nums2) 这种写法很有意思。也是遍历,但是是一种更加简洁的写法。

if (nums_set.find(num) != nums_set.end()) 表示判断ums2的元素 在nums_set里有出现过 ,为什么是不等号?

解释:

在这段代码中,nums_set.find(num)是用来查找nums2中的元素num是否在nums_set中出现过。如果num存在于nums_set中,find()函数返回一个指向该元素的迭代器,指向该元素的位置;否则,返回的迭代器等于nums_set.end(),表示未找到该元素。

因此,判断**nums2的元素是否在nums_set中出现过,可以通过比较nums_set.find(num)的结果是否等于nums_set.end()来实现。如果不等于nums_set.end(),说明找到了该元素,表示该元素在nums_set中出现过;如果等于nums_set.end(),说明未找到该元素,表示该元素在nums_set**中没有出现过。

202. 快乐数

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n快乐数 就返回 true ;不是,则返回 false

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0vHQSKUQ-1685441418962)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/22c13d00-7f84-46f8-9c44-c84e334405de/Untitled.png)]

自己思路:这个正整数n可能很大,先要想办法获取到每个位置的数字。可以先将整型数字n变为字符串,之后分别获取每个位置上面的数字。分别求取每个位置上数字的平方和。判断是否等于1。什么时候循环停止呢?

十分钟到了,没想出好的思路,直接看代码随想录:用 % 操作重复获取n的最后一位数并进行平方操作,最后求和。将求和的这个封装成一个函数。

用哈希表判断是否重复出现了, 如果重复出现,则该数不是。

class Solution {
public:
    // 取数值各个位上的单数之和
    int Sum(int n){
        int sum = 0;
        while(n){
            sum += (n % 10) * (n % 10);
            n = n/10;
        }
        return sum;
    }

    bool isHappy(int n) {
        unordered_set<int> result_set;
        while(1){
            int sum = Sum(n);
            if (sum == 1){
                return true;
            } 
            //如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
            if(result_set.find(sum) != result_set.end()){
                return false;
            }
            else{
                result_set.insert(sum);
            }
            n = sum;
        }
    }
};
  • 时间复杂度: O(logn)
  • 空间复杂度: O(logn)

时间复杂度和空间复杂度不太看得懂。

result_set.find(sum) != result_set.end() 这个表示sum在set钟出现过,自己写的时候还是没习惯这种写法。多看几遍加深印象。

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

自己思考:只要找到满足条件的一组整数就行。题目相当于有个不能重复的要求(数组中同一个元素在答案里不能重复出现,比如目标值为6,那么不能两次用同一个3)。数组长度为10000以内,那好像也可以用数组,但是有点消耗内存了。用unordered_map会自动有个去重的操作,不仅需要记录数字值,还需要记录下标,也不太合适。所以想到了map操作。用map分别记录数组中的数和下标。遍历nums,分别将其中的数值添加进map的中,并比较是否有相加等于9的数。找到之后返回两个数的下标。

代码随想录:总体而言,思路差不多。

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 {};
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

需要注意的点:

1、什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。

2、本题我们不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适

使用数组和set来做哈希法的局限。

  • 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
  • set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lsh4JbO8-1685441418963)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/05b16b5b-3fd7-4500-8d91-a3d90bf5198b/Untitled.png)]

这道题目中并不需要key有序,选择std::unordered_map 效率更高!

困难及收获

困难

1、几种哈希表的数字处理

2、C++语法不熟

今日收获

1、几种哈希表:数组、set和map的各自使用场景。以及C++定义

2、result_set.find(sum) != result_set.end() 查找某个元素是否出现过,ture表示出现。

3、map的操作,iter->second 取第二个值,插入: map.insert(pair<int, int>(nums[i], i));``

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值