算法力扣刷题记录 十四【349. 两个数组的交集】及unordered_set用法

前言

哈希表结构篇,第二题。
记录十四:力扣【349.两个数组的交集】
加油,继续。


一、 题目阅读

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

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

提示:

1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000

二、第一次尝试

思路

  • 从题干看,两点要求:唯一(不重复)、不在乎顺序。
  • 函数返回值类型:vector< int >。

那么:背后意思——看nums2中这个元素有没有在num1中出现过。

(1)直白方法:外层遍历nums2,内层遍历nums1,如果相等,加入result。最后再检查result中有元素重复没有。

(2)继上一篇,还有方法:哈希法。根据两点:唯一、无序。可以选择哈希结构unorderded_set。(可能有的同学不知道unorderded_set如何使用,跳转最后 “补充” 章节学习后,可以在回过头继续读。)

  • 定义unordered_set set1;//无序,唯一,放较长数组的不重复元素
  • 交集的最大长度是较短的数组长度。假设nums1的长度小于nums2的长度:
  • 第一步:往set1里面insert (nums1[i])。把nums1中所有不重复的元素放入set1.
  • 第二步:遍历nums2,使用set1.find(nums2[j]),看能否找到,如果返回不是set1.end(),代表找到。那么该元素需要放到交集里面。
  • 第三步:因为举例函数的返回值类型是vector< int >,如果直接往vector中放元素,还需要判断有没有重复元素。所以额外定义unordered_set set2,把元素insert到set2,最后遍历set2,把set2的元素push_back到vector< int >。

代码实现(通过测试)

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        vector<int> Intersection;//先把返回值摆上
        unordered_set<int> set1;//无序,唯一,放较长数组的不重复元素
        unordered_set<int> set2;//交集

        if(nums1.size() < nums2.size()){
            for(int i = 0;i < nums1.size();i++){
                set1.insert(nums1[i]);
            }

            for(int j = 0;j < nums2.size();j++){
                auto result = set1.find(nums2[j]);
                if(result  != set1.end()){//找到
                    set2.insert(nums2[j]);
                }
            }
        }else{
            for(int i = 0;i < nums2.size();i++){
                set1.insert(nums2[i]);
            }
            for(int j = 0;j < nums1.size();j++){
                auto result = set1.find(nums1[j]);
                if(result != set1.end()){
                    set2.insert(nums1[j]);
                }
            }
        }

        //把set2中的元素取出来放到vector<int> Intersection中返回
        for(int x:set2){
            Intersection.push_back(x);
        }
        return Intersection;
    }
};

修正:说往set1中放较短的nums,其实先放入哪个都可以。
那么if就可以去掉,合二为一,代码更短

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        vector<int> Intersection;//先把返回值摆上
        unordered_set<int> set1;//无序,唯一,放较长数组的不重复元素
        unordered_set<int> set2;//交集

        
            for(int i = 0;i < nums1.size();i++){
                set1.insert(nums1[i]);
            }

            for(int j = 0;j < nums2.size();j++){
                auto result = set1.find(nums2[j]);
                if(result  != set1.end()){//找到
                    set2.insert(nums2[j]);
                }
            }
        

        //把set2中的元素取出来放到vector<int> Intersection中返回
        for(int x:set2){
            Intersection.push_back(x);
        }
        return Intersection;
    }
};

三、代码随想录

学习内容

(1)用unordered_set来解决问题。
(题外话:“把xxx转换成哈希表结构”,听着可能很复杂,其实就是借助容器:数组、set、map这种结构,利用成员函数来操作,更底层实现可以暂时不关心。平时可能见vector多,其余的都是类似。可以记住常用的成员函数即可。)

改进

  • 我把nums1每个元素一个一个insert,其实可以用unordered_set构造函数带参,直接构建:

    unordered_set set1(nums1.begin() , nums1.end()); //range范围构造函数
    unordered_set set1;//定义了空set

  • 省略result,直接if(set1.find(nums2[j]) != set1.end()),少了一行。

(2)使用数组
提示中:1 <= nums1.length, nums2.length <= 1000。有界。
数组:直接把下标当作数值的key。
思路:nums1出现过的元素,值=索引,对应数组元素记1.用nums2判断这个值是否为1.

哈希结构:要是考虑效率(速度),看底层实现。


总结:

(1)不要听到哈希就陌生,认为就是个叫法(不专业,但理解下)。其实更多定义vector、set、map容器,调用成员函数,记录常用函数。
(2)选择结构的方法:就是容器的特点。


补充unordered_set

用到容器:unordered_set,所以学习下<unordered_set>中常用的成员函数。
(可以直接学习https://cplusplus.com/reference/unordered_set/unordered_set/,看函数原型)

定义

template < class Key,                        // unordered_set::key_type/value_type  
           class Hash = hash<Key>,           // unordered_set::hasher          
           class Pred = equal_to<Key>,       // unordered_set::key_equal        
           class Alloc = allocator<Key> >    // unordered_set::allocator_type          
class unordered_set;

解释:

  • 本质是一个模板类。实例化创造对象,有了对象就可以调用成员函数。

  • 模版参数,定义了4个。叫“类型占位符”,初始化的时候执行,那么该占位符都被替换掉。

    • 第一个:Key——元素的类型。往unordered_set里面放什么东西,也就是哈希函数的参数。
      比如:unoerdered_set<int> set1;
      	   unoerdered_set<char> set2;
      	   unoerdered_set<Myclass> set3;	//Myclass是我自己定义的一个类
    
    • 第二个:Hash——哈希函数。如果自己定义了哈希函数,可以把函数名(指向函数的指针)放在这。如果没有,就用人家默认的,出现碰撞概率1.0/std::numeric_limits<size_t>::max(),认为极低就可。

      如果真的自己定义了哈希函数:size_t hashFunction(Key),一个参数,类型是Key,返回值是size_t。
      
    • 第三个:Pred——判断Key相不相等。知道unordered_set元素唯一,怎么判断两个Key相等,默认是"==",如果有自己的相等定义,就把自己的函数名放在这。

        如果真的自己定义比较方式:bool operator()(const Key& ,const Key&){};
        函数重载(),给两个类型Key的参数,返回值bool类型。
      
    • 第四个:Alloc——内存分配模型。基本还用不到,默认就是最简单的内存分配模型。

总结:你需要知道:
(1)unordered_set是一个类模版,定义时一般会指定第一个模版参数——set用来放什么类型的东西。
(2)更进一步,自定义哈希函数放到第二位,自定义判断相等函数放到第三位。

特点

  • 问:什么时候用unordered_set?
    答:唯一、不重复、无序。见到这些字眼,可考虑定义unordered_set。
  • 问:怎么区分元素唯一?
    答:Key这个地方给的参数不一样。

构造函数

用来实例化对象的。一般构造函数原型很多,主要是给的参数不一样。初始化之后的set里面有什么。(后面的Hash,Pred,Alloc也可以给,但这不属于本文适合对象,更高级的用法以后再说)

  • 默认、不给参数时:这个set里面没有元素,空的。比如:

      unordered_set<int> set1;//空的
    
  • 给两个参数:类型是InputIterator,比如容器调用begin()和end()。把[第一个参数,第二个参数)范围内的元素复制到新建的set中。比如:

      vector<int> nums;或者list<int> nums;//只要有迭代器就好。
      unordered_set<int> set2(nums.begin(),nums.end());//别的容器类型转到unordered_set类型中,元素一样,不改动,但是去重。
    
  • 给另外一个unordered_set对象。调用copy构造函数。创建一个和参数一模一样的set。比如:

      unordered_set<int> set3(set2);
    
  • 直接用列表初始化。比如:

      unordered_set<string> set4({"blue","green","red"});//这样set4就已经有了3个元素。
    

总结:到此四种方式足够当前阶段使用。(更多move构造函数传参右值引用先不说。)

运算符=

也是用来创建一个unordered_set。但是用“=”。比如:

上面第3条改成:unordered_set<int> set3 = set2;//一个作用
上面第4条改成:unordered_set<string> set4={"blue","green","red"};//没区别,意思是一样的

析构函数

在作用域结束时,一般自动调用析构函数,先调用元素类型的析构函数,再释放set的内存。

获取大小

以下的set1是我定义的一个unordered_set。

set1.empty();//如果set1空,返回true;不空,有元素,返回false。没有参数,返回类型bool
set1.size();//没有参数,返回类型size_type一个无符号整数。得到set1有几个元素。
set1.max_size();//没有参数,返回set1最多能放多少元素。

总结:和其他容器一样含义。很好理解

迭代器

set1.begin();//没有参数,返回值类型iterator。指向set1的第一个元素。没有哈希冲突时,一个索引只放一个元素,用这个就可以。
set1.begin(n);//传一个整数(索引),返回类型local_iterator。指向bucket的第一个元素,这是有哈希冲突,一个索引放了多个元素,我需要遍历链表。
for(auto it = set1.begin(2);it != set1.end(2);++it)

总结:提前说:哈希表单位是bucket,认为是一个桶,就是之前说的用链表来存放多个元素解决哈希冲突问题。begin一个传参一个不传参

set1.end();//没有参数,返回值类型iterator。指向set1的最后一个元素的后面。没有哈希冲突时,一个索引只放一个元素,用这个就可以。
set1.end(n);//传一个整数(索引),返回类型local_iterator。指向bucket的最后一个元素的后面,这是有哈希冲突,一个索引放了多个元素,我需要遍历链表。

总结:指向最后一个元素的后面,不指向任何元素。

set1.cbegin();
set1.cbegin(n);
set1.end();
set1.end(n);
//这一部分可以不用在意。有上面的begin和end足够了。
//解释:cbegin也是指向unorderded_set的第一个元素。cend指向unorderded_set的最后一个元素的后面。
虽然cbegin函数原型上明确const_,begin有不带const_的原型,但是unordered_set::begin介绍中说明在unordered_set类中所有iterator都不能修改它指向的元素,只能用来增加/删除元素。所以我说可以不用在意。

总结

  • 记住begin()-end();begin(n)-end(n)这两对即可。
  • 不能用返回值来修改元素。因为是const所以不能修改。

查找元素

哈希结构就是根据索引找元素更高效,所以有几个成员函数用来查找元素。

  • find

      iterator find ( const key_type& k ); 
      const_iterator find ( const key_type& k ) const;
      这两个没有区别,因为上面说过iterator加不加const_,都不能用来修改元素。
      所以使用时:传参一个 输入x(要找的元素),
      作用是:找到x返回指向x的iterator,如果找不到就返回set.end()所指向的位置。
      示例:
      	假设unordered_set<int> set1中有元素23,没有元素26.
      	auto it = set1.find(23);//it != set1.end();,因为it指向元素23所在的索引。
      	auto it = set1.find(26);//it == set1.end();,因为找不到,it指向最后一个元素的后面。
    
  • count

      函数原型:size_type count ( const key_type& k ) const;
      作用:看unordered_set中有几个元素是k?
      但是我们知道unordered_set不让重复,所以也用来判断元素k在不在unordered_set中,如果在,返回值是1;如果不在,返回值是0.
      示例:
      	沿用find的示例假设:
      	set1.count(23) == 1;//找到23,只有它独苗,所以返回值判断等于1,说明unordered_set中有它。
      	set1.count(26) == 0;//因为找不到26,所以返回值判断等于0,说明unordered_set中没有k。
      
      另外,触类旁通:我们可以想到multiset中肯定也有count函数,因为multiset允许重复元素,那么可以计数k在multiset中有几个。
    
  • equal_range

      pair<iterator,iterator>   equal_range ( const key_type& k );
      pair<const_iterator,const_iterator>   equal_range ( const key_type& k ) const; //合二为一:加不加const_无所谓,两个原型,用法一样。
      
      //解释<pair>:
      	(1)知道这个数据类型struct,在#include <utility>。
      	(2)定义一个pair对象:pair<int,string> pair1(2,"apple");//< >中填什么视情况而定。
      	(3)获取成员:pair1.first取到int这个数;pair1.second取到string这个数;其余同理。
      	(4)swap():交换两个pair的内容。比如:pair<int,string> pair2(3,"banana"); pair2.swap(pair1);//这样pair2就变成(2,"apple"),pair1就变成(3,"banana")。
      
      //直到返回值类型之后,回归equal_range:示例:
      pair result = set1.equal_range(23);//23就是传的参数。
      result.first指向23的第一个索引,result.second指向23的最后一个索引的后面。[result.first,result.second)这个范围内都是23.
      pair result = set1.equal_range(26);//找不到26,那么result.first和result.second都等于set1.end()
      
      那我们知道unordered_set中元素不重复,调用equal_range,如果result.first != set1.end(),说明存在元素。如果相等,就不存在,可以用来判断元素是否存在。
      另外,可以想到multiset中允许元素重复,调用equal_range,[result.first,result.second)这个范围内都是给equal_range传的参数。
    

总结:find(k); count(k); equal_range(k);

  • 从名字,知道这三个作用不一样,find是真正的找k存不存在。其余两个是因为unordered_set不允许重复,借来用。
  • 但是根据返回值类型可以if不同的条件:
    • iterator it = set1.find(k);
      if(it != set1.end()) //找到k
    • int result = set1.count(k);
      if(result != 0) //找到k
    • pair result= set1.equal_range(k);
      if(result.first != set1.end()) //找到k

修改unordered_set中元素

迭代器可以指向unordered_set 中的元素,但是不能通过*it 修改元素。那么怎么往unordered_set中添加、删除、清空、修改元素呢?

  • 插入:只有当unordered_set中没有这个元素,才可能插入成功。插入成功之后,size+1.

    • insert(常用)

        有多个原型,根据情况选择合适参数。
        第一组:
        	pair<iterator,bool> insert ( const value_type& val );	//主要用这个
        	iterator insert ( const_iterator hint, const value_type& val );	//hint指向unordered_set中某个元素的位置,想在hint之后开始找插入元素放的地方,但是编译器优化可能采纳也可能不管hint。所以:传递hint是一回事,编译器听不听是另一回事。
        	所以多用第一个:插入成功,返回值pair.first指向该插入元素,pair.second=true;插入不成功,返回值pair.first指向unordered_set中已有元素的位置,pair.second=false。
        
        第二组:
        	pair<iterator,bool> insert ( value_type&& val );
        	iterator insert ( const_iterator hint, value_type&& val ); //解释同上
        	主要看第一个:和第一组有什么区别呢?这里的参数是一个表达式(右值),value_type&& val没有保留自己;而第一组:const value_type& val插入后,容器中的元素是val的副本。
        	
        	比如:
        	std::unordered_set<std::string> myset = {"yellow","green","blue"};
        	std::string mystring = "red";
        	 myset.insert (mystring);             // copy insertion,第一组第一个。插入元素:red
        	 myset.insert (mystring+"dish");    // move insertion ,第二组第一个。插入元素:reddish
      
        第三组:
        	template <class InputIterator> void insert ( InputIterator first, InputIterator last );
        	和构造函数第二点一个意思,给一个范围。
        	比如:
        		std::array<std::string,2> myarray = {"black","white"}; //array容器也有iterator
        		myset.insert (myarray.begin(), myarray.end());
        		
        第四组:
        	void insert ( initializer_list<value_type> il ); //给一个初始化列表
        	myset.insert ( {"purple","orange"} );
      

      总结
      (1)插入一个元素时,并且判断有没有成功插入,用第一组第一个,返回值pair类型;
      (2)插入一个表达式算出来的元素时,并且判断有没有成功插入,用第二组第一个,返回值pair类型;
      (3)插入多个,但是元素目前在别的容器类型中,比如array,list,用第三组,传递范围没有返回值
      (4)插入多个,用初始化列表,用第四组,没有返回值

  • 删除
    当成功删除,调用元素类型的析构函数,并且unordered_set的size减1。

      原型一:iterator erase ( const_iterator position ); 给一个位置,删除成功,返回iterator指向最后一个被删元素的后一个位置。
      	比如:set1.erase(set1,begin());//但因为unordered_set无序,所以不能确定删的是哪个。
      
      原型二:size_type erase ( const key_type& k );给指定元素,返回值是删除了几个k;unordered_set不重复,所以成功删除返回1;不成功,删除不存在的元素,返回0.
      	比如:set1.erase(k) ==1;//删除k,并且k只有1个可删,所以返回1.
      	
      原型三:iterator erase ( const_iterator first, const_iterator last ); 给一个范围,[first,last)范围内都删掉。删除成功,返回iterator指向最后一个被删元素的后一个位置。
      	比如:set1.erase(set1.find(3),set1.find(5)); //给了范围,但是因为无序,不知道下面究竟是如何分布的。
    

    总结:给位置、指定元素、范围都可以删除。但要知道无序,确定好位置或范围。

  • 清空

      set1.clear();//清空,size=0,没有参数,没有返回值。
    
  • 和另一个unordered_set交换元素。

      void swap ( unordered_set& ust );
      必须是同类型。size可以不一样。没有返回值。
      可以set1.swap(set2);
      也可以swap(set1,set2); //这用的是std::swap下的函数。作用一样。
    

Buckets

前面迭代器提到哈希表真正的单位是bucket。bucket不一样代表哈希函数输出的值不一样,索引不一样。容器自动增加 bucket 的数量,每次增加 bucket 数量都导致重新哈希。

  • 计数Buckets

      size_type bucket_count() const noexcept;
      使用:不传参,返回bucket的数量。
      示例:set1.bucket_count();
    
  • 每个bucket中有几个元素

    size_type bucket_size ( size_type n ) const;
    作用:知道第n个bucket中有几个元素。
    示例:int num = set1.bucket_size(n);//返回一个整数。
    
  • 想知道某个元素在哪个bucket?

      size_type bucket ( const key_type& k ) const;
      作用:bucket计数从0开始到bucket_count-1。可以知道某个元素在哪个bucket编号内,如果在同一个bucket,就说明有冲突,放到同一个bucket中。
      示例:
      	unordered_set<string> set1={"apple","banana","orange"};
      	int num = set1.bucket("apple"); //元素"apple"在哪个bucket呢?num可以获取。
    

汇总

定义——构造——获取大小——迭代器——查找元素——修改元素——bucket;触类旁通,set和multiset肯定也有这些。
选定哈希结构之后,可以调用成员函数来进行操作,因为访问高效,所以才用它们。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值