代码随想录算法训练营第六天|哈希表

目录

理论基础

        哈希表的主要概念和特点:

常见的三种哈希结构 

242.有效的字母异位词

相关题目:

49.字母异位词分组

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

349. 两个数组的交集

202. 快乐数

1. 两数之和

第454题.四数相加II

第15题. 三数之和

第18题. 四数之和


理论基础

哈希表(Hash Table),也称为散列表,是根据关键码值(Key value)而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数称为哈希函数,存放记录的数组称为哈希表。

哈希表的主要概念和特点:

  1. 哈希函数:哈希函数是哈希表的核心,它接受一个输入(通常是字符串或整数),并产生一个输出,这个输出通常是一个整数,称为哈希值或哈希码。理想情况下,哈希函数应该能够均匀地分布输入到输出范围,从而减少哈希冲突(即不同的输入产生相同的哈希值)。

  2. 哈希冲突:由于哈希函数的输出范围通常是有限的(比如一个固定大小的数组索引),因此不同的输入可能会产生相同的哈希值,这就是哈希冲突。解决哈希冲突的方法有很多,常见的有开放寻址法(当冲突发生时,寻找下一个空闲位置)链地址法(每个哈希表位置存储一个链表,所有映射到该位置的元素都存储在这个链表中)。

  3. 性能:哈希表在平均情况下提供了常数时间复杂度(O(1))的查找、插入和删除操作,这使得它成为处理大量数据时的理想选择。然而,在最坏情况下(如所有元素都映射到同一个位置),哈希表的性能会退化到线性时间复杂度(O(n))。

常见的三种哈希结构 

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

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

集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::set红黑树有序O(log n)O(log n)
std::multiset红黑树有序O(logn)O(logn)
std::unordered_set哈希表无序O(1)O(1)

           

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

映射底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::map红黑树key有序key不可重复key不可修改O(logn)O(logn)
std::multimap红黑树key有序key可重复key不可修改O(log n)O(log n)
std::unordered_map哈希表key无序key不可重复key不可修改O(1)O(1)

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

如何选择,使用 set  or multiset  or  unordered_set?

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

什么时候用哈希法?

  • 当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候


242.有效的字母异位词

题目链接:242. 有效的字母异位词 - 力扣(LeetCode)

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

解题思路:

使用哈希表(数组)来记录字符串s中每个字符出现的次数,然后遍历字符串t,逐一减少哈希表中对应字符的计数。

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

class Solution {
public:
    bool isAnagram(string s, string t) {
        // 使用数组记录s中字符出现的次数
        int record[26] = {0};

        // 遍历s,记录
        for(int i = 0; i < s.length(); i++){
            record[s[i] - 'a']++;
        }

        // 遍历t,减去t中字符出现的次数
        for(int i = 0; i < t.length(); i++){
            record[t[i] - 'a']--;
        }

        // 查看数组中,是否有元素大于0或者小于0
        for(int i = 0; i < size(record); i++){
            if(record[i] != 0){
                return false;
            }
        }
        return true;
    }
};

相关题目:


49.字母异位词分组

题目链接:49. 字母异位词分组 - 力扣(LeetCode)

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

解题思路:

使用哈希表(unordered_map)来存储这些同形异义词组

其中键是单词中字母排序后的结果,而值则是所有具有相同排序键的原始单词的列表。

接下来,我们遍历给定的字符串数组(strs)。对于数组中的每一个单词,我们执行以下步骤:

  1. 排序单词:将单词中的字母按字母顺序排序,得到一个排序后的字符串。这个排序后的字符串将作为我们在字典中查找或插入的键。

  2. 查找或插入字典

    • 我们检查排序后的字符串(即排序键)是否已经在字典中作为键存在。
    • 如果不存在,我们在字典中创建一个新的条目,键是排序后的字符串,值是一个新的空列表,用于存储具有此排序键的原始单词。
    • 如果存在,我们只需将当前的原始单词添加到与该排序键相关联的列表中。
  3. 继续遍历:我们重复上述步骤,直到遍历完数组中的所有单词。

最后,我们将字典中所有的值(即所有同形异义词的列表)收集起来,并将它们作为结果返回。

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_map<string,vector<string>> mp;

        for(const string& word : strs){
            string sorted_word = word;
            sort(sorted_word.begin(),sorted_word.end());// 对单词进行排序

            // 使用排序后的单词作为键
            mp[sorted_word].push_back(word);
        }

        // 提取结果,将map的值(即vector<string>)转换为vector<vector<string>>
        vector<vector<string>> result;
        for(const auto& pair : mp){
            result.push_back(pair.second);
        }
        return result;

    }
};

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

题目链接:438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

解题思路:

采用滑动窗口结合字符频率统计的方法

基本思路是,首先统计字符串 p 中每个字符的频率,然后使用一个滑动窗口在字符串 s 上滑动,同时维护一个当前窗口内字符的频率统计。当窗口的大小与 p 的长度相等时,我们检查当前窗口的字符频率是否与 p 的字符频率相匹配,如果匹配,则找到了一个异位词,记录起始索引。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> result;
        unordered_map<char,int> pFreq;
        unordered_map<char,int> windowFreq;

        // 统计p中字符的频率  
        for(char c : p){
            pFreq[c]++;
        }

        // 初始化窗口频率表
        int left = 0,matched = 0,required = p.size();
        for(int i = 0; i < p.size(); i++){
            if(pFreq.find(s[i]) != pFreq.end()){
                windowFreq[s[i]]++;
                if(windowFreq[s[i]] == pFreq[s[i]]){
                    matched++;
                }
            }

        }

        // 如果初始窗口就是答案之一
        if(matched == required){
            result.push_back(left);
        }

        // 滑动窗口 
        for(int right = p.size(); right < s.size(); right++){
            // 窗口右移,加入新字符
            char rightChar = s[right];
            if(pFreq.count(rightChar)){
                windowFreq[rightChar]++;
                if(windowFreq[rightChar] == pFreq[rightChar]){
                    matched++;
                }
            }

            // 窗口左移,移除左边界字符
            char leftChar = s[left];
            if(pFreq.count(leftChar)){
                if(windowFreq[leftChar] == pFreq[leftChar]){
                    matched--;
                }
                windowFreq[leftChar]--;
            }
            // 移动左边界
            left++;

            // 检查是否匹配
            if(matched == required){
                result.push_back(left);
            }
        }
        return result;

    }
};


 

349. 两个数组的交集

题目链接:349. 两个数组的交集 - 力扣(LeetCode)

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

解题思路:

使用哈希表记录数组元素

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

所以,这里哈希表采用的是unorder_set

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());
    }
};

202. 快乐数

题目链接:202. 快乐数 - 力扣(LeetCode)

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

「快乐数」 定义为:

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

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

解题思路:

对于1个数,会有以下三种可能。

  1. 最终会得到 1。
  2. 最终会进入循环。
  3. 值会越来越大,最后接近无穷大。

情况1:

情况2:

情况3:

DigitsLargestNext
1981
299162
3999243
49999324
1399999999999991053

对于 3 位数的数字,它不可能大于 243。这意味着它要么被困在 243 以下的循环内,要么跌到 1。

4 位或 4 位以上的数字在每一步都会丢失一位,直到降到 3 位为止。

所以我们知道,最坏的情况下,算法可能会在 243 以下的所有数字上循环,然后回到它已经到过的一个循环或者回到 1。但它不会无限期地进行下去,所以我们排除第三种选择。

利用哈希集合(HashSet)来记录已经出现过的数,以此来检测是否进入了无限循环。这是因为,如果一个数在迭代过程中重复出现,那么它之后的迭代结果也会重复出现,导致无法到达 1。

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;
        }
    }
};

 我们也可以发现,可以使用快慢指针的思想,判断是否存在环

class Solution {
public:
    int getSum(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;
        while(slow != fast && fast != 1){
            slow = getSum(slow);
            fast = getSum(fast);
            fast = getSum(fast);
        }
        
        return fast == 1;
    }
};


1. 两数之和

题目链接:1. 两数之和 - 力扣(LeetCode)

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

解题思路:

使用哈希表来存储遍历过的元素及其索引。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> result;
        unordered_map<int,int> nums_map;

        for(int i = 0; i < nums.size(); i++){
            int complement = target - nums[i]; 
            if(nums_map.find(complement) != nums_map.end()){
                // 如果找到了补数,则返回它们的索引
                result.push_back(nums_map[complement]);
                result.push_back(i);
                return result;
            }

            // 如果没找到,将当前元素及其索引添加到哈希表中
            nums_map[nums[i]] = i;
        }
        return result;
    }
};

第454题.四数相加II

题目链接:454. 四数相加 II - 力扣(LeetCode)

给你四个整数数组 nums1nums2nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

解题思路:采用哈希表来优化查找过程

我们先固定两个数组(例如 nums1 和 nums2),计算所有可能的 nums1[i] + nums2[j] 的和,并将这些和出现的次数存储在一个哈希表中。然后,我们遍历另外两个数组(nums3 和 nums4),对于每一对 (nums3[k], nums4[l]),我们计算它们的相反数 -(nums3[k] + nums4[l]),并检查这个相反数是否存在于我们之前构建的哈希表中。如果存在,则说明找到了一个满足条件的元组,我们只需将对应的计数加到结果中即可。

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        // 用于存储 nums1[i] + nums2[j] 的和及其出现的次数
        unordered_map<int,int> record;
        int  result = 0;

        // 遍历 nums1 和 nums2 的所有组合,并统计和的出现次数  
        for(int x : nums1){
            for(int y : nums2){
                record[x+y]++;
            }
        }

        // 遍历 nums3 和 nums4 的所有组合,查找 -(nums3[k] + nums4[l]) 是否在 record 中
        for(int x : nums3){
            for(int y : nums4){
                if(record.find(0 - x -y) != record.end()){
                    result += record[0 - x -y];
                }
            }
        }
        return result;

    }
};

第15题. 三数之和

题目链接:15. 三数之和 - 力扣(LeetCode)

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

解题思路:排序和双指针

首先,对数组进行排序,这样可以方便地跳过重复的元素,并利用有序数组的特性来减少搜索空间。我们可以将问题分解为一系列的子问题,即固定一个数,然后在剩余的有序数组中查找两个数,使得它们的和等于目标值(在这个问题中是0减去固定的那个数)。这样,原问题就被转化为了一个更简单的“两数之和”问题

难点和关键点:去重

在排序后的数组上,我们使用三个指针来遍历数组:

  1. 固定指针(i:这个指针用于遍历数组中的每个元素,并将其视为三元组中的第一个数。由于数组已经排序,我们可以确保在遍历过程中,nums[i] 是递增的。

  2. 左指针(left:这个指针从固定指针的下一个位置开始,向右移动,用于寻找三元组中的第二个数。

  3. 右指针(right:这个指针从数组的末尾开始,向左移动,用于寻找三元组中的第三个数。

在每次将指针 i 向右移动之前,检查 nums[i] 是否与前一个元素 nums[i-1] 相同。如果是,则跳过这个元素,因为以它开头的所有三元组都会与上一个 nums[i-1] 开头的三元组重复。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) {
                return result;
            }
            
            // 去重a
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int left = i + 1;
            int right = nums.size() - 1;
            while (right > left) {
                // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
                /*
                while (right > left && nums[right] == nums[right - 1]) right--;
                while (right > left && nums[left] == nums[left + 1]) left++;
                */
                if (nums[i] + nums[left] + nums[right] > 0) right--;
                else if (nums[i] + nums[left] + nums[right] < 0) left++;
                else {
                    result.push_back(vector<int>{nums[i], nums[left], nums[right]});
                    // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;

                    // 找到答案时,双指针同时收缩
                    right--;
                    left++;
                }
            }

        }
        return result;
    }
};

第18题. 四数之和

题目链接:18. 四数之和 - 力扣(LeetCode)

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abc 和 d 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

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

解题思路:“四数之和”问题可以看作是“三数之和”问题的一个扩展

  • 在“三数之和”中,通常使用一个外层循环固定一个数,然后使用两个指针在剩余元素中查找另外两个数。
  • 在“四数之和”中,则是使用两个外层循环固定前两个数,然后使用两个指针在剩余元素中查找另外两个数。
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result;
        sort(nums.begin(),nums.end());
        if(nums.size() < 4){
            return result;
        }
        for(int i = 0; i < nums.size() - 3; i++){
            if(i > 0 && nums[i] == nums[i - 1]){
                continue;
            }
            for(int j = i + 1; j < nums.size() - 2; j++){
                if(j > i + 1 && nums[j] == nums[j - 1]){
                    continue;
                }
                int left = j + 1,right = nums.size() - 1;
                while(left < right){
                    long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
                    if(sum == target){
                        result.push_back(vector<int>{nums[i],nums[j],nums[left],nums[right]});

                        while(left < right && nums[left + 1] == nums[left]){
                            left++;
                        }
                        while(left < right && nums[right - 1] == nums[right]){
                            right--;
                        }
                        left++;
                        right--;
                    }else if(sum > target){
                        right--;
                    }else if(sum < target){
                        left++;
                    }
                }
            }
        }
        return result;
    }
};

注意:不要判断nums[0] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值