代码随想录DAY06 - 哈希表 - 08/05

哈希表理论基础

哈希表的定义和作用

哈希表(也叫散列表),Hash Table,即根据关键码的值来直接访问元素的数据结构。数组其实就是哈希表的一种,其中数组索引就是关键码,在数组中我们根据索引下标直接访问到数组元素。

哈希表最重要的作用是能快速查询一个元素是否在集合里。

哈希函数

哈希函数(hash function),作用是把关键码映射为哈希表的索引下标,即 index = HashFunction(key)。常见的哈希函数即除留余数法, HashFunction(key) = hashcode(key) % tableSize. 其中 tableSize 指哈希表的大小。

关键字不一定是数值,可能是字符串类型,而 hashcode 就是通过特定编码方式将其他数据格式转化为不同数值的方法。

哈希碰撞

如上述所言,哈希函数可以将关键字映射为索引,但是如果哈希函数将多个关键字映射到了同一下标,则会造成冲突,即哈希碰撞。

Q1:如何减少冲突?

构造更合适的哈希函数,尽量使函数的映射关系为 一对一 而不是 多对一。

Q2:如何处理冲突?

哈希碰撞的两种解决方法:拉链法、线性探测法

拉链法:

将发生哈希冲突的元素存储在链表中,哈希表中对应索引下标的元素存放的则是指向该链表的指针。

线性探测法(要保证 dataSize > tableSize):

利用哈希表剩余的空间存储冲突的数据,例如,当数据发现位置已经被占用,则寻找下一个空位。

三种哈希结构

三种常见的哈希结构分别是数组、map(映射)、set(集合)。其中 map 和 set 是C++中两个主要的关联容器(关联容器中的元素是按照关键字来保存和访问的)。map中的元素是键值对(key-value),set 的元素即关键字 key。C++ 标准模板库(STL)根据 是否有序 和 是否可重复 将 map 和 set 进行分类,其中,允许重复关键字的命名中包含单词 multi,无序都以单词 unordered 开头。

 // 头文件
 #include <set> // 包括 set 和 multiset
 #include <unordered_set> // 包括 unordered_set
 #include <map> // 包括 map 和 multimap
 #include <unordered_map> // 包括 unordered_map
set 集合

如果需要使用集合解决哈希问题,优先使用 unordered_set,因为其查询和增删效率最优。

集合底层实现分类能否更改数值查询效率增删效率
set红黑树有序不重复不能O(log n)O(log n)
multiset红黑树有序可重复不能O(log n)O(log n)
unordered_set哈希表无序不重复不能O(1)O(1)
map 映射
映射底层实现分类能否更改数值查询效率增删效率
map红黑树key 有序不重复不能O(log n)O(log n)
multimap红黑树key 有序可重复不能O(log n)O(log n)
unordered_map哈希表key 无序不重复不能O(1)O(1)

有效的字母异位词

题干

题目:给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词(若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词)。其中 s 和 t 仅包含小写字母。

示例 1: 输入: s = "anagram", t = "nagaram" 输出: true

示例 2: 输入: s = "rat", t = "car" 输出: false

链接:. - 力扣(LeetCode)

思路

首先理解字母异位词的意思,是指字符串 t 是 字符串 s 的字母打乱顺序的结果。那么只需要保证 t 和 s 的字母、字母个数相同即可判定是t 是字母异位词。

方法一:遍历字符串 s 和字符串 t,用两个数组 sCount[26] 和 tCount[26]分别记录下 s 和 t 中26个字母出现的次数。再次遍历两个数组对比是否有字母出现的次数不同。

方法二:看了代码随想录的题解,思想关键也是用数组存储字母出现的次数,不同的是只需要用一个数组存储 s 的字母次数即可,而在遍历字符串 t 时将字母次数减一,如果最后数组中字母次数都为0,说明 t 是 s 的字母异味词 。

代码

方法一
class Solution {
 public:
     bool isAnagram(string s, string t) {
         // 字符串长度不同直接返回 false
         if (s.size() != t.size()){
             return false;
         }
         int sCount[26] = {0}; // 初始化为 0
         int tCount[26] = {0};
         // 程序到这一步说明 s 和 t 长度肯定相同
        // 因此可以只用一个for循环同时遍历两个字符串
         for (int i = 0; i < s.size(); ++i) {
             sCount[s[i]-'a']++;
             tCount[t[i]-'a']++;
         }
         for (int i = 0; i < 26; ++i) {
             if (sCount[i] != tCount[i]){
                 return false;
             }
         }
         return true;
     }
 };
方法二
class Solution {
 public:
     bool isAnagram(string s, string t) {
         int sCount[26] = {0}; // 初始化为 0
         // 因为分别用两个 for 循环遍历 s 和 t 两个字符串,所以不需要像方法一一样比较长度
         for (int i = 0; i < s.size(); ++i) {
             sCount[s[i]-'a']++;
         }
         for (int i = 0; i < t.size(); ++i) {
             sCount[t[i]-'a']--;
         }
         for (int i = 0; i < 26; ++i) {
             if (sCount[i] != 0){
                 return false;
             }
         }
         return true;
     }
 };

两个数组的交集

题干

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

链接:. - 力扣(LeetCode)

思路

方法一:由于交集元素唯一且不需要考虑顺序,那么可以用 无序且不可重复集合 unordered_set 来存储其中一个数组的元素,之后无需用两个 for 循环遍历数组,只需在集合中进行查询对比,因为集合查询的效率远远高于数组。首先遍历数组 nums1,将元素存储在集合 numSet 中。之后遍历数组 nums2,如果 nums2 的元素也在集合中则将其存储到交集数组中,同时删除集合中的该元素,删除是为了避免数组 nums2 之后可能有重复的交集元素。时间复杂度O(n)

方法二暴力解法:两个for循环遍历数组,逐一比较数组中的元素找出相同的交集。时间复杂度O(n^2)

代码

方法一
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        vector<int> result; // 存储结果
        unordered_set<int> numSet(nums1.begin(),nums1.end()); // 用集合存储数组去重
        for (int num : nums2) {
            if (numSet.find(num) != numSet.end()){
                result.push_back(num); // 交集元素只需要 push_back 一次就够了,所以之后要删除
                // 同时在集合中删除该元素,免得 数组 nums2 后续元素有重复
                numSet.erase(num);
            }
        }
        return result;
    }
};
方法二:暴力解法
class Solution {
 public:
     vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
         vector<int> result; // 存储结果
         for (int i = 0; i < nums1.size(); ++i) {
             for (int j = 0; j < nums2.size(); ++j) {
                 if (nums1[i] == nums2[j]){
                     // find 函数在头文件 #include <algorithm> 中
                     if (find(result.begin(),result.end(),nums1[i]) == result.end()){
                         result.push_back(nums1[i]);
                     }
                 }
             }
         }
         return result;
     }
 };

快乐数

题干

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

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。

  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。

  • 如果这个过程 结果为 1,那么这个数就是快乐数。

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

示例:

输入:19 输出:true 解释: 1^2 + 9^2 = 82 8^2 + 2^2 = 68 6^2 + 8^2 = 100 1^2 + 0^2 + 0^2 = 1

链接:. - 力扣(LeetCode)

思路

解决此题最关键的是两个问题:

Q1:如何判定 n 是快乐数?

n 最后能变成 1。

Q2:如何判定 n 是非快乐数?

n 变化过程中出现了循环,即出现了重复。

代码

class Solution {
 public:
     // 求每次变化后 n 变为了啥
     int replace(int n){
         int num = 0; // 记录每一位数字
         int sum = 0; // 记录平方和
         while (n > 0){
             num = n%10;
             sum += num*num;
             n = n/10;
         }
         return sum;
     }
     bool isHappy(int n) {
         unordered_set<int> nums; 
         // 存储每次变换后的数,无重复集合,如果之后新的平方和已经出现在该集合中,说明出现了循环
         bool flag;
         while (1){
             if (n == 1){ // 当 n 变为 1 时,说明是快乐数
                 flag = true;
                 break;
             }
             if (nums.find(n) == nums.end()){
                 // 没找到说明是新的数,插入集合中
                 nums.insert(n);
             } else{ 
                 // 如果找到了之前的数,说明循环,不是快乐数
                 flag = false;
                 break;
             }
             n = replace(n); // 不断替换成每一位的平方和
         }
         return flag;
     }
 };

两数之和

题干

题目:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个位置的元素在答案里不能重复使用两遍。你可以按任意顺序返回答案。2 <= nums.length <= 10^4,nums[ i ] 和 target 都可能是负数。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9

所以返回 [0, 1]

题干:. - 力扣(LeetCode)

思考

方法一暴力解法:两两对比数组中的元素,找到 和 为 target 的下标。时间复杂度 O(n^2),在力扣中运行时间为一百多毫秒。

方法二map:看了题解,可以在一次遍历数组时,用 map 的键值对存储前面遍历过的数组中的元素和对应的下标,在后续遍历时我们需要查询 map 是否存在能和之后的元素配对成 target 的数值,如果有则返回,如果没有则存入map中。时间复杂度为 O(n),在力扣中运行时间大概为10毫秒以内。

Q1:为什么是用 map 存储遍历过元素?

因为我们在找 target 时面对两个问题,一是两个元素是否匹配其和为 target,二是如果匹配那么两个元素的数组下标是什么?我们不仅需要操作元素值,也需要查询到数组下标,而map的键值对刚好可以将元素值和下标进行存储和映射。map 可以直接通过元素值返回数组下标。

Q2:map 是不可重复的,那么遍历数组时碰到前后有重复元素时是什么情况?

假如之前map已经存储了元素 3,而后续遍历数组时又碰到元素 3,如果 target 为6,刚好配对,直接 return;如果 target 不为 6,那么不配对,此时将后面的元素 3 替代掉原来map 中的元素 3,虽然键值对中的数组下标改变了,但是元素值没变,并不影响之后的配对。

代码

方法一:暴力解法
class Solution {
 public:
     vector<int> twoSum(vector<int>& nums, int target) {
         vector<int> index; // 存储结果下标
         for (int i = 0; i < nums.size(); ++i) {
             for (int j = i+1; j < nums.size(); ++j) {
                 if (nums[i]+nums[j] == target){
                     index.push_back(i);
                     index.push_back(j);
                     return index;
                 }
             }
         }
         return {};
     }
 };
方法二:map
class Solution {
 public:
     vector<int> twoSum(vector<int>& nums, int target) {
         unordered_map<int,int> pastNum; // 可以无序
         vector<int> result;
         for (int i = 0; i < nums.size(); ++i) {
             if (pastNum.find(target-nums[i]) != pastNum.end()){
                 // 找到配对的
                 result.push_back(pastNum[target-nums[i]]);
                 result.push_back(i);
                 return result;
             } else{
                 pastNum.insert({nums[i],i});
             }
         }
         return {}; // 如果for循环找不到满足条件的,返回空
     }
 };

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值