【代码随想录】【LeetCode】学习笔记04-哈希表

前言

哈希法牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果遇到需要判断一个元素是否出现过的场景,应该第一时间想到哈希法

  1. 四个常见哈希表类型对比:
集合名底层实现是否有序数值能否重复能否修改数据增、删、查效率
set红黑树有序不重复不能修改O(log n)
unordered_set哈希表无序不重复不能修改O( 1 )
map红黑树key有序key不重复key不能修改O(log n)
unordered_map哈希表key无序key不重复key不能修改O( 1 )

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

  1. set 和 map:
    key的存储方式使用红黑树实现的,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
    虽然set、map 的底层实现是红黑树,不是哈希表,但是它们依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。

  2. undered_map的用法总结:
    来源:【https://www.cnblogs.com/xuelisheng/p/10771961.html】:
    unordered_map的迭代器是一个指针,指向这个元素,通过迭代器来取得它的值。
    它的键、值分别是迭代器的first和second属性。

1 it->first;               // same as (*it).first   (the key value)
2 it->second;              // same as (*it).second  (the mapped value) 
  1. .find() / end()**:
    返回的都是元素的迭代器(键+值)。
    find()如果没找到,返回unordered_map::end。
    括号内的参数是键key,但一般是“重要的信息”设成key,序号设为第二个值value。

  2. map的基本操作:
    来源:【https://www.cnblogs.com/xuelisheng/p/10771961.html】

#include<iostream> 
#include<map>
#include<string>

using namespace std;

int main()
{
	// 构造函数
	map<string, int> dict;

	// 插入数据的三种方式
	dict.insert(pair<string, int>("apple", 2));
	dict.insert(map<string, int>::value_type("orange", 3));
	dict["banana"] = 6;

	// 判断是否有元素
	if (dict.empty())
		cout << "该字典无元素" << endl;
	else
		cout << "该字典共有" << dict.size() << "个元素" << endl;

	// 遍历
	map<string, int>::iterator iter;
	for (iter = dict.begin(); iter != dict.end(); iter++)
		cout << iter->first << ends << iter->second << endl;

	// 查找的2种方法
	if ((iter = dict.find("banana")) != dict.end()) //  返回一个迭代器指向键值为key的元素,如果没找到就返回end()
		cout << "已找到banana,其value为" << iter->second << "." << endl;
	else
		cout << "未找到banana." << endl;

	if (dict.count("watermelon") == 0) // 返回键值等于key的元素的个数
		cout << "watermelon不存在" << endl;
	else
		cout << "watermelon存在" << endl;

	pair<map<string, int>::iterator, map<string, int>::iterator> ret;
	ret = dict.equal_range("banana"); // 查找键值等于 key 的元素区间为[start,end),指示范围的两个迭代器以 pair 返回
	cout << ret.first->first << ends << ret.first->second << endl;
	cout << ret.second->first << ends << ret.second->second << endl;

	iter = dict.lower_bound("boluo"); // 返回一个迭代器,指向键值>=key的第一个元素。
	cout << iter->first << endl;
	iter = dict.upper_bound("boluo"); // 返回一个迭代器,指向值键值>key的第一个元素。
	cout << iter->first << endl;
	return 0;
}

输出:

该字典共有3个元素
apple2
banana6
orange3
已找到banana,其value为6.
watermelon不存在
banana6
orange3
orange
orange
  1. 【字符串】:size()和length()没有区别:
string str="0123456789";
cout <<"str.length()="<<str.length()<<endl;  //结果为10
cout <<"str.size()="<<str.size()<<endl;      //结果为10

//来源:https://blog.csdn.net/z_qifa/article/details/77744482

242. 有效的字母异位词

(仍然存疑,见注释)
重点:小范围的数据集,可以用数组代替哈希表,但要注意数组序号是数字,因此必须用c-‘a’(且不能是’0’)

class Solution {
public:
    bool isAnagram(string s, string t) {
        ///unordered_map<string ,int> umap;
        vector<int> hash(26,0);
        //if(s.size()!=t.size()) return false;这句加上最好
        for(char c: s){
            hash[c-'a']++;
        }
        for(char c: t){
            hash[c-'a']--;
            //if(umap.count(c)){                umap[c]--;            }
            //如果硬要这样,该怎么写?(不知道,见454好像可以这样啊)(见383,貌似是错在unordered_map<string ,int> umap;应该是char,int)
            //else return false;
        }
        for(int i=0; i<hash.size(); i++){//(char c: s){
            if(hash[i]!=0)return false;//umap
        }
        return true;
    }
};

349.两个数组的交集

  1. 知识点巩固:
    限制了数值的大小的题目可以使用数组来做哈希。
    set和multiset底层实现都是红黑树,
    unordered_set的底层实现是哈希表,读写效率是最高的,并不需要对数据进行排序,而且还不会让数据重复,但相比数组耗时、占空间大

  2. 区分unordered_set和unordered_map:https://blog.csdn.net/weixin_45847364/article/details/121654719

vector转成unordered_set:只能在uset构造的时候转:unordered_set <int> uset (vec.begin(), vec.end());
unordered_set转成vector:只能分两步转:(insert, 三个参数)
vector<int> vres; vres.insert(vres.end(), res.begin(), res.end());
或者强制转换vector<int> vres = vector<int>( res.begin(), res.end() );

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set <int> uset(nums1.begin(), nums1.end());
        unordered_set <int> res;
        for(int i: nums2){
            if(uset.count(i))  res.emplace(i);
        }
        vector<int> vres;
        vres.insert(vres.end(), res.begin(), res.end());
        return vector<int>( res.begin(), res.end() );
        // return vres;
    }
};

202.快乐数

在这个class中写了两个函数,第一个用于得到数n每一位上的单数的平方和

必须熟练记住!如何获取一个整数的每一位:
123%10 = 3;
123/10 = 12
12%10=2;
12/10=1
1%10=1;
1/10=0

class Solution {
public:
    int gosum(int n){
        int sum = 0;
        while(n!=0){
            sum+=n%10*(n%10);
            n = n/10;
        }
        return sum;
    }
    bool isHappy(int n) {
        unordered_set<int> uset;
        while(1){
            if(n==1) return true;
            if(uset.count(n)) {
                if(!uset.count(1)) return false;
            }
            uset.emplace(n); 
            n = gosum(n);
          }
        return true;
        
    }
};

1. 两数之和

u_map 和 map 的遍历:
for中三步走+ ->

        for( unordered_map<int, int>::iterator iter = _map.begin(); iter!=_map.end(); ++iter){
            int f = iter->first;
            ...

或者
for中:+.

        for( auto iter : _map){
            int f = iter.first;
            ...

u_map 和 map 的插入:
_map[ni] = i;
或者:
_map.insert(pair<int, int>(nums[i], i));

以上参考【https://blog.csdn.net/sinat_18811413/article/details/120608212】

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        //WAY 1 
        ///不可以,因为要输出答案对应的下标
        sort(nums.begin(), nums.end());
        int left = 0, right = nums.size()-1;
        while(left<right){
            if(nums[left]+nums[right]>target) right--;
            else if(nums[left]+nums[right]<target) left++;
            else return vector<int> {left, right};
        }
        return vector<int> {left, right};
        
        //WAY 2
        //会错在nums = [3,3], target = 6这里:必须先判断再输入nums
        unordered_map<int, int> _map;
        for(int i= 0; i<nums.size(); i++) _map[nums[i]] = i;
        for( unordered_map<int, int>::iterator iter = _map.begin(); iter!=_map.end(); ++iter){
            int f = iter->first;
            if(_map.find(target-f)!=_map.end()) return vector<int> {_map[f], _map[target-f]};
        }
        // for( auto iter : _map){
        //     int f = iter.first;
        //     if(_map.find(target-f)!=_map.end()) return vector<int> {_map[f], _map[target-f]};
        // }

        //WAY 3
        for(int i= 0; i<nums.size(); i++){
            int ni = nums[i];
            if(_map.find(target-ni)!=_map.end()) return vector<int>{i, _map[target-ni]};
           else _map[ni] = i;///或者: _map.insert(pair<int, int>(nums[i], i)); 
        } 
        return {};
    }
};

454.四数相加II

四个不同数组,求目标值对应的下标

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> umap;
        int res = 0;
        //WAY1 3重叠加,错在大数据量的用例上
        for(int l = 0;l<nums4.size(); l++) umap[nums4[l]]++;
        for(int i = 0; i<nums1.size(); i++){
            for(int j = 0; j<nums2.size(); j++){
                for(int k = 0; k<nums3.size(); k++){
                    int tar = 0-nums1[i]-nums2[j]-nums3[k];
                    if(umap.find(tar)!=umap.end()) res += umap[tar];
                }
            }
        }
        ///WAY2 2重+2重,可行!
        for(int a: nums1){
            for(int b: nums2){
                umap[a+b]++;
            }
        }
        for(int c: nums3){
            for(int d: nums4){
                int tar = 0-c-d;
                if(umap.count(tar)) res += umap[tar];
            }
        }
        return res;
    }
};

套路:只记录方法数量总和的题,尽量降低for嵌套层数:用其中两个+已知条件,表示剩下的变量
本题解题步骤:
首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
最后返回统计值 count 就可以了

383.赎金信

因为题目【只有小写字母】,那可以采用一个长度为【26】的数组记录magazine里字母出现的次数。
依然是【【数组在哈希法中的应用】】。

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        ///WAY1 WRONG BUT WHY ?
        //unordered_map<char, int> umap;
        // for(int i=0; i<ransomNote.size();i++) umap[ransomNote[i]]++;或者下一句
        // for(char c: ransomNote){            umap[c]++;        } 
        // for(char c: magazine){
        //     cout<<c;//a 
        //     if(!umap.count(c)) cout<<c;cout<<9;//''9  why???
        //     if(umap[c]==0)return false;
        //     umap[c]--;
        //     //stdout: a9a9bb9
        // }
        //WAY2 RIGHT magazine和ransomNote不要搞反了
        if(ransomNote.size()>magazine.size())return false;
        vector<int> var(26,0);
        for(char c: magazine) var[c-'a']++;
        for(int i = 0; i<ransomNote.size(); i++){
            int magac = ransomNote[i]-'a';
            //cout<<magac<<var[magac]<<endl;
            if(var[magac]==0)return false;  
            var[magac]--;
        }
        return true;
    }
};

15. 三数之和(双指针题目)

  1. 哈希法:
    不推荐
  2. 双指针法:
    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;            }
            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 {
                    result.push_back(vector<int>{nums[i], nums[left], nums[right]});
                    while (right > left && nums[right] == nums[right - 1]) right--;//注意用while剪枝
                    while (right > left && nums[left] == nums[left + 1]) left++;
                    right--;//注意while更新靠【最后】的手动更新!
                    left++;
                }
            }

        }
        return result;
    }
  1. 两方法共同点:1-sort(nums.begin(), nums.end()); 2- 需要去重
    其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。
    而且使用哈希法 在使用两层 for 循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在 leetcode 上通过,但是程序的执行时间依然比较长 。
    注意:两数之和 就不能使用双指针法,因为1.两数之和要求返回的是索引下标, 而双指针法一定要排序双指针法一定要排序双指针法一定要排序,一旦排序之后原数组的索引就被改变了。如果要求返回的是数值的话,就可以使用双指针法了。

18. 四数之和(双指针题目)

双指针法将时间复杂度:O(n ^ k)的解法优化为 O(n ^ (k-1))的解法。也就是降一个数量级
15.三数之和的双指针解法是一层for循环 num[i] 为确定值,然后循环内有left和right下标作为双指针,找到 nums[i] + nums[left] + nums[right] = = 0。
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] = = target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。
那么一样的道理,五数之和、六数之和等等都采用这种解法。

和上一题是完全一样的,建议按下面代码直接背会。
对应题型:对有重复元素值的一个数组,求其中:下标不同的、总数固定、总和固定的元素,的组合的集合。
对应难点:下标不重复的情况下对值去重,不能暴力

sort()函数默认排序为从小到大

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> res;
        if(nums.size()<4)return {};
        sort(nums.begin(), nums.end());
        for(int i = 0; i<nums.size(); i++){
            if (nums[i] > target && nums[i] >= 0 && target >= 0) {break; }
            if(i>0 && nums[i]==nums[i-1]) continue;
            for(int j = i+1; j<nums.size(); j++){
                if (nums[j] + nums[i] > target && nums[j] + nums[i] >= 0 && target >= 0) {break;}
                if(j>i+1 && nums[j]==nums[j-1])continue;//这里要背住:一开始的去重,是“向已遍历方向”去重
                int left = j+1;
                int right = nums.size()-1;
                while(left<right){
                    //long tar = nums[i]+nums[j]+nums[left]+nums[right];//
                    if((long)nums[i]+nums[j]+nums[left]+nums[right]<target) left++;//不懂为什么一定要这样,不能写成上一行的
                    else if((long)nums[i]+nums[j]+nums[left]+nums[right]>target)right--;
                    else{
                        res.push_back({nums[i], nums[j], nums[left], nums[right]});
                        while(left<right && nums[left]==nums[left+1]) left++;//这里要背住:1-就是找到结果以后再去重,2-而且是“向未遍历方向”去重
                        while(left<right && nums[right]==nums[right-1])right--;
                        left++;
                        right--;
                    }
                }
            }
        }
        return res;
    }
};

后面没看

350.两个数组的交集||

class Solution{
    public:
    vector<int> intersect(vector<int>&nums1, vector<int>&nums2) {
        unordered_map<int, int> m;
        vector<int> intersection;
        if (nums1.size() > nums2.size()){
            return intersect(nums2 , nums1);
        }

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

        for (int j : nums2){  //int j 记得初始化, j是nums2的每个值而不是其序号
            if (m.count(j)){
                //count ()函数本质上:依次检查unordered_map中是否存在具有给定键的元素,返回0/1;
                //find()函数本质上:返回找到的或者end的键值对
                m[j]--;
                intersection.push_back(j);  //vector是.push_back(), 哈希表是insert()
            }
            if (m[j] == 0){
                m.erase(j);  //哈希表.erase()
            }
        }
        return intersection;
    }
};

387.First Unique Character in a String

class Solution{
public:
   int firstUniqChar(string s){
       unordered_map<int, int>frequency;
       for (char c : s){
           frequency[c]++;                //① mymap["banana"] = 22;    frequency['a'] = 3
       }

       for(int i =0; i < s.size(); i++){   //多维数组的元素总数用sizeof()
           if (frequency[s[i]] == 1){        //②  注意哈希表的写法
               return i;
           }
       }
       return -1;
   }
};

第二种炫技解法,见识到了for在哈希表中的用法。。就直接CV大法好了

class Solution {
public:
    int firstUniqChar(string s) {
        unordered_map<int, int> position;
        int n = s.size();
        for (int i = 0; i < n; ++i) {
            if (position.count(s[i])) {
                position[s[i]] = -1;
            }
            else {
                position[s[i]] = i;
            }
        }
        int first = n;
        for (auto [_, pos]: position) {
            if (pos != -1 && pos < first) {
                first = pos;
            }
        }
        if (first == n) {
            first = -1;
        }
        return first;
    }
};


发现Leecode的题目语言和CSDN的广告语言(CH/EN)是一致的,账号一个登出了另一个也登出了!神奇子

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值