代码随想录刷题记录(5)| 哈希表(哈希表理论基础,242.有效的字母异位词,349. 两个数组的交集, 202. 快乐数,1. 两数之和)

目录

(一)哈希表理论基础

1. 哈希表

2. 哈希函数

3. 哈希碰撞

(1)拉链法

(2)线性探测法

4. 常见的三种哈希结构

(二)有效的字母异位词

1. 题目描述

2. 思路

3. 解题过程

4. 相关题目

(1)383. 赎金信 - 力扣(LeetCode)

① 题目描述   

② 解题过程

(2)49. 字母异位词分组 - 力扣(LeetCode)

5. 滑动窗口框架

(三)两个数组的交集

1. 题目描述

2. 思路

3. 解题过程

4. 相关题目

(四)快乐数

1. 题目描述

2. 思路

3. 解题过程 

(1)哈希集合 

(2)双指针

(五)两数之和

1. 题目描述

2. 思路

3. 解题过程

4. 关于map的一点补充


(一)哈希表理论基础

1. 哈希表

        哈希表是根据关键码的值而直接进行访问的数据结构,一般用来快速判断一个元素是否出现集合里。

2. 哈希函数

        哈希函数,把元素直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这个元素是否在这个集合里。

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

        如果 hashCode 得到的数值大于哈希表的大小,为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样就保证了元素一定可以映射到哈希表上。

3. 哈希碰撞

        如果元素的数量大于哈希表的大小,此时就算哈希函数计算得再均匀,也避免不了会有几个元素同时映射到哈希表同一个索引下标的位置。有几个元素映射到同一个索引下标这一现象叫做哈希碰撞

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

(1)拉链法

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

(2)线性探测法

        使用线性探测法一定要保证 tableSize 大于 dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。例如冲突的位置放了小李,那么就向下找一个空位放置小王的信息。所以要求 tableSize 一定要大于 dataSize ,不然哈希表上就没有空置的位置来存放冲突的数据了。

4. 常见的三种哈希结构

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

  • 数组

  • 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)

  unordered_set 底层实现为哈希表,set 和 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)

        unordered_map 底层实现为哈希表,map 和 multimap 的底层实现是红黑树。同理,map 和 multimap 的 key 也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

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

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

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

(二)有效的字母异位词

242. 有效的字母异位词 - 力扣(LeetCode)

1. 题目描述

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

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

2. 思路

        数组就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。

3. 解题过程

难易程度:简单

标签:哈希表、字符串、排序

class Solution {
public:
    bool isAnagram(string s, string t) {
        if(s.size() != t.size()){
            return false;
        }
        vector<int> count(26, 0);
        // 计数,s出现一次增加,t出现的减少
        for(int i = 0; i < s.size(); i++){
            count[s[i] - 'a']++;
            count[t[i] - 'a']--;
        }
        // 如果是字母异位词,count中所有应该为0
        for(int i = 0; i < 26; i++){
            if(count[i] != 0){
                return false;
            }
        }
        return true;
    }
};

4. 相关题目

(1)383. 赎金信 - 力扣(LeetCode)
① 题目描述   

        给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

        如果可以,返回 true ;否则返回 false 。

magazine 中的每个字符只能在 ransomNote 中使用一次。

② 解题过程
class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        if(ransomNote.size() > magazine.size()){
            return false;
        }
        vector<int> count(26, 0);
        for(int i = 0; i < magazine.size(); i++){
            count[magazine[i] - 'a']++;
        }
        for(auto & c : ransomNote){
            count[c - 'a']--;
            if(count[c - 'a'] < 0){
                return false;
            }
        }
        return true;
    }
};

 


(2)49. 字母异位词分组 - 力扣(LeetCode)

我的做法:统计每个字符串中字母出现的次数,存数组哈希表,成了一个二维的n*26的数组,(中间的错误思路:将这个二维的数组中的每行存为一个字符串,得到长度为n的一个字符串数组,然后进行比较。问题:如果字母出现次数超过一位数,比较可能出现问题)然后暴力对比,三重循环。

方法一:排序

由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。

class Solution {
public:
 vector<vector<string>> groupAnagrams(vector<string>& strs) {
     unordered_map<string, vector<string>> mp;
     for (string& str: strs) {
         string key = str;
         sort(key.begin(), key.end());
         mp[key].emplace_back(str);
     }
     vector<vector<string>> ans;
     for (auto it = mp.begin(); it != mp.end(); ++it) {
         ans.emplace_back(it->second);
     }
     return ans;
 }
};

emplace_back()push_abck() 的区别是:push_back() 在向 vector 尾部添加一个元素时,首先会创建一个临时对象,然后再将这个临时对象移动或拷贝到 vector 中(如果是拷贝的话,事后会自动销毁先前创建的这个临时元素);而 emplace_back() 在实现时,则是直接在 vector 尾部创建这个元素,省去了移动或者拷贝元素的过程。一般情况下,emplace_back() 方法比 push_back()方法效率高。

方法二:计数

由于互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的,故可以将每个字母出现的次数使用字符串表示,作为哈希表的键。由于字符串只包含小写字母,因此对于每个字符串,可以使用长度为26的数组记录每个字母出现的次数。

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        // 自定义对 array<int, 26> 类型的哈希函数
        // 定义了一个lambda函数arrayHash,用于计算array<int, 26>类型的哈希值。
        // 它使用了一个名为fn的哈希函数对象,对arr中的每个元素进行哈希计算,并通过accumulate函数累加哈希值。
        auto arrayHash = [fn = hash<int>{}] (const array<int, 26>& arr) -> size_t {
            return accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
                return (acc << 1) ^ fn(num);
            });
        };
​
        // 定义了一个无序映射(unordered_map),其中键是array<int, 26>类型,值是vector<string>类型。
        // decltype(arrayHash)用于指明哈希函数的类型,并将其作为第二个参数传递给unordered_map构造函数,
        // 以便在插入元素时使用自定义的哈希函数。
        unordered_map<array<int, 26>, vector<string>, decltype(arrayHash)> mp(0, arrayHash);
        for (string& str: strs) {
            array<int, 26> counts{};
            int length = str.length();
            for (int i = 0; i < length; ++i) {
                counts[str[i] - 'a'] ++;
            }
            mp[counts].emplace_back(str);
        }
        vector<vector<string>> ans;
        for (auto it = mp.begin(); it != mp.end(); ++it) {
            ans.emplace_back(it->second);
        }
        return ans;
    }
};

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

我的做法:暴力,把p的字母次数统计出来,将其与每个s的相同长度子串比较。

方法一:滑动窗口 思路

根据题目要求,我们需要在字符串 s 寻找字符串 p 的异位词。因为字符串 p 的异位词的长度一定与字符串 p 的长度相同,所以我们可以在字符串 s 中构造一个长度为与字符串 p 的长度相同的滑动窗口,并在滑动中维护窗口中每种字母的数量;当窗口中每种字母的数量与字符串 p 中每种字母的数量相同时,则说明当前窗口为字符串 p 的异位词。在算法的实现中,我们可以使用数组来存储字符串 p 和滑动窗口中每种字母的数量。同时,当字符串 s 的长度小于字符串 p 的长度时,字符串 s 中一定不存在字符串 p 的异位词。但是因为字符串 s 中无法构造长度与字符串 p 的长度相同的窗口,所以这种情况需要单独处理。

方法二:优化的滑动窗口

在方法一的基础上,我们不再分别统计滑动窗口和字符串 p 中每种字母的数量,而是统计滑动窗口和字符串 p 中每种字母数量的差;并引入变量 differ 来记录当前窗口与字符串 p 中数量不同的字母的个数,并在滑动窗口的过程中维护它。在判断滑动窗口中每种字母的数量与字符串 p 中每种字母的数量是否相同时,只需要判断 differ 是否为零即可。

class Solution {
public:
 vector<int> findAnagrams(string s, string p) {
     int sLen = s.size(), pLen = p.size();
​
     if (sLen < pLen) {
         return vector<int>();
     }
​
     vector<int> ans;
     vector<int> count(26);
     for (int i = 0; i < pLen; ++i) {
         ++count[s[i] - 'a'];
         --count[p[i] - 'a'];
     }
​
     int differ = 0;
     for (int j = 0; j < 26; ++j) {
         if (count[j] != 0) {
             ++differ;
         }
     }
​
     if (differ == 0) {
         ans.emplace_back(0);
     }
​
     for (int i = 0; i < sLen - pLen; ++i) {
         if (count[s[i] - 'a'] == 1) {  // 窗口中字母 s[i] 的数量与字符串 p 中的数量从不同变得相同
             --differ;
         }
         else if (count[s[i] - 'a'] == 0) {  // 窗口中字母 s[i] 的数量与字符串 p 中的数量从相同变得不同
             ++differ;
         }
         --count[s[i] - 'a'];
​
         if (count[s[i + pLen] - 'a'] == -1) {  // 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从不同变得相同
             --differ;
         }
         else if (count[s[i + pLen] - 'a'] == 0) {  // 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从相同变得不同
             ++differ;
         }
         ++count[s[i + pLen] - 'a'];
         
         if (differ == 0) {
             ans.emplace_back(i + 1);
         }
     }
​
     return ans;
 }
};

5. 滑动窗口框架

void slidingWindow(string s, string t) {
    unordered_map<char, int> need, window;
    for (char c : t) need[c]++;
    
    int left = 0, right = 0;
    int valid = 0; 
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...
​
        /*** debug 输出的位置 ***/
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

本题应用:

vector<int> findAnagrams(string s, string t) {
    unordered_map<char, int> need, window;
    for (char c : t) need[c]++;
​
    int left = 0, right = 0;
    int valid = 0;
    vector<int> res; // 记录结果
    while (right < s.size()) {
        char c = s[right];
        right++;
        // 进行窗口内数据的一系列更新
        if (need.count(c)) {
            window[c]++;
            if (window[c] == need[c]) 
                valid++;
        }
        // 判断左侧窗口是否要收缩
        while (right - left >= t.size()) {
            // 当窗口符合条件时,把起始索引加入 res
            if (valid == need.size())
                res.push_back(left);
            char d = s[left];
            left++;
            // 进行窗口内数据的一系列更新
            if (need.count(d)) {
                if (window[d] == need[d])
                    valid--;
                window[d]--;
            }
        }
    }
    return res;
}

(三)两个数组的交集

349. 两个数组的交集 - 力扣(LeetCode)

1. 题目描述

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

2. 思路

        如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费,此时就要使用另一种结构体了,set

        set的用法:插入(insert)、判断是否存在元素(count)、删除(erase)、查找某个指定元素的迭代器(find)

unordered_set<int> nums_set(nums1.begin(), nums1.end());

3. 解题过程

难易程度:简单

标签:数组、哈希表、双指针、二分查找、排序

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> nums_set(nums1.begin(), nums1.end());
        vector<int> result;
        for(auto & num : nums2){
            if(nums_set.count(num)){
                result.emplace_back(num);
                nums_set.erase(num);
            }
        }
        return result;
    }
};

4. 相关题目

350. 两个数组的交集 II - 力扣(LeetCode)

(四)快乐数

 202. 快乐数 - 力扣(LeetCode)

1. 题目描述

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

「快乐数」 定义为:

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

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

2. 思路

题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!

3. 解题过程 

难易程度:简单

标签:哈希、数学、双指针 

(1)哈希集合 

        其实题目中就给了提示无限循环,如果注意到这个就并不难解。

class Solution {
public:
    // 计算每个位置上的数字的平方和
    int calculate(int n){
        int result = 0;
        while(n){
            result += (n % 10) * (n % 10);
            n /= 10;
        }
        return result;
    }

    bool isHappy(int n) {
        unordered_set<int> nums;
        while(n != 1){
            // 如果重复出现就不是快乐数
            if(nums.count(n)){
                return false;
            }
            nums.insert(n);
            n = calculate(n);
        }
        return true;
    }
};

(2)双指针

使用 “快慢指针” 思想,找出循环:“快指针” 每次走两步,“慢指针” 每次走一步,当二者相等时,即为一个循环周期。此时,判断是不是因为 1 引起的循环,是的话就是快乐数,否则不是快乐数。

参考题解:202. 快乐数 - 力扣(LeetCode)

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. 两数之和 - 力扣(LeetCode)

1. 题目描述

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

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

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

2. 思路

        这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。判断元素是否出现,这个元素就要作为 key,所以数组中的元素作为 key,有 key 对应的就是 value,value 用来存下标。所以 map 中的存储结构为 {key:数据元素,value:数组元素对应的下标}。

3. 解题过程

难易程度:简单

标签:数组、哈希表

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 it = map.find(target - nums[i]);
            if(it != map.end()){
                return {i, it->second};
            }
            // 把这个数插进去
            map.insert(pair<int, int>(nums[i], i));
        }
        return {};
    }
};

4. 关于map的一点补充

        map 是 STL 的一个关联容器,它提供一对一的数据处理能力(有序键值对),第一个元素称为关键字(key,第二个称为关键字的值(value),其中关键字是唯一的。

        pair 是“二元结构体”的替代品,将两个元素捆绑在一起,节省编码时间。相当于以下定义:

struct pair
{
   typename1 first;
   typename2 second;
}

         二者配合使用:

#include<iostream>
#include<map>
using namespace std;

map<int,string> mp;

int main() {
    pair<int, string> p;
    p.first = 0;
    p.second = "haha";

    pair<int, string> p1;
    p1 = make_pair(1,"haha1");

    pair<int, string> p2 = make_pair(2,"haha2");

    pair<int, string> p3(3,"haha3");

    mp.insert(p);
    mp.insert(p3);
    mp.insert(p1);
    mp.insert(p2);

    mp.insert(make_pair(99,"hah99"));
    mp.insert(pair<int, string>(9,"hah9"));

    for(map<int,string>::iterator it = mp.begin(); it!= mp.end(); it++ )
      cout << it->first <<"  "<< it->second << endl; 
    return 0;
} 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值