Studying-代码随想录训练营day6| 哈希表理论基础、242.有效的字母异位词、349.两个数组的交集、202.快乐数、1.两数之和

第五天端午节休息😊,祝大家端午节快乐🎉,第六天(ง •_•)ง,编程语言:C++。

目录

哈希表理论基础

哈希表(别名:散列表,英文名:Hash table):哈希表能够通过关键码索引,直接访问元素。

哈希函数:

哈希碰撞:

常见的三种哈希结构: 

总结:

242.有效的字母异位词

349.两个数组的交集

202.快乐数

1.两数之和

总结:


哈希表理论基础

文档讲解:哈希表理论基础

哈希表(别名:散列表,英文名:Hash table):哈希表能够通过关键码索引,直接访问元素。

数组就是一张哈希表:哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素:

作用:哈希表能够快速判断一个元素是否出现集合里。例如要查询一个名字是否在这所学校里,如果采用枚举的话时间复杂度为O(1)。但如果采用哈希表,且关键码为名字,则只需要O(1)就可以做到。

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

哈希函数:

本质是将关键码通过特定的编码的方式(特定的哈希函数)转变为不同的数值。这就意味着可以将其他数据格式(关键码的数据格式)转化为统一的数值,比如将学生名字映射为哈希表上的索引数字。而后查找的时候再通过数值进行反函数找到对应的关键码。例子如下:

 

注:取模的操作是为了解决hashCode得到的数值大于哈希表的长度,对其进行哈希表长度的取模,能够保证学生姓名一定可以映射到哈希表上。

显然,这样一直将学生姓名插入下去,会出现两种情况:1.学生的数量大于哈希表的长度,无法再插入到空白位置;2.两个学生的姓名通过哈希函数得到的数值相同,映射到了同一个下标。

前者还可以通过增加哈希表容量的方式进行解决,但后者就得需要修改哈希函数,才能使得两个姓名得到的数值不同,但总体来说就算哈希函数计算的再均匀,也难以避免会存在几位学生的名字同时映射到哈希表同一个索引下标的位置。这个现象也被成为哈希碰撞。

哈希碰撞:

哈希碰撞解决办法:1.拉链法;2.线性探测法。

1.拉链法:

将哈希表内的元素通过链表的方式存储,当发生冲突时,将冲突的元素都存储在链表里。这种方法要注意选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。

2.线性探测法:

该方法的方式,是在发生哈希碰撞时,寻找后面的空位放置冲突的元素。因此采用这种方式一定要保证哈希表的长度大于数据的个数,否则哈希表上将没有空的位置存放冲突的数据。

 

常见的三种哈希结构: 

哈希表一般存在三种数据结构:数组、set(集合)、map (映射)。

1.数组:

数组就是一张哈希表,它的关键码固定为int型,即下标。能够通过下标直接查找元素。

2.set:

unordered_set底层实现即为哈希表,采用哈希映射存放数据,因此是无序的。set和multiset的底层实现是红黑树,红黑树是一种平衡二叉搜索树,每次插入元素的时候都会进行排序,因此key是有序的,且不可以修改。

3.map:

unordered_map底层实现为哈希表,map和multimap的底层实现是红黑树。同理,map和multimap的key也是有序的。

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

而map是一个key value的数据结构。在map中,对key是有限制的,对value是没有限制的。也就是map和multimap中key是通过红黑树进行存储的,unordered_map中也是将key作为关键码,三者在查找时都是对key进行查找。

总结:

1.当我们遇到要快速判断一格元素是否出现在集合里时,可以考虑是否能够使用哈希法。

2.当要判断一格元素是否在别的地方出现过,也可以考虑是否能够使用哈希法。

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


242.有效的字母异位词

文档讲解:代码随想录有效的字母异位词

视频讲解:手撕有效的字母异位词

题目:

初看:第一想法是,使用两个数组存储“s”和“t”内的元素,然后遍历进行比较。两个数组的容量设计为26,下标0-25依次对应26个字母a-z。

学习:通过视频学习,发现实际上采用一个数组sum就可以完成。数组其实就是一个简单的哈希表,在遍历字符串s的时候,可以通过将s[ i ] - 'a'的方式(其实就是一个哈希函数),计算出每个字母的下标。采用这种方式不需要记住字符a的ASCII码。之后在遍历字符串t时,只需要将t中出现的字符进行-1操作即可。最后遍历数组sum,只要里面存在不等于0的元素,则说明两个字符串存在数量不同的字母,即不是字母异位词,return false。反之全为0,return true。

代码:

//时间复杂度:O(n)
//空间复杂度:O(26)--O(1)
class Solution {
public:
    bool isAnagram(string s, string t) {
        int sum[26] = {0};
        for (char c : s) {
            sum[c - 'a']++;
        }
        for (char c : t) {
            sum[c - 'a']--;
        }
        for (int i = 0; i < 26; i++) {
            if(sum[i] != 0) return false;
        }
        return true;
    }
};

收获:

  1. 数组就是一个特殊的哈希表,其关键码为下标。当元素个数较少且数量一定时,即可采取数组作为哈希表。
  2. 字符之间的加减,实际上是将字符转化为ASCII码后进行计算。 

349.两个数组的交集

文档讲解:代码随想录两个数组的交集

视频讲解:手撕两个数组的交集

题目:

初看:第一想法是通过遍历两个数组,当存在相同数时则将其加入答案数组中。但要注意答案数组中需要去重。时间复杂度为O(n*m),n,m分别为两个数组的长度。

学习:1.去重可以通过set、或者unordered_set这两个类内部自行去重,使用unordered_set的读写效率最高,且本题不需要输出元素有序,因此采用unordered_set。2.不需要反复遍历两个数组, 将一个数组中的元素放入到一个unordered_set<int>sum中,之后遍历另一个数组的元素,通过关键码查询哈希表内是否含有相同的元素,若有,则加入到答案数组中,时间复杂度降为O(n+m)。注意:最后需要返回一个vector类,要注意改变数据类型。

代码:

//时间复杂度O(n+m)
//空间复杂度O(n)
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> ans;
        unordered_set<int> sum(nums1.begin(),nums1.end());
        for (int c : nums2) {
            if (sum.count(c) == 1) {
                ans.insert(c);
            }
        }
        vector<int>anss(ans.begin(), ans.end());
        return anss;
        //return vector<int>(result_set.begin(),result_set.end());
    }
};

收获:

  1. 本题实际上也可以采用数组来构建哈希表,因为给的两个数组nums1和nums2内的元素大小在1-1000之间,数量较少且一定,可以通过设置一个数组容量为1000的int型数组存储各元素。但当数量继续增加时,如果给定的nums数组中元素比较少,且特别分散,就可能会造成数组空间的极大浪费。
  2. (nums.begin(), nums.end())在stl中大部分类都具备这种初始化方式,要学会熟练使用,但要注意使用的时候,注意赋值对象类型要一致。

202.快乐数

文档讲解:代码随想录快乐数

题目:

初看:第一想法是通过一个while循环,不断更新n的数值,同时每次更新后要判断当前数值是否为1,如果为1则是快乐数,返回true。如果不为1则判断之前是否出现过同样的数,如果出现过,则说明进入了循环,不是快乐数,返回false。两个判断都不成立则进入新一轮循环。

学习:可以通过创建一个哈希表来存储循环过程中出现的数值,之后判断之前是否出现过同样的数,就可以通过查询哈希表内是否有相同数值即可。

代码:

//时间复杂度O(logn)
//空间复杂度O(logn)
class Solution {
public:
    bool isHappy(int n) {
        unordered_set<int> ans;

    while(true) {
        int sum = 0;
        while (n) {
            sum += (n % 10) * (n % 10);
            n /= 10;
        }
        if (sum == 1) return true;        
        if (ans.count(sum) == 1) return false;
        ans.insert(sum);
        n = sum;
    }
    }
};

其他方法:学习到另一种双指针的方法,也可以找到循环点,且不需要创建哈希表。就是构造一对快慢指针,快指针每次进行两轮循环,慢指针进行一轮循环。类似于之前链表中找环的方式,如果存在环的话,快指针会先进入循环队列,慢指针后进入循环队列,由于快指针相对慢指针只多走了一步,因此一定会追上慢指针,此时可通过判断慢指针的数值,来判断是否是快乐数。

代码:

//时间复杂度O(logn)
//空间复杂度O(1)
class Solution {
public:
    int bitSquareSum(int n) {
        int sum = 0;
        while(n > 0)
        {
            int bit = n % 10;
            sum += bit * bit;
            n = n / 10;
        }
        return sum;
    }
    
    bool isHappy(int n) {
        int slow = n, fast = n;
        do{
            slow = bitSquareSum(slow);
            fast = bitSquareSum(fast);
            fast = bitSquareSum(fast);
        }while(slow != fast);
        
        return slow == 1;
    }
};

1.两数之和

文档讲解:代码随想录两数之和

视频讲解:手撕两数之和

题目:

初看:第一想法是通过双循环遍历数组,找到适合的两两组合。这种方式时间复杂度为O(n^2)。

学习:通过之前的题发现,其实这也是一个寻找对应元素是否存在的问题,即target=9时,第一个元素为7,则寻找其他元素中是否含有(9-7)的元素,如果有则返回对应的下标。本题要注意不能一开始就把所有元素放入哈希表,这样会导致查询时存在相同数错误,例如target=8时,第一个元素为4,如果一开始就把所有元素放入哈希表,会导致查询到自己,返回两个相同下标。

代码:

//时间复杂度O(n)
//空间复杂度O(n)
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> ans;
        for(int i = 0; i < nums.size(); i++) {
            auto it = ans.find(target - nums[i]);
            if (it != ans.end()) return {it->second, i};
            ans.insert(pair<int, int>(nums[i], i));
        }
        return {};
    }
};

收获:

  1. 本题采用哈希表的原因是因为,它每次循环本质是以一个元素为基点,寻找其对应的元素是否存在,而这种情形,就适合使用哈希表来解决。
  2. 本题采用map是因为,本题需要返回的是下标,而不是元素值。但是我们在查找的时候,需要查找的是元素值 。因此需要建立一对键值对,将元素值与下标对应起来,其中元素值为关键码key,下标为value。

总结:

第六天💪,哈希表还需要多练习,一开始在map的存储上出了错误,要牢记key和value的关系。

  • 45
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值