代码随想录第六天 | 哈希表:理论基础,不同数据结构的哈希表(map(leetcode 242,1),数组(leetcode 242),set(leetcode 349,202))

1、哈希表理论基础

参考资料与图片来源 代码随想录

1.1 哈希表概述及用途

哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素

牺牲了空间换取了时间,因为我们要使用额外的数组set或者是map来存放数据,才能实现快速的查找
代码随想录第六天 | 哈希表:理论基础,不同数据结构的哈希表(map(leetcode 242,1),数组(leetcode 242),set(leetcode 349,202))_第1张图片
一般哈希表都是用来快速判断一个元素是否出现集合里

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

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

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

1.2 哈希函数

将学生姓名映射到哈希表上就涉及到了哈希函数

通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了
代码随想录第六天 | 哈希表:理论基础,不同数据结构的哈希表(map(leetcode 242,1),数组(leetcode 242),set(leetcode 349,202))_第2张图片
如果hashCode得到的数值大于哈希表的大小

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

1.3 哈希碰撞

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

两个元素都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞

一般哈希碰撞有两种解决方法, 拉链法和线性探测法
代码随想录第六天 | 哈希表:理论基础,不同数据结构的哈希表(map(leetcode 242,1),数组(leetcode 242),set(leetcode 349,202))_第3张图片
代码随想录第六天 | 哈希表:理论基础,不同数据结构的哈希表(map(leetcode 242,1),数组(leetcode 242),set(leetcode 349,202))_第4张图片

1.4 常见的实现哈希表数据结构

三种数据结构
1、数组
2、set(集合)
3、map(映射)

代码随想录第六天 | 哈希表:理论基础,不同数据结构的哈希表(map(leetcode 242,1),数组(leetcode 242),set(leetcode 349,202))_第5张图片
std::unordered_set底层实现为哈希表std::set 和std::multiset的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset
代码随想录第六天 | 哈希表:理论基础,不同数据结构的哈希表(map(leetcode 242,1),数组(leetcode 242),set(leetcode 349,202))_第6张图片
std::unordered_map底层实现为哈希表std::map 和std::multimap的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序

map是一个key value的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的

2、不同数据哈希表的使用

2.1 leetcode 242:map&数组

第一遍代码,注释map的基本操作

class Solution {
public:
    bool isAnagram(string s, string t) {
        unordered_map<char, int> mp;//定义一个map来记录字符串s里字符出现的次数
        for(char c : s) {
            if(mp.find(c) == mp.end()) {
                mp.insert(pair<char, int>(c, 1));//map的插入insert()
            }
            else {
                auto temp = mp.find(c);//map寻找元素(key)find(),不用auto就是unordered_map::iterator
                temp->second++;
            }
        }
        for(char h : t) {
            if(mp.find(h) == mp.end()) {//当find找不到的时候
                return false;
            }
            else {
                auto temp = mp.find(h);
                temp->second--;
            }
        }
        for(auto iter = mp.begin(); iter != mp.end(); iter++) {//map的迭代循环
            if(iter->second != 0) {
                return false;
            }
        }
        return true;
    }
};

也可以直接用数组来代替unordered_map的功能,其余思想一致

定义一个数组叫做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++) {
            // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
            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) {
                // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
                return false;
            }
        }
        // record数组所有元素都为零0,说明字符串s和t是字母异位词
        return true;
    }
};

2.2 leetcode 349:什么时候set/什么时候数组

第一遍代码:

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> s;//记录nums1的所包含的数字
        unordered_set<int> re;//避免结果重复
        vector<int> res;//最后返回的数组
        for(int i:nums1) {
            s.insert(i);
        }
        for(int j:nums2) {
            if(s.find(j) != s.end()) {
                re.insert(j);
            }
        }
        for(int c:re) {
            res.push_back(c);
        }
        return res;
    }
};

使用数组来做哈希的题目,是因为题目都限制了数值的大小(leetcode 242: 26个)
直接使用set不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的
而这道题目没有限制数值的大小,就无法使用数组来做哈希表了
而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费

此时就要使用另一种结构体了set ,关于set,C++给提供了如下三种可用的数据结构:
std::set
std::multiset
std::unordered_set

使用unordered_set读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复
unordered_set nums_set(nums1.begin(), nums1.end());//直接用数组初始化set
return vector(result_set.begin(), result_set.end());//直接用set初始化数组

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());//直接用数组初始化set
        for (int num : nums2) {
            // 发现nums2的元素 在nums_set里又出现过
            if (nums_set.find(num) != nums_set.end()) {
                result_set.insert(num);
            }
        }
        return vector<int>(result_set.begin(), result_set.end());//直接用set初始化数组
    }
};

增添了数值范围:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
所以就可以使用数组来做哈希表了, 因为数组都是1000以内的

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
        int hash[1005] = {0}; // 默认数值为0
        for (int num : nums1) { // nums1中出现的字母在hash数组中做记录
            hash[num] = 1;
        }
        for (int num : nums2) { // nums2中出现话,result记录
            if (hash[num] == 1) {
                result_set.insert(num);
            }
        }
        return vector<int>(result_set.begin(), result_set.end());
    }
};

2.3 leetcode 202:unordered_set(无穷循环没有思路)

第一次不知道怎么判断,特别还有可能无穷循环
题目中说了会无限循环,那么也就是说求和的过程中sum会重复出现
这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止

看了思路写代码

class Solution {
public:
    bool isHappy(int n) {
        int sum = 0;
        unordered_set<int> s;
        int tmp = n;
        while(tmp != 0) {//条件为tmp%10 != 0不对,因为数字中间可能出现0
            sum += (tmp % 10)*(tmp % 10);
            tmp = tmp / 10;
        }
        while(s.find(sum) == s.end()) {//注意find找不到的写法
            if(sum == 1) {
                return true;
            }
            s.insert(sum);
            tmp = sum;
            sum = 0;
            while(tmp != 0) {
                sum += (tmp % 10)*(tmp % 10);
                tmp = tmp / 10;
            }
        }
        return false;
    }
};

对于

while(tmp != 0) {
	sum += (tmp % 10)*(tmp % 10);
       tmp = tmp / 10;
}

这一段可以复用,可以写成一个函数

class Solution {
public:
    // 取数值各个位上的单数之和
    int getSum(int n) {
        int sum = 0;
        while (n) {
            sum += (n % 10) * (n % 10);
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        unordered_set<int> set;
        while(1) {
            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;
        }
    }
};

2.4 leetcode 1:unordered_map(第一遍逻辑有问题,包含multiset.insert/erase用法)

因为multiset会自己排序,所以没办法指定元素插入位置,如果借助数组找到下标那么由于multiset是有序的需要返回不同的下标,需要在数组找下标上动脑筋而非set上,有序插入删除位置不受控

std::multiset.insert(position, element)它没有指定要插入的位置,它仅指向要开始搜索操作以插入的位置,以加快处理速度。插入是根据多集容器遵循的顺序完成的

vector为空不可以用下标去引用加入别的元素

std::multiset.erase(element)指的是借助其值从多重集中删除的特定元素。此方法将擦除此值的所有实例(所有等于这个值的全删了)

C++ multiset insert()用法及代码示例

C++ multiset erase()用法及代码示例

class Solution {
public:
    int find_num(vector<int>& nums, int target) {
        for(int i = 0; i < nums.size(); i++) {
            if(nums[i] == target) {
                return i;
            }
        }
        return -1;
    }
    vector<int> twoSum(vector<int>& nums, int target) {
    //每次遍历到一个数,就找nums有没有target-那个数的,快速查找用哈希
        multiset<int> hash(nums.begin(), nums.end());//[3, 3] 6就需要可以存放重复的元素
        vector<int> result;
        for(multiset<int>::iterator iter = hash.begin(); iter != hash.end(); iter++) {//int a : nums不行,因为如果有相同的元素按题意只能删一个,如果erase元素的话全删了
            int a = *iter;
            hash.erase(iter);//不能重复,比如target=6,不能同一个元素3两遍
            if(hash.find(target - a) != hash.end()) {
                result.push_back(find_num(nums, a));//因为要返回下标
                int nn = find_num(nums, target - a);//处理相同值([3,3] 6)返回的下标问题
                vector<int> newnums;
                cout << 1;
                if(find_num(nums, a) == find_num(nums, target - a)) {
                    int j = 0;
                    for(int i = find_num(nums, a)+1; i < nums.size(); i++) {
                        //newnums[j++] = nums[i];
                        //报错runtime error: reference binding to null pointer of type 'int' (stl_vector.h)
                        //vector为空不可以用下标去引用
                        newnums.push_back(nums[i]);
                    }
                    nn += 1;
                    nn += find_num(newnums, target - a); 
                }
                result.push_back(nn);
                break;//唯一
            }
            hash.insert(iter, a);//因为multiset会自己排序,所以没办法指定元素插入位置(还是会排序)
        } 
        return result;   
    }
};

使用哈希法:当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候

本题需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是是否出现在这个集合就应该想到使用哈希法了

2.5 使用map数据结构原因

不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适(如果用set找是否存在再去数组里找,首先对于set首先不会存相同的元素,就算用multiset,因为是有序的,所以在确保有相同元素在数组中正确找到不同下标比较麻烦)
std::map 和std::multimap 的key也是有序

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> um;
        //通过拿到一个寻找之前有没有能加起来为target,解决不可重复问题,之前的元素用map记录元素以及下标,注意key不可重复,map的查找是通过key完成的
        vector<int> re;
        for(int i = 0; i < nums.size(); i++) {
            if(um.find(target - nums[i]) != um.end()) {
                re.push_back(i);
                re.push_back(um.find(target - nums[i])->second);
                return re;
            }
            um.insert(pair<int, int>(nums[i], i));
        }
        return re;
    }
};

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

使用std::unordered_map(map/multimap通用)需要明确两点:
1、map用来做什么
2、map中key和value分别表示什么

map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)

接下来是map中key和value分别表示什么

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

那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标

在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素

你可能感兴趣的:(leetcode,c++,数据结构,leetcode,算法,c++,哈希算法)