算法训练营 Day5 哈希表 1 | 242.有效的字母异位词 | 349.两个数组的交集 | 202.快乐数 | 1.两数之和

62 阅读9分钟

算法训练营 Day5 哈希表 1 | 242.有效的字母异位词 | 349.两个数组的交集 | 202.快乐数 | 1.两数之和

查阅文档地址:programmercarl.com/

本期题目地址:

  1. 242.有效的字母异位词 - 简单 - 力扣
  2. 349. 两个数组的交集
  3. 第 202 题。快乐数 - 简单 - 力扣
  4. 1. 两数之和 - 简单 -力扣

本期题目答案地址:

  1. 力扣官答 - 链接

本期同类题目地址:

  1. 383.赎金信 - 简单 - 力扣
  2. 49.字母异位词分组 - 中等 - 力扣链接
  3. 438.找到字符串中所有字母异位分词 - 中等 - 力扣
  4. 350. 两个数组的交集 II - 简单 - 力扣

目录:

  1. 基本概念(做题前要理解的概念)
  2. 我的解法
  3. 疑问点(过程中产生了问题并且查找资料解决)

语言

采用C++,一些分析也是用于 C++,请注意。

基本概念

  1. 哈希表(hash table),国外也叫散列表。一般哈希表是用来快速判断一个元素是否出现在集合里面。
  2. 哈希函数(hash function),哈希函数使得哈希表的索引和值有一一对应的映射关系;可以通过查询索引下标快速查值,类似数组。
  3. 哈希表内部实现原理:哈希函数通过 hashCode 把名字通过特定的编码方式转化为数值。如果 hashCode 得到的值大于哈希表的大小,内部会进行一个取模操作。难以避免映射到同一个索引下标,发生哈希碰撞
  4. 哈希碰撞:解决方法有两种,一是拉链法,二是线性探测法。拉链法:以一种链式形式让发生哈希冲突的元素存储再同一个索引下标;线性探测法:使用其他空余的索引下标来存放,所以哈希表的大小一定要足够大。
  5. 常见哈希结构 - 数组、set、map 的区别:
    • 数组
    • set(集合):类似于数学概念中的集合。
    • map(映射):类似于 python 中的 map 字典的概念。
集合底层实现查询效率增删效率
std::set红黑树O(log n)O(log n)
std::multiset红黑树O(log n)O(log n)
std::unordered_set哈希表O(1)O(1)
  • 通过命名/底层实现结构可以区分是否有序、受否可以重复 的性质;查询效率和增删效率和底层实现结构有关。
  • 红黑树是一种平衡二叉搜索树,key 值是有序的,key 不可以修改,所以只能删除和增加。
集合底层实现查询效率增删效率
std::map红黑树O(log n)O(log n)
std::multimap红黑树O(log n)O(log n)
std::unordered_map哈希表O(1)O(1)
  • 通过命名/底层实现容器可以区分是否有序、受否可以重复 的性质;查询效率和增删效率和底层实现结构有关。
  • 这些都属于 C++ 标准库。

242.有效的字母异位词

242.有效的字母异位词 - 简单 - 力扣

法一:字符串可以使用 sort 函数进行排序再次比较。

法二:因为只包含 26 个小写字母,开长度为 26 的动态数组容器(模拟哈希表)可以存下所有的计数结果。

我的代码

// 使用 map 容器
// 时间复杂度:O(n+m)
// 空间复杂度:O(n)
class Solution {
public:
    bool isAnagram(string s, string t) {
        if(s.length() != t.length()) {
            return false;
        }

        map<char, int> mp;

        for(auto ss : s) {
            mp[ss] ++;
        }

        for(auto ss : t) {
            mp[ss] --;
            if(mp[ss] < 0) {
                return false;
            }
        }

        return true;
    }
};

我的疑惑

  1. 我刚开始使用 vector str;尝试存储计数结果,原来使用 vector vec;也可以,只需要计数的时候与处理一下 str - 'a';
  2. 进阶,可以计数 unicode 的字符,核心在于字符是离散且未知范围的。所以采用哈希表来维护。使用 map 或者 set 容器。

383.赎金信

383.赎金信 - 简单 - 力扣

我的代码

// 题目限定在 26 个小写字母,本题可以开长度 26 的动态数组模拟哈希数组
// 时间复杂度:O(n+m)
// 空间复杂度:O(n);n = 26 长度;
class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        if(ransomNote.length() > magazine.length()) {
            return false;
        }

    vector<int> vec(26, 0);
    for(auto ss : magazine) {
        vec[ss-'a'] ++;
    }
    for(auto ss : ransomNote) {
        vec[ss-'a'] --;
        if(vec[ss-'a'] < 0) {
            return false;
        }
    }
    return true;
    }
};

49.字母异位词分组

49.字母异位词分组 - 中等 - 力扣链接

题目说明:strs[i]仅包含小写字母的字符串。

我的代码

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        
        unordered_map<string, vector<string>> mp;
        for(auto ss : strs) {
            int counts[26] = {0};
            for(char c : ss) {
                counts[c-'a'] ++;
            }
            string key = "";
            for(int i = 0; i < 26; i ++) {
                key.push_back(i+'a');
                key.push_back(counts[i]);
            }
            mp[key].push_back(ss); // vector 类型使用 push_back
        }
        vector<vector<string>> res;
        for(auto ans : mp) {
            res.push_back(ans.second);
        }
        return res;
    }
};

我的疑惑

  1. 法一排序法,哈希表可以存放<string, vector>;
  2. 没想到,使用词频来构造特征词。力扣官答 - 链接
  3. 在 C++ 里直接用 + 来拼接字符串,每次拼接都会生成新的字符串对象,这会带来较大的性能开销。C++ 里推荐用 std::stringstream 或者 std::string 的 append 方法。
  • to_string:把整数类型转换为字符串类型。
  • std::stringstream 来避免性能问题,同时将 (i + 'a') 强制转换为 char 类型。
stringstream KeyStream;
KeyStream << static_cast<char>(i+'a') << to_string(counts[i]);
return KsyString.str();
key.push_back(i+'a');
key.push_back(counts[i]);

438.找到字符串中所有字母异位分词

438.找到字符串中所有字母异位分词

我的代码

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        // 特殊情况
        if(s.size() < p.size()) {
            return vector<int>();
        } 

        // 初始化
        vector<int> vec(26,0);
        vector<int> res;
        int diff = 0;
        for(int i = 0; i < p.size(); i ++) {
            ++vec[p[i] - 'a']; // p 是+
            --vec[s[i] - 'a']; // s 是-
        }
        for(int i = 0; i < 26; i ++) {
            if(vec[i] != 0)diff ++;
        }
        if(diff == 0)res.emplace_back(0);

        int pL = p.size(); 
        for(int l = 1; l < s.size() - pL + 1; l ++) {
            // 移除左端口
            // 问题一:移除的元素下标是 l-1
            ++vec[s[l-1]-'a'];
            if(vec[s[l-1]-'a'] == 0) {
                diff --;
            } else if(vec[s[l-1]-'a'] == 1){
                diff ++;
            }
            // 添加右端口
            int r = l + pL - 1;
            --vec[s[r]-'a'];
            if(vec[s[r]-'a'] == 0) {
                diff --;
            } else if(vec[s[r]-'a'] == -1){
                diff ++;
            }

            if(diff == 0) {
                res.emplace_back(l);
            }
        }
        return res;
    }
};

我的疑惑

  1. 先写下特殊情况,当 s.size() < p.size() 返回空,以防后面忘记。
  2. 动态数组直接进行比较,sCounts == pCounts,相等要求数值和空间大小一样。
  3. ans.emplace_back(0) 与 ans.push_back(0) 的区别。在添加单个元素,推荐第一个写法,性能更优。
  4. diff 可以统计数组非零的个数

349. 两个数组的交集

349. 两个数组的交集

我的代码

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> nums{nums1.begin(), nums1.end()};
        vector<int> res;
        for(auto it : nums2) {
            if(nums.find(it) != nums.end()) {
                nums.erase(it);
                res.emplace_back(it);
            }
        }
        return res;
    }
};

我的疑惑

  1. 我想到哈希,但是没有想到用 set 容器,现成的集合容器。unordered_set us 用法可以去都包里面搜索一下。unordered_set us{ nums1.begin(), nums1.end() };是使用迭代器范围来初始化 us;
  2. 法二:也可以借助指针和排序。可以见官答了解。
  3. 这道题目没有限制数值的大小,就无法使用数组来做哈希表了。

350. 两个数组的交集 II

350. 两个数组的交集 II - 简单 - 力扣

我的代码

// 简单高效
class Solution {
public:
    std::vector<int> intersect(std::vector<int>& nums1, std::vector<int>& nums2) {
        std::unordered_map<int, int> hashmap;
        std::vector<int> res;

        for (auto nn : nums1) {
            hashmap[nn]++;
        }
        for (auto nn : nums2) {
            if (hashmap.count(nn) && hashmap[nn] > 0) {
                --hashmap[nn];
                res.emplace_back(nn);
            }
        }
        return res;
    }
};    

我的疑惑

  1. if (m[key]) 会让 unordered_map 插入键 1 并将对应的值初始化为 0。运行该代码后,会发现 unordered_map 的大小变为 1。
  2. 为避免这种不必要的插入操作,推荐使用 find 或者 count 方法来检查键是否存在:
  • 使用 find 方法:find 方法会返回一个迭代器,若键存在,迭代器指向该键值对;若不存在,迭代器等于 end()。
  • 使用 count 方法:count 方法会返回键在容器中出现的次数,由于 unordered_map 和 map 里键是唯一的,所以返回值要么是 0(键不存在),要么是 1(键存在)。
  1. 仅仅依靠 hashmap.count(nn) 来判断元素是否存在,那么在遍历 nums2 时,只要元素在 hashmap 里存在过,就会将其添加到结果数组中,不会考虑该元素在 nums1 中的实际出现次数。
  2. 如果给定的数组已经排好序呢?你将如何优化你的算法?如果 nums1 的大小比 nums2 小,哪种方法更优?如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?

第 202 题。快乐数

第 202 题。快乐数 - 简单 - 力扣

我的代码

class Solution {
public:
    int action(int num) {
        int ans = 0;
        while(num) {
            int temp = num % 10;//个位上的数字
            ans += temp * temp;
            num /= 10;
        }
        return ans;
    }

    bool isHappy(int n) {
        // 问题一:如何判断他是无限循环的数字
        unordered_set<int> hashmap;
        while(n != 1 && !hashmap.count(n)) {
            hashmap.insert(n);
            n = action(n);
        }
        return n == 1;
    }
};

我的疑惑

  1. 如何取数字上各个数字这个问题已经很熟练了。
  2. 如何判断他是无限循环,可以用链表、快慢指针、哈希表。

1. 两数之和

1. 两数之和 - 简单 -力扣

题目前提只会存在一个有效答案。

我的代码

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hashmap;
        for(int i = 0; i < nums.size(); i ++) {
            int temp = target - nums[i];
            if(hashmap.find(temp) != hashmap.end()) {
                return {hashmap.find(temp)->second, i}; // 问题二:注意返回格式
            }
            hashmap[nums[i]] = i;
        }
        return {}; // 问题一:{}表示返回空数组
    }
};

我的疑惑

  1. 为了减少暴力法中 target-x 的查找时间,使用哈希表 hashmap.find(x).,时间复杂度降低到从 O(N) 降低到 O(1)。
  2. 创建哈希表,对于每一个 x,首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 重复匹配。

总结

算法训练营 Day5 聚焦哈希表,242、349 等多道力扣题,介绍哈希基本概念、常见结构及区别。用 C++ 实现各题解法,含代码、时间空间复杂度分析,还提出疑惑点及解决思路,如容器使用、字符串拼接性能等。