leetcode刷题记录——数据结构(入门)
——参考代码随想录和力扣顺序刷题(https://programmercarl.com/)
文章目录
哈希表
首先什么是哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。
哈希表是根据关键码的值而直接进行访问的数据结构。
这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是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来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
leetcode题目
思路:
- 创建一个unordered_set容器,用s.find()查找有无数组元素,如果有则说明有重复的,如果没有则加入;
- 如果没有匹配的元素,会返回这个容器的结束迭代器 set.end();
代码:
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 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路:
- 使用哈希表,遍历一遍数组就把所有数字全部存进哈希表了,因此只需要每次遇到nums[i]就寻找target - nums[i]即可;
- 困惑点:使用unordered_map还是unordered_set
代码:
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++ 基础数据类型或自定义类型的元素),它的功能是在使用关联式容器的过程中,如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式。
- 弃用序列式容器,转而选用关联式容器存储元素,往往就是看中了关联式容器可以快速查找、读取或者删除所存储的元素,同时该类型容器插入元素的效率也比序列式容器高。
- 也就是说,使用关联式容器存储的元素,都是一个一个的“键值对”( <key,value> ),这是和序列式容器最大的不同。除此之外,序列式容器中存储的元素默认都是未经过排序的,而使用关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序。
C++ STL 标准库提供了 4 种关联式容器,分别为 map、set、multimap、multiset,其各自的特点如表 1 所示。
关联式容器名称 | 特点 |
---|
map | 定义在 | set | 定义在 头文件中,使用该容器存储的数据,各个元素键和值完全相同,且各个元素的值不能重复(保证了各元素键的唯一性)。该容器会自动根据各个元素的键(其实也就是元素值)的大小进行升序排序(调用 std::less)。 | 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++ 基本数据类型、结构体、类自定的类型)创建成一个新元素<first, second> 。通过其构成的元素格式不难看出,使用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 。
思路:
- 双指针
- 从后开始比较nums1和nums2,大者放进nums1的后面,直到nums2放完则循环结束;
- 如果leftIndex小于0并且rightIndex还大于0则说明还需要将nums2的最后一个数字放回来;(难点)
- 因为两个数组都是有序的,因此rightIndex如果结束,则说明已经有序了;
- 要把leftIndex >= 0加入到条件中,否则会出现leftIndex小于0,而rightIndex=0的情况;
代码:
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;
}
};
思路:
- 利用哈希表进行存储第一个数组,并赋值为1,若有重复则+1
- 根据哈希表遍历第二个数组,如果有相同且hash[nums2[i]] > 0则加进result中,并且hash[nums2[i]]–;
- 为什么要hash[nums2[i]] > 0 原因:进行计数,若小于0则说明超出出现次数。
代码:
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 宫内只能出现一次。(请参考示例图)
思路:
- 使用哈希表,判断某一个数字在某一行,一列,一个矩形内是否只出现过一次;
- 难点:如何判断在一个正方形内是否只出现一次:
- 思路:本质就是将9x9的矩阵按照3x3的大小分成9个。
- 怎么分,就是按照row:0-2,3-5,6-8(数组索引)(1-3…也一样)进行分类;
- 方法:将索引除以3,(int)(0-2)/3 = 0;(int)(3-5)/3 = 1;(int)(6-8)/3 = 2;
- 进阶:将一个box的数字放到一行中:
- 进行这样处理:box[i/3 + (j/3) * 3];为什么不能box[i/3 + j/3],因为这样会导致(0,3)和(3,0)两个点在一行中,因此对i和j其中一个进行处理(*3)可以改变;
代码:
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:
- 牺牲空间:新创一个mxn的空间(vector row(m), col(n))来记录哪一行哪一列有;
思路2:
- 可以达到O(1)的额外空间;
- 使用bool isRowzero = false;bool isColumnzero = false;来表示第0行和第0列是否出现0;
- 从内部(1,1)开始遍历;如果该行有出现0,则将该行对应的第0行和第0列设置为0;
- 遍历第0行和第0列,将内部(从(1,1)开始)置为0;
- 在根据isColumnzero和isRowzero来对第0行进行置0;
代码:
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;
}
};
思路:
- 难点:int row = (i*n + j)/c;int column = (i*n + j)%c;
- 为什么要*n(列数):相当于将二维数组变成一个1*n的一维数组;
- 二维数组:行数加1,列数相同的情况下,相当于i*n,多了n个;
代码:
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;
}
};
思路:
- 哈希表,访问过加进去,再次访问就是循环了;
- visited.count(head)在容器中查找值为 key 的元素的个数。
代码:
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;
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(']');
else if (st.empty() || st.top() != s[i]) return false;
else st.pop();
}
return st.empty();
}
};
参考:代码随想录
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/valid-parentheses 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
|