——参考代码随想录和力扣顺序刷题(https://programmercarl.com/)
哈希表是根据关键码的值而直接进行访问的数据结构。
这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
接下来哈希碰撞登场。
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
(数据规模是dataSize, 哈希表的大小为tableSize)
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放冲突的数据了。如图所示:
其实关于哈希碰撞还有非常多的细节,感兴趣的同学可以再好好研究一下,这里我就不再赘述了。
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
这里数组就没啥可说的了,我们来看一下set。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?
实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
思路:
代码:
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
unordered_set<int> s;
for(int i=0;i<nums.size();i++) {
if(s.find(nums[i]) != s.end()) return true;
else s.insert(nums[i]);
}
return false;
}
};
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路:
代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> hashtable;
for(int i=0;i<nums.size();i++) {
auto it = hashtable.find(target - nums[i]);
if(it != hashtable.end()) {
return {it->second,i};
}
hashtable.insert(pair<int,int>(nums[i],i));
}
return {};
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/two-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
C++ STL关联式容器种类
参考:http://c.biancheng.net/view/7166.html
关联式容器则大不一样,此类容器在存储元素值的同时,还会为各元素额外再配备一个值(又称为“键”,其本质也是一个 C++ 基础数据类型或自定义类型的元素),它的功能是在使用关联式容器的过程中,如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式。
C++ STL 标准库提供了 4 种关联式容器,分别为 map、set、multimap、multiset,其各自的特点如表 1 所示。
关联式容器名称 | 特点 |
---|---|
map | 定义在 头文件中,使用该容器存储的数据,其各个元素的键必须是唯一的(即不能重复),该容器会根据各元素键的大小,默认进行升序排序(调用 std::less)。 |
set | 定义在 头文件中,使用该容器存储的数据,各个元素键和值完全相同,且各个元素的值不能重复(保证了各元素键的唯一性)。该容器会自动根据各个元素的键(其实也就是元素值)的大小进行升序排序(调用 std::less)。 |
multimap | 定义在 头文件中,和 map 容器唯一的不同在于,multimap 容器中存储元素的键可以重复。 |
multiset | 定义在 头文件中,和 set 容器唯一的不同在于,multiset 容器中存储元素的值可以重复(一旦值重复,则意味着键也是重复的)。 |
C++ STL pair用法详解
参考:http://c.biancheng.net/view/7169.html
我们知道,关联式容器存储的是“键值对”形式的数据,比如:
<"C语言教程", "http://c.biancheng.net/c/">
<"[Python](http://c.biancheng.net/python/)教程", "http://c.biancheng.net/python/">
<"[Java](http://c.biancheng.net/java/)教程", "http://c.biancheng.net/java/">
如上所示,每行都表示一个键值对,其中第一个元素作为键(key),第二个元素作为值(value)。
考虑到“键值对”并不是普通类型数据,C++ STL 标准库提供了pair 类模板,其专门用来将 2 个普通元素 first 和 second(可以是 C++ 基本数据类型、结构体、类自定的类型)创建成一个新元素
。通过其构成的元素格式不难看出,使用pair 类模板来创建“键值对”形式的元素,再合适不过。
C++ STL unordered_map容器用法详解
参考:http://c.biancheng.net/view/7231.html
unordered_map 容器,直译过来就是"无序 map 容器"的意思。所谓“无序”,指的是unordered_map 容器不会像 map 容器那样对存储的数据进行排序。换句话说,unordered_map 容器和 map 容器仅有一点不同,即 map 容器中存储的数据是有序的,而 unordered_map 容器中是无序的。
unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。
C++ STL unordered_set容器用法详解
参考:http://c.biancheng.net/view/7250.html
unordered_set 容器,可直译为“无序 set 容器”,即 unordered_set 容器和 set 容器很像,唯一的区别就在于 set 容器会自行对存储的数据进行排序,而 unordered_set 容器不会。
总的来说,unordered_set 容器具有以下几个特性:
给你两个按 非递减顺序 排列的整数数组 nums1
和 nums2
,另有两个整数 m
和 n
,分别表示 nums1
和 nums2
中的元素数目。
请你 合并 nums2
到 nums1
中,使合并后的数组同样按 非递减顺序 排列。
**注意:**最终,合并后数组不应由函数返回,而是存储在数组 nums1
中。为了应对这种情况,nums1
的初始长度为 m + n
,其中前 m
个元素表示应合并的元素,后 n
个元素为 0
,应忽略。nums2
的长度为 n
。
思路:
代码:
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int leftIndex = m-1;
int rightIndex = n-1;
int Index = m+n-1;
while(rightIndex >= 0) {
if(leftIndex >= 0 && nums1[leftIndex] >= nums2[rightIndex]) {
nums1[Index] = nums1[leftIndex];
leftIndex--;
Index--;
}
else {
nums1[Index] = nums2[rightIndex];
rightIndex--;
Index--;
}
}
return;
}
};
思路:
代码:
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int,int> hash;
vector<int> result;
for(int i=0;i<nums1.size();i++) {
if(hash.find(nums1[i]) != hash.end()) {
hash[nums1[i]]++;
}
else {
hash.insert(pair<int,int>(nums1[i],1));
}
}
for(int i=0;i<nums2.size();i++) {
if(hash.find(nums2[i]) != hash.end() && hash[nums2[i]] > 0) {
hash[nums2[i]]--;
result.push_back(nums2[i]);
}
}
return result;
}
};
请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
思路:
代码:
class Solution {
public:
bool isValidSudoku(vector<vector<char>>& board) {
int row[9][10] = {0};
int column[9][10] = {0};
int box[9][10] = {0};
for(int i=0;i<9;i++) {
for(int j=0;j<9;j++) {
if(board[i][j] != '.') {
int number = board[i][j] - '0';
if(row[i][number]) return false;
if(column[j][number]) return false;
if(box[i/3 + (j/3) * 3][number]) return false;
row[i][number] = 1;
column[j][number] = 1;
box[i/3 + (j/3) * 3][number] = 1;
}
else continue;
}
}
return true;
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/valid-sudoku
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:
思路2:
代码:
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
bool isRowzero = false;
bool isColumnzero = false;
for(int i=0;i<m;i++) {
if(matrix[i][0] == 0) {
isRowzero = true;
}
}
for(int i=0;i<n;i++) {
if(matrix[0][i] == 0) {
isColumnzero = true;
}
}
for(int i=1;i<m;i++) {
for(int j=1;j<n;j++) {
if(matrix[i][j] == 0) {
matrix[0][j] = 0;
matrix[i][0] = 0;
}
}
}
for(int i=1;i<m;i++) {
if(matrix[i][0] == 0) {
for(int j=1;j<n;j++) {
matrix[i][j] = 0;
}
}
}
for(int i=1;i<n;i++) {
if(matrix[0][i] == 0) {
for(int j=1;j<m;j++) {
matrix[j][i] = 0;
}
}
}
if(isRowzero) {
for(int j=0;j<m;j++) {
matrix[j][0] = 0;
}
}
if(isColumnzero) {
for(int j=0;j<n;j++) {
matrix[0][j] = 0;
}
}
return;
}
};
代码:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
vector<int> table(26,0);
for(int i=0;i<magazine.length();i++) {
table[magazine[i]-'a']++;
}
for(int i=0;i<ransomNote.length();i++) {
table[ransomNote[i]-'a']--;
if(table[ransomNote[i]-'a'] < 0) return false;
}
return true;
}
};
思路:
代码:
class Solution {
public:
vector<vector<int>> matrixReshape(vector<vector<int>>& mat, int r, int c) {
int m = mat.size();
int n = mat[0].size();
if(m * n != r * c) return mat;
vector<vector<int>> ans(r,vector<int>(c));
for(int i=0;i<m;i++) {
for(int j=0;j<n;j++) {
int row = (i*n + j)/c;
int column = (i*n + j)%c;
ans[row][column] = mat[i][j];
}
}
return ans;
}
};
思路:
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
unordered_set<ListNode *> visited;
while(head != NULL) {
if(visited.count(head)) {
return true;
}
else{
visited.insert(head);
head = head -> next;
}
}
return false;
}
};
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
思路1:
代码:
class Solution {
public:
bool isValid(string s) {
if(s.length()%2 != 0) return false;
stack<char> st;
for(int i=0;i<s.length();i++) {
if(s[i] == '(' || s[i] == '{' || s[i] == '[') {
st.push(s[i]);
}
else {
if(st.empty()) return false;
else if(st.top() == '(') {
if(s[i] == ')') {
st.pop();
}
else return false;
}
else if(st.top() == '{') {
if(s[i] == '}') {
st.pop();
}
else return false;
}
else if(st.top() == '[') {
if(s[i] == ']') {
st.pop();
}
else return false;
}
}
}
if(!st.empty()) return false;
else return true;
}
};
思路2:
代码:
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) return false; // 如果s的长度为奇数,一定不符合要求
stack<char> st;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') st.push(')');
else if (s[i] == '{') st.push('}');
else if (s[i] == '[') st.push(']');
// 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
// 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
else if (st.empty() || st.top() != s[i]) return false;
else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
}
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return st.empty();
}
};
参考:代码随想录
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/valid-parentheses
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。