哈希表:leetcode 242.有效的字母异位词、349.两个数组的交集、202.快乐数、1.两数之和

leetcode 242.有效的字母异位词

leetcode 349.两个数组的交集

leetcode 202.快乐数

leetcode 1.两数之和

代码随想录算法公开课

哈希表基础

以下图片和理论来源于代码随想录-哈希表基础

What is hash table?

哈希表是根据关键码的值而直接进行访问的数据结构。直白来讲哈希表就是数组,可以通过下标直接访问数组之中的元素。

What problem can hash table solve?

一般哈希表都是用来快速判断一个元素是否出现集合里。如果我们需要查询一个元素是否存在于一个大集合中,使用枚举法的时间复杂度为O(n),而用哈希表的话时间复杂度为O(1)。将元素映射到哈希表上就涉及到hash function,也就是哈希函数

哈希函数

哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把各个元素映射为哈希表上的索引数字了。

如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?

此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作(hashCode(name) % tableSize),就要我们就保证了各个元素一定可以映射到哈希表上了。

那么如果元素的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几个元素同时映射到哈希表同一个索引下标的位置,这样就无法实现直接查找。此时就变成一个哈希碰撞问题。

哈希碰撞

如下图所示,两个元素同时映射到索引下标的为1的地方,造成哈希碰撞(Collisions)。

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

拉链法

刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了

(数据规模是dataSize, 哈希表的大小为tableSize)

其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。

线性探测法

使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。

当某一元素通过哈希函数映射到的位置已无空位,造成哈希碰撞,那么就向下找一个空位放置其信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。

常见的三种哈希结构

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

  • 数组

  • set(集合)

  • map(映射)

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

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

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

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

在map 中是一个key value 的数据结构,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。

那么应该怎么选择哈希数据结构呢?

大体上的判断准则是,如果范围可控,用数组就可以了;如果范围很大,就用set;如果数据存在key和value的对应,就用map。

总结

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

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

leetcode 242.有效的字母异位词

暴力排序

代码实现

class Solution {
public:
    bool isAnagram(string s, string t) {
        if (s.length() != t.length()) {
            return false;
        }
        sort(s.begin(), s.end());
        sort(t.begin(), t.end());
        return s == t;
    }
};
  • 时间复杂度O(nlogn)

  • 空间复杂度O(logn)

细节处理

  1. 先判断两个字符串长度是否相同,如果不相同不可能成为异位词。

  1. 使用sort()函数对字符串进行排序,再判断两个字符串是否完全相同。

  1. 时间复杂度:O(nlogn),其中 n 为 s 的长度。排序的时间复杂度为 O(nlogn),比较两个字符串是否相等时间复杂度为 O(n),因此总体时间复杂度为 O(nlogn +n) = O(nlogn)。

  1. 空间复杂度:O(logn),排序需要O(logn) 的空间复杂度。

哈希数组

代码实现

class Solution {
public:
    bool isAnagram(string s, string t) {
        int record[26] = {0};
        for(int i = 0; i < s.size(); i++){
            record[s[i] - 'a']++;
        }
        for(int j = 0; j < t.size(); j++){
            record[t[j] - 'a']--;
        }
        for(int k = 0; k < 26; k++){
            if(record[k] != 0)
                return false;
        }
        return true;
    }
};
  • 时间复杂度O(n)

  • 空间复杂度O(1)

细节处理

  1. 定义一个数组叫做record,大小为26 ,初始化为0。因为字符a到字符z的ASCII也是26个连续的数值。

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

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

  1. 同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。

leetcode 349.两个数组的交集

哈希set

代码实现

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result;
        unordered_set<int> nums(nums1.begin(), nums1.end());
        for(int num: nums2){
            if(nums.find(num) != nums.end()){
                result.insert(num);
            }
        }
        return vector<int>(result.begin(), result.end());
    }
};
  • 时间复杂度O(n+m)

  • 空间复杂度O(n)

细节处理

  1. 使用数组来做哈希的题目,是因为题目都限制了数值的大小。而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。

  1. 本题使用set中的unordered_set,因为其底层实现是哈希表。使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且它还可以自动对数据进行去重操作。set中的multiset则允许重复数据的出现。

  1. 凡是遇到哈希问题都用set并不是一个好的选择,直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。在数据量大的情况,耗时差距是很明显的。

  1. 时间复杂度的计算:设n和m分别为nums1和nums2的数组长度,使一个unordered_set储存num1中的元素需要O(n),遍历nums2需要O(m),所以时间复杂度为O(m+n)。

排序+双指针法

代码实现

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        sort(nums1.begin(), nums1.end());
        sort(nums2.begin(), nums2.end());
        int length1 = nums1.size(), length2 = nums2.size();
        int index1 = 0, index2 = 0;
        vector<int> result;
        while(index1 < length1 && index2 < length2){
            int num1 = nums1[index1], num2 = nums2[index2];
            if(num1 == num2){
                if(!result.size() || num1 != result.back()){
                    result.push_back(num1);
                }
                index1++;
                index2++;
            }
            else if(num1 < num2){
                index1++;
            }
            else{
                index2++;
            }
        }
        return result;
    }
};
  • 时间复杂度O(nlogn+mlogm)

  • 空间复杂度O(logm+logn)

细节处理

  1. 本题的重点主要在于取交集并去重。在不使用unordered_set的情况下去重,可以考虑从数组的大小排序方面入手,当数组呈顺序排列时(如从小到大时),此时将新元素加入result数组,如果与result数组的前一个元素相同的话,就可认为出现了重复元素,那么此时舍弃这个新加入的元素即可实现去重操作。代码中使用了if(num1 != result.back())语句判断是否出现重复元素。

  1. 双指针思路,定义两个指针分别指向经过排序后的数组的第一位,随后依次向后移动,比较两个数组对应的索引的值,发现值相同则将该值push_back进result数组,再根据1中的判断条件看是否出现重复元素。

  1. 时间复杂度的分析:m和n分别是两个数组的长度。对两个数组排序的时间复杂度分别是O(mlogm)和O(nlogn),双指针寻找交集元素的时间复杂度是O(m+n),因此时间复杂度O(mlogm+nlogn)。

  1. 空间复杂度的分析:m 和 n 分别是两个数组的长度。空间复杂度主要取决于排序使用的额外空间,可见上文sort()的空间复杂度分析。

leetcode 202.快乐数

代码实现

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;
            if(set.find(sum) != set.end()){
                return false;
            }
            else{
                set.insert(sum);
            }
            n = sum;
        }
    }
};

细节处理

  1. 题目中说存在无限循环,那么说明,一旦发生无限循环,这个数就不是快乐数。

  1. 需要用到多次求和的值进行对比,如果存在与之前求和的值相等的情况,那么就进入了无限循环。

  1. 额外定义一个getSum()求和函数,方便在主函数中进行计算。

leetcode 1.两数之和

哈希map

代码实现

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

  • 空间复杂度O(n)

细节处理

  1. 本题我们需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是是否出现在这个集合。那么就应该想到使用哈希法了。

  1. 本题我们不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。这道题目中并不需要key有序,选择std::unordered_map 效率更高。

总结

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值