DAY5 哈希表(一):哈希表基础+数组练习+set容器练习

1概念

哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。

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

直白来讲其实数组就是一张哈希表。

哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
在这里插入图片描述
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。

例如要查询一个名字是否在这所学校里。

要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。

我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。

将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数

2.哈希函数

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

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

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

此时问题又来了,哈希表我们刚刚说过,就是一个数组。

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

接下来哈希碰撞登场

3.哈希碰撞

如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞
在这里插入图片描述
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。

拉链法

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

在这里插入图片描述
(数据规模是dataSize, 哈希表的大小为tableSize)

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

线性检测法

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

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

4.常见的三种哈希结构

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

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

三种数据结构的简介:

  1. 数组:数组是最基本的数据结构之一,它是由一系列相同类型的元素组成的,并且每个元素都有一个索引来标识它在数组中的位置。数组的优点是访问元素的速度非常快,因为只需要通过索引就可以直接找到元素,不需要遍历整个数组。但是,数组的大小在创建时就已经固定,不能动态增加或减少。
  2. Set:Set是一个不包含重复元素的集合。它的主要用途是进行存在性检查,也就是检查一个元素是否存在于集合中。Set通常是通过哈希技术实现的,所以检查元素是否存在的操作非常快,时间复杂度接近O(1)。在Python中,set是一个内置的数据类型,而在Java中,HashSet类提供了类似的功能。
  3. Map:Map(也被称为字典或哈希映射)是一种将键(Key)映射到值(Value)的数据结构。这种映射是一对一的,也就是说,每个键只能对应一个值,但是一个值可以被多个键共享。Map的一个重要应用就是快速查找,因为通过键,我们可以直接找到对应的值,而不需要遍历整个数据结构。在Python中,dict是一个内置的数据类型,而在Java中,HashMap类提供了类似的功能。

三种数据结构基本使用方式

  1. 数组:在C++中,std::array是一个固定大小的数组,其大小在编译时需要已知。这是一个std::array的使用示例:

    #include <array>
    
    int main() {
        std::array<int, 5> arr = {1, 2, 3, 4, 5};
        for(int i = 0; i < arr.size(); i++) {
            std::cout << arr[i] << std::endl;
        }
        return 0;
    }
    
  2. Setstd::unordered_set是一个不包含重复元素的集合。其内部实现是一个哈希表,所以查找元素的速度非常快。以下是一个std::unordered_set的使用示例:

    e#include <unordered_set>
    
    int main() {
        std::unordered_set<int> set;
        set.insert(1);
        set.insert(2);
        set.insert(3);
    
        if(set.find(2) != set.end()) {
            std::cout << "2 is in the set" << std::endl;
        }
        return 0;
    }
    
  3. Mapstd::unordered_map是一个哈希表,它可以将键映射到值。以下是一个std::unordered_map的使用示例:

    #include <unordered_map>
    
    int main() {
        std::unordered_map<std::string, int> map;
        map["Alice"] = 18;
        map["Bob"] = 20;
    
        if(map.find("Alice") != map.end()) {
            std::cout << "Alice is " << map["Alice"] << " years old." << std::endl;
        }
        return 0;
    }
    

set和map在c++中提供数据结构的对比

set容器

std::unordered_setstd::setstd::multiset 都是C++标准模板库(STL)中的容器。它们都可以用来存储元素,但是各自有各自的特性和使用场景。

  • std::set:这也是一个集合类型的容器,它存储唯一的元素,但是元素是按照一定的顺序存储的。这种顺序是通过元素的比较函数来确定的。std::set的内部实现是基于红黑树的,红黑树是一种自平衡的二叉查找树,所以std::set的查找,插入,删除的时间复杂度是O(logn)。

  • std::multiset:这是类似于std::set的容器,但是它允许存储重复的元素。同样,std::multiset的内部实现也是基于红黑树的。

  • std::unordered_set:这是一个集合类型的容器,它存储唯一的元素,并且元素的存储没有任何特定的顺序。它的内部实现是基于哈希表的,所以其查找,插入,删除的时间复杂度都是接近O(1)。但是由于哈希表的特性,它不支持顺序遍历。

所以,std::set确实是一个容器,它与std::unordered_setstd::multiset一样,都是STL的一部分。

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
在这里插入图片描述
std::unordered_set底层实现为哈希表,std::setstd::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

map容器

std::unordered_mapstd::mapstd::multimap 都是C++标准模板库(STL)中的容器,它们都属于映射容器(map containers)。

  • std::map:这也是一个关联容器,它存储的元素是键值对,每个键都唯一,但是键值对是按照键的顺序存储的。这种顺序是通过键的比较函数来确定的。std::map的内部实现是基于红黑树的,红黑树是一种自平衡的二叉查找树,所以std::map的查找,插入,删除的时间复杂度是O(logn)

  • std::multimap:这是类似于std::map的容器,但是它允许存储键相同的键值对。同样,std::multimap的内部实现也是基于红黑树的。

  • std::unordered_map:这是一个关联容器,它存储的元素是键值对,每个键都唯一,且键值对的存储没有任何特定的顺序。这是因为std::unordered_map的内部实现是基于哈希表的,所以它的查找,插入,删除的时间复杂度都是接近O(1)。

所以,std::map是一个映射容器,与std::unordered_mapstd::multimap一样,它们都是STL的一部分。这些容器的选择和使用,通常根据具体的应用需求和性能要求来决定。

在这里插入图片描述
std::unordered_map 底层实现为哈希表,std::mapstd::multimap 的底层实现是红黑树。同理,std::mapstd::multimapkey也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

如果底层实现是红黑树,那么key排列就是有序的。且key是不可修改的。能否重复要看不同容器的情况。

set容器与map容器总结

当我们要使用集合来解决哈希问题的时候,优先使用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也是一样的道理。

为什么底层是哈希表的容器,查找效率高?

哈希表的工作原理是,它使用一个哈希函数将键映射到一个“桶”中,这个“桶”就是哈希表的一个位置。当你要查找一个元素时,哈希表首先计算键的哈希值,然后直接跳到对应的桶中。因此,无论哈希表中有多少元素,查找操作的时间复杂度通常都是O(1),也就是常数时间

然而,这种效率是有一定代价的。首先,哈希函数需要是一个好的哈希函数,否则会导致很多键都映射到同一个桶中,这就会产生哈希冲突,大大降低查找效率。其次,unordered_map不保证元素的顺序,这与mapmultimap(它们都是基于平衡二叉搜索树实现的)不同,后者可以保证元素以特定的顺序存储。

另外,哈希表的插入、删除操作也是常数时间复杂度O(1),但这是在最理想的情况下,即哈希冲突很少的情况下。在实际使用中,如果哈希冲突太多,哈希表的性能就会下降。而平衡二叉搜索树的插入、删除操作时间复杂度是O(log n),n是元素数量。在元素数量很大时,这个时间复杂度仍然能保持较好的性能。

关于查询uset.find(res)操作

代码示例:139.单词拆分

//背包在外物品在内
        for(int i=0;i<=s.size();i++){
            //物品就是字典里的元素
            for(int j=0;j<wordDict.size();j++){
                int len=wordDict[j].size();
                if(i>=len){
                    auto res = s.substr(i-len,len);
                    //只要末尾这一段元素能和字典单词对应,且前面的部分也是true,就返回true
                    if(wordD.find(res)!=wordD.end()&&dp[i-len]==true) dp[i]=true;
                }
            }
        }

当我们讨论哈希表(在C++中,通常称为unordered_map或unordered_set)的操作的时间复杂度时,通常会说它的时间复杂度是O(1),也就是常数时间复杂度。这是因为哈希表的设计使得它能够快速地定位到一个特定的元素。

然而,这是基于一个假设,即哈希函数能够很好地分布元素,从而避免冲突。在冲突极少的情况下,查找、插入和删除等操作的平均时间复杂度确实可以被认为是O(1)。但是在最坏的情况下(也就是所有的元素都映射到同一个哈希值),哈希表的所有操作的时间复杂度都会退化到O(n),其中n是哈希表中元素的数量

但是,实际中由于好的哈希函数设计,我们一般认为哈希表的查找、插入和删除操作的平均时间复杂度是O(1),在分析算法时间复杂度时,我们通常会这样认为。

平衡二叉搜索树补充

平衡二叉搜索树(Balanced Binary Search Tree)是一类特殊的二叉搜索树,它们可以自动保持树的平衡,从而保证在插入、删除和查找操作上的效率。红黑树就是平衡二叉搜索树的一种。

在C++的标准模板库(STL)中,mapmultimapsetmultiset等关联容器的实现通常使用红黑树作为底层数据结构。红黑树能够在每次插入和删除操作后自动调整树的结构,保证树的高度大致平衡,这就保证了在最坏的情况下,查找、插入和删除操作的时间复杂度都是O(log n),其中n是树中元素的数量。

需要注意的是,虽然红黑树是一种常用的平衡二叉搜索树的实现,但并不是唯一的实现。AVL树、2-3树、B树等都是平衡二叉搜索树的不同实现,它们有各自的特性和适用场景。

补充:

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

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

在这里插入图片描述

5.总结

当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。

如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!

1.有效字母异位词

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

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

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

说明: 你可以假设字符串只包含小写字母。

什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。这句话很重要,在做哈希表题目都要思考这句话。

思路:

比较直接的思路是暴力遍历,遍历两个字符串

本题目中。字符串都是小写字母组成的,意味着a–z,而且字母的ASCII码是连续的,因此可以使用数组存放a–z,a对应数组0的位置,z对应数组25的位置

用数组统计第一个字符串里每一个字母出现的频率,再统计第二个字符串每个数组出现的频率,再做减法。

最后哈希数组内所有元素都是0的话,说明这两个字符串就是有效字母异位词。如果数组里出现-1,+2等,说明有多出来的字母。

注意:在哈希值比较小的情况下,也就是范围比较小(范围可控)的情况,用数组即可;如果数值很大就用set,如果k对应value的话就用map。

伪代码

//定义一个哈希数组
int hash[26];
//默认数组值都是0
//用数组统计第一个字符串出现次数
for(i=0;i<s.size();i++){
    hash[s[i]-'a']++; //这一步的意思是字母a到下标为0做了映射
    //此处不需要知道a的ASCII码,只需要减下去,0对应a,1对应b。
    //字母做计算的时候会自动用ascii码计算,这样的话a就对应到hash数组下标0的位置了
    //这句代码的作用是字符串s中每个字母出现的频率都统计在哈希表里面
}

    //当我们遍历下一个字符串t的时候,不需要定义新数组,在这个数组基础上再做--的操作就可以了。
for(i=0,i<t.size(),i++){
    hash[t[i]-'a']--; //直接减掉就行
}

//如果哈希数组内所有元素都是0,那么就是有效数组异位词
for(i=0;i<hash.size;i++){
    if(hash[i]!=0){
        return false;
    }  
}
return true; //循环结束都没有return,就可以说明是全0数组

hash[s[i]-'a']++是用来统计字符串 s 中每个小写字母出现的次数

s[i]-'a' 的结果是一个介于0和25之间的整数,它表示字母 s[i] 在字母表中的位置(假设 s[i] 是一个小写字母)。比如说,如果 s[i] 是 ‘a’,那么 s[i]-'a' 的结果就是0;如果 s[i] 是 ‘b’,那么 s[i]-'a' 的结果就是1,以此类推。

hash[s[i]-'a']++ 这句代码的意思是将对应字母的计数加一也就是让对应字母代表的下标++。这是通过对 hash 数组中对应位置的元素进行自增(++)操作来实现的。自增操作 ++ 会使得操作数的值增加1。

所以这段代码整体的作用就是遍历字符串 s,然后统计每个小写字母出现的次数,并将结果存储在 hash 数组中。

遇到题目,感觉可以使用哈希法的话,先看看能不能用数组。如果哈希值比较大就不能用数组,需要用set容器。但是能用数组还是用数组

完整版

class Solution {
public:
    bool isAnagram(string s, string t) {
        int record[26]={0}; //注意数组的初始化方式
        for(int i=0;i<s.size();i++){ //访问字符串大小也可以直接用s.size()
            record[s[i]-'a']++;
        }
        for(int i=0;i<t.size();i++){
            record[t[i]-'a']--;
        }
        for(int j=0;j<26;j++){
            if(record[j]!=0){
                return false;
            }
        }
        return true;

    }
};

2.两个数组的交集

本题目中,如果没有<1000的数值限制,推荐还是使用set来做。因为这个int可能非常大,可能上亿,就很难用数组做哈希映射,数组下标放不了那么大的数。

本题目中加了<1000的限制,因此相对来说数组好一些。先用set做一下练习。

哈希表适用于解决什么样的题目?

哈希表擅长解决:**给出一个元素,判断是否在这个集合里出现过。**遇到这种场景,就需要第一个考虑哈希表。

具体使用哪个哈希表,数组set还是map,需要具体分析。

如果数值很大,上限很高,用数组就不合适了。如果数值不是很大,但是数值分布很分散,例如一个0一个5一个1000000,那么这种情况如果用数组下标来映射,就要开一个1000000的数组,这样过于浪费存储空间。那么这种情况下,也是用set比较合适的。

思路

num1转化为哈希表,里面所有元素放到哈希表里,再去遍历num2查询每一个元素去哈希表里查是否出现过。如果出现过,最终放到result集合里。注意集合是去重的

还有一个难点是选择哈希结构,这道题目就使用set来解决。实际上限制大小1000的话数组更好。

3个set结构的选择

本题目中使用**unorderd_set,因为他做映射的时候效率是最高的,做取值操作的时候效率也是最高的**。具体可见基础部分。(因为另外两个底层是红黑树,取值的时候还需要查找的过程。)

std::unordered_set:这是一个集合类型的容器,它存储唯一的元素,并且元素的存储没有任何特定的顺序。它的内部实现是基于哈希表的,所以其查找,插入,删除的时间复杂度都是接近O(1)。但是由于哈希表的特性,它不支持顺序遍历。

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

伪代码

unordered_set<int> result;  //做去重,这个集合里100个元素2,最后也只有一个2
//这种set结构直接帮我们做去重操作

unordered_set<int> num_set(nums1.begin(), nums1.end()); 
//注意可以直接做初始化,把数组转变成unordered_set里面的存储结构

//nums1已经放进哈希表了
//再用nums2进行一个查询操作,遍历nums2
for(i=0;i<nums2.size();i++){
    if(num_set.find(nums2[i])!=nums_set.end()){
        result.insert(nums2[i]); //不需要去重,会自行去重
    }
}
return vector(result); //vector容器可以直接把result放在


set容器用法补充

这里**unordered_set<int> num_set(nums1.begin(), nums1.end());是创建一个无序集合num_set**,并将nums1数组的所有元素放入该集合中。

接着在for循环中,遍历nums2数组,每次取出一个元素nums2[i]

num_set.find(nums2[i])是在num_set集合中查找元素nums2[i]如果找到该元素,find方法返回一个迭代器,该迭代器指向找到的元素;如果没有找到该元素,find方法返回num_set.end()

所以if(num_set.find(nums2[i])!=num_set.end())的意思就是:如果在num_set集合中找到了nums2[i]这个元素,那么将这个元素插入到result集合中。

最后,result集合就是nums1nums2的交集,而且元素不重复。

在这里插入图片描述

完整版

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int>result;
        unordered_set<int>nums_set(nums1.begin(),nums1.end());
        //可以使用数组的begin和end直接放进set容器里
        
        //遍历nums2
        for(int i=0;i<nums2.size();i++){
            if(nums_set.find(nums2[i])!=nums_set.end()){
                result.insert(nums2[i]); //set的insert用法
            }
        }
        //返回result,直接使用vector的容器转换
        return vector<int>(result.begin(),result.end());

    }
};
 for(int i=0;i<nums2.size();i++){
            if(nums_set.find(nums2[i])!=nums_set.end()){
                result.insert(nums2[i]);
            }
        }

能不能改成

while(nums_set.find(nums2[i])!=nums_set.end()){
                result.insert(nums2[i]);
      }        
问题1:万一元素里有有负数,或者非整数,还适用吗?

改下类型参数就行了,比如unordered_set<double>就支持浮点数了。

问题2:set容器find()函数的用法

不可以将这个for循环直接替换为while循环,因为while循环缺少遍历nums2数组的部分。如果直接替换成while循环,程序将无法正确遍历nums2数组的所有元素,而且会陷入死循环。

在 C++ STL 中,unordered_set::find 函数只要找到了一个与给定值相等的元素,就会返回一个指向该元素的迭代器。如果在集合中没有找到相等的元素,那么 find 函数就会返回 unordered_set::end,这是一个特殊的迭代器,表示集合的末尾,也就是集合中的“不存在”的位置。

所以,if(nums_set.find(nums2[i])!=nums_set.end()) 这一句的意思就是“如果在集合 nums_set 中找到了一个nums2[i] 相等的元素”。如果找到了,就把这个元素插入到 result 集合中。

这一操作并不会影响 nums_set 中的其他元素,也就是说,即使在 nums_set 中找到了一个与 nums2[i] 相等的元素,find 函数也不会继续查找其他元素。所以,find 函数在找到第一个相等元素后就会返回,而不是找到所有相等元素后再返回。

问题3:vector容器能否支持直接转换其他类型的数据?

本题目中,unordered_set的begin()和end()成员函数返回的迭代器可以直接用于构造一个vector

**你可以将任何有begin()和end()成员函数的容器类型转换为vector,这是因为vector的构造函数接受两个迭代器,它们分别表示要复制的元素范围的开始和结束。**在这个代码中,unordered_set的所有元素被复制到了一个新的vector中。

注意: unordered_set和vector有一些重要的区别。例如,unordered_set中的元素是唯一的并且没有特定的顺序,而vector中的元素可以重复,并且保持插入顺序。所以在转换时,可能会丢失一些原始数据结构的特性。在这个例子中,unordered_set被转换为vector是安全的,因为我们只关心哪些元素存在,而不关心它们的顺序或是否重复。

(几乎所有的STL容器都有begin()和end()成员函数,这些函数返回指向容器中元素的迭代器。)

return vector<int>(nums.begin(),nums.end());
有begin()和end()成员函数的stl容器包括什么?

C++的标准模板库 (STL) 提供了一系列用于存储和操作数据的容器。几乎所有的STL容器都有begin()和end()成员函数,这些函数返回指向容器中元素的迭代器。

以下是一些主要的STL容器:

  1. 序列容器:这些容器用于存储元素的有序集合。
    • vector: 动态数组,支持快速的随机访问。
    • deque: 双端队列,支持在头部和尾部进行插入和删除操作。
    • list: 双向链表,支持快速的插入和删除操作。
    • forward_list: 单向链表。
    • array: 静态数组,支持快速的随机访问。
  2. 容器适配器:这些容器在其它容器的基础上提供特定的接口。
    • stack: 栈,提供后进先出(LIFO)的数据管理。
    • queue: 队列,提供先进先出(FIFO)的数据管理。
    • priority_queue: 优先队列,元素的出队顺序基于元素的优先级。
  3. 关联容器:这些容器用于存储键值对或者有序集合。
    • set: 集合,包含的元素都是唯一的。
    • multiset: 多重集合,允许元素重复。
    • map: 映射,包含的是键值对,键是唯一的。
    • multimap: 多重映射,允许键重复。
  4. 无序关联容器:这些容器用于存储键值对或者无序集合。
    • unordered_set: 无序集合,包含的元素都是唯一的。
    • unordered_multiset: 无序多重集合,允许元素重复。
    • unordered_map: 无序映射,包含的是键值对,键是唯一的。
    • unordered_multimap: 无序多重映射,允许键重复。

注意,尽管容器适配器stack, queue和priority_queue没有直接提供begin()和end()成员函数,但是它们底层使用的容器(通常是dequelist)是有这些函数的,你可以间接地通过访问这些底层容器来使用begin()和end()。

补充1:

直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的

不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。

补充2:set的key含义

在C++ STL中,unordered_set是一个集合类型,它存储的元素都是唯一的,也就是说同一个值只能在集合中出现一次。unordered_set内部使用哈希表来存储元素,这使得查找、插入和删除操作的平均时间复杂度都能达到O(1)。

unordered_set元素本身就是哈希表的key。也就是说,当你插入一个元素时,这个元素的值就是它的key。为了把元素存入哈希表,unordered_set会使用一个哈希函数来计算元素值的哈希值,然后根据这个哈希值来决定元素在哈希表中的位置。

对于unordered_set<int> num_set(nums1.begin(), nums1.end());这段代码,nums1数组中的每一个元素都会被插入到num_set集合中,并且作为哈希表的key。如果数组中有重复的元素,那么在集合中只会存储一次。

所以,set的key实际上就是你插入的元素值unordered_set使用哈希函数来计算这个key的哈希值,然后根据哈希值把元素存储到哈希表的合适位置。

补充3:map的key含义

对于setunordered_set这类容器,插入的元素本身就是key因为setunordered_set只存储单一的元素值,并且保证每个元素值在集合中唯一。

但对于mapunordered_map这类键值对容器,key和value是分开的。当你插入一个键值对(pair)到mapunordered_map时,你需要提供一个key和一个对应的value。mapunordered_map中,key用来唯一标识一个元素,而value则是与key关联的数据。

std::map<int, std::string> m;

// 插入键值对
m[1] = "one";
m[2] = "two";
m[3] = "three";

// 这样,键为1的元素的值就是"one",键为2的元素的值就是"two",键为3的元素的值就是"three"。

所以,对于setunordered_set,key就是元素值,而对于mapunordered_map,key和value是分开的,需要分别指定。

数组的解法:

数组解法是针对力扣后面修改了数组值范围的解法,力扣修改后的结果如下
在这里插入图片描述
要是没有这个限制,数组解法就不行了。

问题:

q: 有这个限制,负数确实不可以,非整数呢?数组下标可以是非整数吗?

a: 负数和浮点都不行,数组下标只能是非负整数

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值