数据结构-哈希表-总结

一、理论部分

1.1 定义

哈希表是根据关键码的值而直接进行访问的数据结构。

在这里插入图片描述

要解决的问题一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。

1.哈希函数

哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。

哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
在这里插入图片描述
问题1:如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?

此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。

问题2:如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。

引出哈希碰撞问题

2. 哈希碰撞

如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞
在这里插入图片描述

解决办法1-拉链法:

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

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

解决方法2-线性探测法

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

例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
在这里插入图片描述

1.2 常见哈希结构

1. 数组

没啥可说的

2. set(集合)、map(映射)

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
在这里插入图片描述
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
在这里插入图片描述
std::unordered_map 底层实现为哈希表,std::mapstd::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

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

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

其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。

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

这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?

实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
在这里插入图片描述

二、相关题目1-有效的字母异位词

题目

力扣链接
在这里插入图片描述

思路

暴力做法:

先看暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。

哈希法:

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

需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。

在这里插入图片描述
定义一个数组叫做record用来上记录字符串s里字符出现的次数。

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

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

为了减少数组的使用量,检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。

那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。

最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。

代码实现

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 i = 0;i<t.size();i++){
        record[t[i] - 'a']--;
    }
    for(int i = 0;i< 26;i++){
        if(record[i] != 0)
            return false;
    }
    return true;
    }
};

总结体会:

1:.size()是容器或者字符串使用的
2:数组初始化方式

int arr[5];
int arr[5] = {1, 2, 3, 4, 5}; 
int arr[5] = {};  // 所有元素初始化为 0

必须指定大小
3:时间复杂度为3次O(n)->O(n)
4:本题使用数组构建哈希表是因为数据大小已知了,若未知要用其他的数据结构

三、相关题目2-两个数组的交集

题目

数组交集
在这里插入图片描述

思路

这道题目,主要要学会使用一种哈希数据结构:unordered_set,这个数据结构可以解决很多类似的问题。

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

本题中数据大小未知,哈希值比较分散,跨度大,使用数组会造成浪费,因此本题使用set来解决问题

set中有三种重要数据结构:
在这里插入图片描述

std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
(问题:三者区别是什么,用处?)
在这里插入图片描述

代码实现

unordered_set 的使用
定义
不指定大小

#include <unordered_set>
std::unordered_set<int> mySet;  // 定义一个名为 mySet 的空的 unordered_set

指定大小

std::unordered_set<int> mySet(num);//例如:num = 100

插入删除

mySet.insert(10);

mySet.erase(10);

新语法的使用:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums2 = {1, 2, 3, 4, 5};

    for (int num : nums2) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上述示例中,我们定义了一个 std::vector(nums2),并使用范围-based for 循环遍历 nums2 中的每个元素。在每次循环迭代中,将当前元素赋值给迭代变量 num,然后输出 num 的值。

代码:

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());//将nums1的内容复制到nums_set中
            for(int num : nums2){
                /*如果元素 num 存在于 nums_set 中,
                则 nums_set.find(num) 返回一个指向该元素的迭代器;
                如果元素 num 不存在于 nums_set 中,
                则返回一个指向集合末尾的迭代器 nums_set.end()。*/
                if(nums_set.find(num) != nums_set.end()){
                    result_set.insert(num);
                }
            }
            return vector<int>(result_set.begin(),result_set.end());
    }
};

总结体会:

  • 时间复杂度: O(mn)
  • 空间复杂度: O(n)
  • 使用数组和set的优劣:
    直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。

四、相关题目3-快乐数

题目

快乐数
在这里插入图片描述

思路

逻辑:
迭代后
1:无限循环
(1)不重复循环:由于数不会无限变大,所以不能每个结果都不一样
(2)有重复循环:检查sum的结果有无重复,有重复就返回false
2:不无限循环
返回true

代码实现

class Solution {
public:
    int getsum(int n){
        int sum = 0;
        while(n){
            sum += (n%10)*(n%10);
            n = n/10;
        }
        return sum;
    }
    /*这个求和比较巧妙,要记住*/
    bool isHappy(int n) {
        unordered_set<int> set;
        int sum = 0;
        while(1)
        {
            sum = getsum(n);
            if(sum == 1)
            return true;
            if(set.find(sum) != set.end())
            return false;
            else
            set.insert(sum);
            n = sum;
        }
    }
};

总结体会:

  • 时间复杂度: O(logn)
  • 空间复杂度: O(logn)

五、相关题目4-两数之和

题目

两数之和
在这里插入图片描述

思路

选择哈希法

将数组的数copy两份,在第一个中遍历数据的时候,查找第二个中有无可以和该数据相匹配(相加=target),在查询匹配时本题涉及到对一个集合(数组)中元素的查找,因此选择哈希法

首先我在强调一下什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否·在集合里的时候,就要第一时间想到哈希法。本题呢,就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
那么我们就应该想到使用哈希法了。

选择map

使用数组和set来做哈希法的局限。

  • 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
  • set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。(set只有key没有value)

因此要选择一种有key和value的存储结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。

map是STL的一个关联容器,它提供一对一的hash。

  • 第一个可以称为关键字(key),每个关键字只能在map中出现一次;
  • 第二个可能称为该关键字的值(value);

有三种map
在这里插入图片描述
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。

同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

这道题目中并不需要key有序,选择std::unordered_map 效率更高! 使用其他语言的录友注意了解一下自己所用语言的数据结构就行。

思路详解

这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。

那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。(此处是为了方便进行)

所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。

为了避免全盘复制的操作(浪费),在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
在这里插入图片描述
问题:
key值要求不唯一,但是如果数组元素重复了在怎么办?
分析:

  • 原题要求只有一种输出,则如果数组有重复数据,那只有可能这组重复数据正好两个且相加就是结果,如不是,结果将会多解
  • 于是,我们担心两次将相同的元素存入同一个key处,新的会覆盖旧的,但是本题代码中是先读取再覆盖,第二个相同的数并没有被写入map中,最后读取结果是不会被覆盖的

代码实现

find解析:不是在找value是在找key

map.find(key)

因为是在找key,所以把数组元素作为key用来查找,对应下标当作value
代码:

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]);
            //找不到的话返回map.end(),找到的话返回std::pair<target - numsp[i],value>
            //寻找是否有匹配,有的话返回两个下标
            if(iter != map.end()){
                return {iter->second,i};//iter->second返回map中的value(第二个值)
            }
            //没有找到匹配则插入到map中
            map.insert(pair<int,int>(nums[i],i));//std::map 的 insert 函数接受一个键值对(std::pair)作为参数来插入元素。
        }
        return {};
    }
};

总结体会:

  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

本题四个重点:

  • 为什么会想到用哈希表
  • 哈希表为什么用map
  • 本题map是用来存什么的
  • map中的key和value用来存什么的

六 、相关题目5-四数相加

题目

四数相加
在这里插入图片描述

思路

本题来源于三数之和还有四数之和的简化,和二数之和差别很大,本题不要求结果不重复,因此选择哈希法,如果结果重复就要用其他方法了,哈希法在这里去重很困难
步骤:

    1. 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
    1. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
    1. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
    1. 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
    1. 最后返回统计值 count 就可以了

(对于为什么不考虑a+c和b+d或者a和b+c+d这样的情况是否和上述情况有所区别:
直接把选取数的过程看作从四个数组里面各自取一个数,只有最后一个数会受前面数的影响,那么可以转换为3+1问题,同理3相加可以转换为2+1问题,那么再等效一下就变成了2+2问题)

代码实现

map操作

value = map[key];//key是索引,value是值

代码

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> map_ab;
        for(int a : nums1)
            for(int b : nums2){
                map_ab[a+b]++;
            }
        int count = 0;
        for(int c:nums3)
            for(int d:nums4){
                if(map_ab.find(0-c-d) != map_ab.end()){
                    count += map_ab[0-c-d];
                }
            }
        return count;
    } 
};

总结体会

  • 时间复杂度: O(n^2)
  • 空间复杂度: O(n^2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n^2

七、相关题目6-赎金信

题目

赎金信
有一点要注意,这是赎金信,为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次
在这里插入图片描述

思路

涉及到元素的查找,且数据大小确定,不要求给出索引值,则用数组是合适的
步骤:类似有效的字母异位词

  • 将每个字符在magazine中出现的次数存在一个record里面
  • 对把目标信件中出现的词在record对应位置-1,如果magazine在record中对应的位置有元素<0,则返回false

代码实现

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int records[26]= {0};//不是[0]
        for(int i = 0; i <magazine.size();i++){
            records[magazine[i] - 'a']++;
        }
        for(int i = 0;i < ransomNote.size();i++){
            records[ransomNote[i] - 'a']--;
            if(records[ransomNote[i] - 'a'] < 0)//可以放到同一个循环中,因为没有--处理到的位置必然不会<0,此处表示刚好减到杂志中对应位置
                return false;
        }
        return true;
    }
};

总结体会

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

========哈希法难以处理的情况双指针法处理

八、相关题目7-三数之和

题目

三数之和
在这里插入图片描述

思考

本题和之前的四数相加存在一个重要的区别:题目中说的不可以包含重复的三元组,意味着要进行去重操作,使用哈希法很困难,因此选择双指针来解决
在这里插入图片描述

主要思路有两部分:
1:如何取三个数:

  • 拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。

  • 接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

2:如何去重:

  • 回想遍历过程,先移动i,然后再保持i不动,移动right和left,这其中每次只有一个指针移动,因此,每一个数如果周围有相同的数,那么结果中就会产生重复,因此,去重是指在nums[i]和nums[i+1]还有nums[i-1]中操作
  • a的去重:a 如果重复了怎么办,a是nums里遍历的元素,那么应该直接跳过去。但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。有同学可能想,这不都一样吗。如果我们的写法是这样:
if (nums[i] == nums[i + 1]) { // 去重操作
    continue;
}

但是,我们先确定a的位置,此时还没有判断b,c,在找b,c的过程中可能会用到nums[i+1],那么nums[i+1]是不能先去掉的,于是我们要等b,c确定了再对a降重,因此我们就要判断相同的nums[i-1]是否要去除,因为nums[i-1]已经判断过了,与之对应的b,c已经确定,而nums[i]和nums[i-1]相同,那么对应的b,c也确定了,所以可以判断了,我们这样写:

if (i > 0 && nums[i] == nums[i - 1]) {
    continue;
}
  • b,c的去重:依据和a相似的逻辑,要先等a,b,c都定了再降重,那么需要把降重放到b,c移动之后,此时不需要再考虑nums[i+1]会被使用,所以直接判断nums[i+1]即可,注意:因为right是往左移,他是判断nums[i-1]

代码实现

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(),nums.end());
        for(int i = 0;i<nums.size();i++){
            if(nums[i] > 0){
                return result;
            }
            if(i > 0&&nums[i] == nums[i -1]){
                continue;//跳出本次循环
            }//a的降重

            int left = i+1;
            int right = nums.size() - 1;

            while(right > left){
                if(nums[i] + nums[left] + nums[right] > 0)
                    right--;
                else if(nums[i] + nums[left] + nums[right] < 0)
                    left++;
                else{//开始b,c的降重
                    result.push_back(vector<int>{nums[i],nums[left],nums[right]});
                    // while(nums[right] == nums[right-1]) right--;
                    // while(nums[left] == nums[left+1])left++;
                    while(right > left&&nums[right] == nums[right-1]) right--;
                    while(right > left&&nums[left] == nums[left+1])left++;
                    //找到一次之后要继续找
                    right--;
                    left++;
                }
            }
        }
        return result;
    }
};

总结体会

  • 时间复杂度: O(n^2)
  • 空间复杂度: O(1)
  • 思考:既然三数之和可以使用双指针法,我们之前讲过的1.两数之和 (opens new window),可不可以使用双指针法呢?
    如果不能,题意如何更改就可以使用双指针法呢? 大家留言说出自己的想法吧!
    两数之和 就不能使用双指针法,因为1.两数之和 (opens new window)要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。
    如果1.两数之和 (opens new window)要求返回的是数值的话,就可以使用双指针法了

九、相关题目8-四数之和

题目

四数之和
在这里插入图片描述

思路

和三数之和很类似,我们将其看做先找三数之和,再找三数之和加一数==target

注意:在三树之和中,和为0,有裁剪部分

if(nums[i] > 0)return result;

但是我们要知道,本题中的target为任意数,那么target>=0target<0是不一样的,target>=0时,nums[i]就>0,再加上比他大的数不可能变小,还可以用原来的裁剪方案,但是对于target<0,此时nums[i]加上比它大的数(某一个负数)结果可能变小,所以不能裁剪,因此裁剪方案变为:

if(nums[i]>target &&(nums[i]>=0)) return result;//只有nums[i]>=0才裁剪

但要注意,在三数之和中最外面只有一重for循环,直接return和break都是一样跳出一重循环,但是四数之和有两层for循环,此时第二层不能用return了只能用break,即:

if(nums[j]+nums[i]>target&&(nums[j]+nums[i]>=0))break;//这里不要再用return result了,用break只跳一重循环

代码实现

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
            vector<vector<int>> result;
            sort(nums.begin(),nums.end());
            for(int i = 0;i < nums.size();i++){
                if(nums[i]>target &&(nums[i]>=0)) return result;
                if(i>0&&nums[i]==nums[i-1])//&&第一个为false就不计算后面,&计算两边
                    continue;
                for(int j = i+1;j<nums.size();j++){
                    if(nums[j]+nums[i]>target&&(nums[j]+nums[i]>=0))break;//这里不要再用return result了,用break只跳一重循环
                    if(j>i+1&&nums[j]==nums[j-1])continue;
                    int left=j+1;
                    int right=nums.size()-1;
                    while(left<right){
                        if((long)nums[i]+nums[j]+nums[left]+nums[right] > target) right--;
                        else if((long)nums[i]+nums[j]+nums[left]+nums[right] < target) left++;
                        else{
                            result.push_back(vector<int>{nums[i],nums[j],nums[left],nums[right]});
                            while(right > left&&nums[right]==nums[right-1])right--;
                            while(right > left&&nums[left]==nums[left+1])left++;
                            right--;
                            left++;
                        }
                    }
                }
            }
            return result;
    }
};

总结体会

  • 时间复杂度: O(n^3)
  • 空间复杂度: O(1)

十.哈希表总结

1基础用法

一般来说哈希表都是用来快速判断一个元素是否出现集合里。

对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用.

哈希函数是把传入的key映射到符号表的索引上。

哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。

2什么时候使用哈希表以及使用哪种哈希表

当我们的思路中涉及到查找相关的步骤,可以考虑把数据放到数组,set或者map中用哈希法解决问题

2.1 数组

对于数据大小(种类)确定,可以使用一个固定大小的数组来存放和查找,且能用数组就用数组,因为简单且效率高

383.赎金信 中同样要求只有小写字母,那么就给我们浓浓的暗示,用数组!

本题和242.有效的字母异位词 很像,242.有效的字母异位词是求 字符串a 和 字符串b 是否可以相互组成,在383.赎金信中是求字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a

上面两道题目用map确实可以,但使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效!

2.2 set

首先要了解数组的局限:

  • 数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
  • 如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。

当给出的数据大小不确定,且只需要一个值,用set

关于set,C++ 给提供了如下三种可用的数据结构:(详情请看关于哈希表,你该了解这些! (opens new window))

  • std::set
  • std::multiset
  • std::unordered_set
    std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希,使用unordered_set 读写效率是最高的,本题并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。(还没有使用过其他set)

2.3 map

数组和set的局限:

  • 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
  • set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。

如果需要存放两个值,且大小未知,则用map

C++提供如下三种map::(详情请看关于哈希表,你该了解这些! (opens new window))

  • std::map
  • std::multimap
  • std::unordered_map

std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层实现是红黑树。

同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解),1.两数之和 中并不需要key有序,选择std::unordered_map 效率更高!

3.常见使用方式

3.1 数组:

把数据值存放在数组的下标中,数组元素用来对表示对下标的操作的结果

record[magazine[i]-'a'] ++;
 record[s[i] - 'a']++;

3.2 set

将数据存放到set中,然后使用find查找

1:多组数据

定义两个set,一个set_1用于数据存放到其中,另一个set_2用于数据挨个到set_1中查找,查找结果放到set_2中

		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);
            }
        }
2:单组数据:

定义一个set,边判断边存入,先判断(查询)是否符合条件,符合条件就return,不符合的就存入set中

			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;

3.3 map

1:存放数据并查找
先判断是否满足条件,没找到匹配的,就把数据存入map中

// 遍历当前元素,并在map中寻找是否有匹配的key
            auto iter = map.find(target - nums[i]); 
            if(iter != map.end()) {
                return {iter->second, i};
            }
            // 如果没找到匹配对,就把访问过的元素和下标加入到map中
            map.insert(pair<int, int>(nums[i], i)); 

2:先用key存放数据,value存放对这个数据操作的结果;然后使用find进行查询

		unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
        // 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
        for (int a : A) {
            for (int b : B) {
                umap[a + b]++;
            }
        }
        int count = 0; // 统计a+b+c+d = 0 出现的次数
        // 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
        for (int c : C) {
            for (int d : D) {
                if (umap.find(0 - (c + d)) != umap.end()) {
                    count += umap[0 - (c + d)];
                }
            }
        }

4.本章题目思路总结

1:

用数组当成哈希表,数据当作下标,把nums[i]用于计数,先计第一个string,用++,再计第二个string,用- -;

2:

涉及两组数据,定义两个set,一个为result,一个set放其中一组数据,把查找set中有无和另一组数据相同的数据,把相同的放到result中

3:

定义一个set,先判断sum是否再result中出现过,然后再把sum放进result中

4:

定义一个map,遍历,先判断目标元素是否在map中,在的话返回结果,不在的话就把当前nums[i]和下标存入map中

5:

涉及到四组数据,按照之前的思路,应该用三层for循环,最后一层查找,但是这样时间复杂度,空间复杂度(要先复制三组数据)都比较高,由于本题要求的答案只是次数的统计,所以没必要按照这样的完整方案实行:
定义一个map,key存放a+b的值,value存放对应值出现的次数,然后再次遍历,将map[0-c-d]的数量加到count上, 最后输出count;
本题算是一种小技巧,和其他题有点区别

6:

用数组,先再magazine中把每一个字母映射到一个数组record中,具体方法:下标存字母对应顺序,数值存字母出现次数,然后再ransom note中把字母对应的下标中的数值- -,并且在此轮循环中就可以判断是否出现record【i】<0(magazine中没有的字母但ransom note中出现了),出现了就return false,一直没出现就return true;

7,8:

用哈希表很复杂(去重麻烦),双指针方便

要点总结:

1:一般为减少时间开支,在最后一个for循环里面,一边遍历,判断存放数据,一边就判断时候输出,能少用一个循环
2:待定(二刷说不定有新体会)

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值