C++学习笔记-哈希表

资料来源:代码随想录

1.哈希表的理论基础

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

比如:数组,关键码就是数组的索引下标。

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

哈希函数:把其他数据格式转化为不同的数值,映射为哈希表上的索引数字。

哈希碰撞:不同的数值映射到同一个索引下标的位置。

解决方法:

(1)拉链法:发生冲突的元素被存储到链表中。

(2)线性探测法:需要保证tablesize>datasize,用空位来存放冲突的元素。

三种常见的哈希结构:数组、set(集合)、map(映射)。

注意:使用数组来做哈希题目时,需要题目限制数值的大小

set有三种可用的数据结构:

  • std::set
  • std::multiset
  • std::unordered_set

 

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

2.有效的字母异位词 242

定义一个record数组(哈希表)用来记录字符串s里字符出现的次数,均初始化为0。

把字符映射到哈希表上。最多有26个字符,所以tablesize=26即可,即哈希表的索引下标从0-25.

在遍历字符串s的时候,只要将s中各元素对应的哈希表中的值+1即可;在遍历字符串t的时候,再将t中各元素对应的哈希表中的值-1.

最后检查:record数组中如果有元素不为0,说明s和t一定是谁多或谁少了字符,return FALSE;元素均为0的话,说明是字母异位词,return TRUE。

class Solution {
public:
    bool isAnagram(string s, string t) {
        int record[26]={0};    //初始化一个有26个空位的数组,初值为0

        for(int i=0; i<s.size(); i++)
        {
            record[s[i]-'a']++;  //不用ASCII码,直接用相对位置即可
        }

        for(int j=0; j<t.size(); j++)
        {
            record[t[j]-'a']--;
        }

        //开始检查record
        for(int i=0; i<26; i++)
        {
            if(record[i]!=0)     //只要有一位数字不为0,就不是异位词
            {
                return false;
            }
        }

        //每一位都为0
        return true;
    }
};

3.两个数组的交集 349

学会std::unordered_set这种数据结构的使用。

它可以用来存放数据,不需要对数据进行排序,且可以让数据不重复

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

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set;  //定义一个无序集合来存放结果,这样可以给结果去重,满足“输出结果每个数都唯一”的要求
        
        unordered_set<int> nums_set(nums1.begin(),nums1.end());
        //把数组nums1中的元素存放进一个无序集合num_set里,给数据去重

        for(int num : nums2) //冒号前是实例化一个集合中包含的元素,冒号后是要遍历的集合
        //意思是遍历nums2集合中的每一个元素
        {
            if(nums_set.find(num)!=nums_set.end())
            //在nums_set集合中寻找nums2集合中的num元素,返回的结果不是nums_set的末尾
            //说明在nums_set集合中找到了这个元素
            //如果是末尾,说明走了一遍都没找到这个元素
            {
                result_set.insert(num); //把找到的元素放到结果集合里
            }
        }

        return vector<int> (result_set.begin(),result_set.end());  //返回交集
    }
};

上面这种做法是用集合做,数据量有限的话还可以用数组做:

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set;

        int hash[1000]={0};  //定义一个数组,数组长度最大为1000

        for(int num : nums1)
        {
            hash[num]=1;  
        }
        //如果一个num在nums1中出现,在哈希表中把对应位置的数改成1

        for(int num : nums2)
        {
            if(hash[num]==1)
            {
                result_set.insert(num);
            }
            //如果nums2中的元素num在哈希表中对应位置的数为1,说明这个元素在nums1中也出现过
            //即为相交的元素,放入结果集合
        }

        return vector<int>(result_set.begin(),result_set.end());
    }
};

4.快乐数 202

class Solution {
public:
   
    int getSum(int n)
    {
        int sum=0;
        while(n)   //一直循环
        {
            sum=sum+(n%10)*(n%10);
            n=n/10;  //先是个位平方,然后是十位,再然后是百位...
        }
        return sum;
    }
    
    bool isHappy(int n)
    {
        unordered_set<int> set;  //放结果
        while(1)
        {
            int sum=getSum(n);  //从上面接收一下计算好的sum
            if(sum==1)
            {
                return true;
            }
            if(set.find(sum)!=set.end())  
           //能找到sum这个数,说明这个数之前出现过,所以已经出现循环了
            {
                return false;
            }
            else
            {
                set.insert(sum);
            }

            n=sum;  //继续下一轮计算
        }
    }
    
};

 5.两数之和 1

因为在本地,我们不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适

再来看一下使用数组和set来做哈希法的局限。

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

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

 这道题目中并不需要key有序,选择std::unordered_map 效率更高! 

思路:

在遍历数组的时候,向map查询是否有和当前遍历元素所匹配的数值(即相加等于target),如果map中有,就找到匹配对,返回下标值;如果没有,就把当前遍历元素以(key,value)即(数值,下标)的方式存进map,因为map存放的就是我们访问过的元素。

一些其它的知识:

map<X,Y>里存放的实际上是一串pair<const X,Y>

如果用一个指针it来指向map中的元素:auto it=map.begin( );  那么解引用*it将得到map中的第一个元素pair< >。

然后可以再接收pair中的两个元素:

it->first将得到key值,it->second将得到value值。

auto在这里起到自动类型识别的作用。用它来定义一个指针,就不用再去想指针是什么类型的了。

迭代器.find( ),如果没有找到的话,指向的是末尾元素的下一个,即.end( )。迭代器的使用方法和指针类似。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        //先初始化一个map用来存放已经访问过的元素
        std::unordered_map<int,int> map;

        for(int i=0; i<nums.size(); i++)
        {
            //遍历数组元素,并在map中寻找是否有和当前元素匹配的值(即相加=target)
            auto iter=map.find(target-nums[i]);
            //定义一个自动识别类型的指针,指向可能的匹配的值

            if(iter!=map.end())  //找到了
            {
                return {iter->second,i};  //则返回匹配的下标和数组中当前元素的下标
            }
            //没找到:将当前元素存入map,因为map用来存放我们已经访问过的元素
            map.insert(pair<int,int>(nums[i],i));
        }

        return {};
    }
};

本题的四个重点:

(1)为什么会想到用哈希表?

因为我们要在数组中寻找是否有和当前元素匹配的值,即查询一个值是否在数组中出现过,所以要用哈希表。

(2)哈希表为什么用map?

用数组的话,如果数组元素少而哈希表太大,就会造成内存空间的浪费;而set是一个集合,里面只能存放key值,而我们既需要记录元素的值,又需要记录元素的下标,所以用map。

(3)本题map是用来存什么的?

存我们已经访问过的元素的值和下标。

(4)map中的key和value用来存什么?

key用来存元素的值,value用来存元素的下标。

6.四数相加 454

【Leetcode】map[key]++的理解与用法_map[]++_fighting_!的博客-CSDN博客

在unorder_map中,可以使用方括号访问键对应的值map[key] ,map[key]是这个key值对应的value。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。

所以我们可以通过map[key]++来直接创建key且使key对应的value值+1,该句代码可以用在需要计数的情况下。

那么会产生一个问题(是我个人在第一次接触时所产生的问题):用map[key]++可以创建key,那么用map[key]--是不是可以删除key呢?

经验证:不是。通过++来建立键,但不可以通过--来删除键,删除只能试用erase函数来删除键。

思路:我们只需要得到满足条件的元组的数量即可,不需要得到具体的元组值。

首先遍历A和B两个数组,统计两个数组元素之和(作为key值)和出现的次数(作为value值),放到unordered_map中,对结果进行去重。

然后遍历C和D两个数组,在刚刚的map中寻找是否有0-(c+d)这个值,即为满足a+b+c+d=0这个要求的值。如果有的话,说明找到了符合要求的元组,将map中0-(c+d)这个值对应的value赋给count,统计出符合要求的元组的数量。

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> map; 
        //定义一个map来存放nums1和nums2遍历的结果
        //key存放a+b的值,value存放a+b出现的次数

        for(int a : nums1)
        {
            for(int b : nums2)
            {
                map[a+b]++;  //计算a+b的值作为key,并将对应的value加1,。注意结果已经去重
            }
        }

        int count=0;  //统计a+b+c+d=0出现的次数

        for(int c : nums3)
        {
            for(int d : nums4)
            {
                if(map.find(0-(c+d))!=map.end())  //找到了符合a+b+c+d=0的元组
                {
                    count=count+map[0-(c+d)];   //map[0-(c+d)]是这个值对应的value
                }
                
            }
        }

        return count;
    }
};

7.赎金信 383

(1)暴力解法

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        for(int i=0; i<magazine.size(); i++)
        {
            for(int j=0; j<ransomNote.size(); j++)
            {
                if(magazine[i]==ransomNote[j])
                {
                    ransomNote.erase(ransomNote.begin()+j); 
                    //在ransom里找是否有和magazine一样的字符,有的话,删除它

                }
            }
        }

        if(ransomNote.size()==0)  //ransom里的都删完了,说明可以由magazine组成ransom
        {
            return true;
        }
        return false;
        //注意:迭代器用size()和length()的效果是一样的
    }
};

(2)哈希解法

思路:维护一个长度为26的数组,在里面相对应的位置存放进magazine每个字符出现的次数,然后用ransomNote来验证这个数组中是否包含了ransomNote所需要的所有字符。

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int record[26]={0};

        if(ransomNote.size()>magazine.size())
        {
            return false;
        }

        for(int i=0; i<magazine.size(); i++)
        {
            record[magazine[i]-'a']++;  //把magazine每个元素,在record数组中对应的位置加一
        }

        for(int j=0; j<ransomNote.size(); j++)
        {
            record[ransomNote[j]-'a']--;  //把ransomNote每个元素,在record数组中对应的位置减一

            //如果此时record中有数小于0,说明ransom里存在magazine里没有的元素
            if(record[ransomNote[j]-'a']<0)
            {
                return false;
            }
        }

        return true;
    }
};

8.三数之和 15

双指针法。

首先将数组排序。然后定义下标:i从0的地方开始,left从i+1的地方开始,right定义在数组结尾的地方。在数组中找到abc使得a+b+c=0,其中a=nums[i]  b=nums[left]  c=nums[right]。

如何移动指针?如果nums[i]+nums[left]+nums[right]>0,说明三数之和大了,因为数组是排序的,所以right下标应该左移,这样才能让三数之和减小。

如果nums[i]+nums[left]+nums[right]<0,说明三数之和小了,就让left下标右移,直到left和right相遇,说明还是不够大,这时需要移动i。所以有一个关于i的for循环。

去重和双指针同时收缩没懂。

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; 
            }
            //nums[i]是三个数里最小的一个了,如果最小的都大于0,那么和不可能为0,直接返回结果

            //对a去重(没懂)
            if(i>0 && nums[i]==nums[i-1])
            {
                continue;
            }



            //定义双指针
            int left=i+1;
            int right=nums.size()-1;
            while(left<right)  //到left=right还没有=0的话,就该让i加1进下一轮循环了
            {
                if(nums[i]+nums[left]+nums[right]>0)
                {
                    right--;
                }
                else if(nums[i]+nums[left]+nums[right]<0)
                {
                    left++;
                }
                else
                {
                    result.push_back(vector<int>{nums[i],nums[left],nums[right]});
                    //对b和c去重(没懂)
                    while(right>left && nums[right]==nums[right-1])
                    {
                        right--;
                    }
                    while(right>left && nums[left]==nums[left+1])
                    {
                        left++;
                    }

                    //找到答案,双指针同时收缩(没懂)
                    right--;
                    left++;
                }
            }
        }

        return result;
    }
};

9.四数之和 18

看不懂!!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值