一:哈希表
一般哈希表都是用来快速判断一个元素是否出现集合里。
直白来讲其实数组就是一张哈希表,哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素。
1.两数之和
题目链接:. - 力扣(LeetCode)
// 哈希表
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int, int> map;
for(int i = 0; i < nums.size(); i++){
auto iter = map.find(target - nums[i]);
if(iter != map.end()){
return {iter->second, i};
}
else{
map.insert(pair<int, int>(nums[i], i));
}
}
return {};
}
};
本题其实有四个重点:
- 为什么会想到用哈希表
本题呢,我就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。那么我们就应该想到使用哈希法了。
- 哈希表为什么用map
这道题目中并不需要key有序,选择std::unordered_map 效率更高!
- 本题map是用来存什么的
map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)
- map中的key和value用来存什么的
这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。
那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
2. 字母异位词分组
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
map<string, int> myhash;
vector<vector<string>> myAns;
int cint =0;
for(int i = 0; i<strs.size(); i++){
string temString = strs[i];
sort(temString.begin(), temString.end());
auto iter = myhash.find(temString);
if(iter == myhash.end()){
myAns.push_back(vector<string>());
myAns.back().push_back(strs[i]);
myhash[temString]=cint;
cint ++;
}
else{
int index = myhash[temString];
myAns[index].push_back(strs[i]);
}
}
return myAns;
}
};
思路:将每个字符串按字母顺序排列,存放到哈系列表中。
3.最长连续序列
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> myset;
for(const int& num : nums){
myset.insert(num);
}
int longestStreak = 0;
for(const int& num : myset){
if(myset.count(num-1) == 0){
int currentNum = num;
int currentStreak = 1;
while(myset.count(currentNum +1)){
currentNum = currentNum + 1;
currentStreak = currentStreak + 1;
}
longestStreak = max(longestStreak, currentStreak);
}
}
return longestStreak;
}
};
思路:将整数存放到哈希表中,遍历整数,使用while循环通过哈希表来寻找连续的整数。
二:双指针
1.移动零
// 使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。
// 右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n = nums.size(), left = 0, right = 0;
while (right < n) {
if (nums[right]) {
swap(nums[left], nums[right]);
left++;
}
right++;
}
}
};
思路:通过右指针来遍历数组,左指针始终指向处理好的序列的尾部(下一个)。
2.盛最多水的容器
/* 算法流程:
1.初始化: 双指针 i , j 分列水槽左右两端;
2.循环收窄: 直至双指针相遇时跳出;
a.更新面积最大值 res;
b.选定两板高度中的短板,向中间收窄一格;
3.返回值: 返回面积最大值 res 即可; */
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0, j = height.size() - 1, res = 0;
while(i < j) {
res = height[i] < height[j] ?
max(res, (j - i) * height[i++]):
max(res, (j - i) * height[j--]);
}
return res;
}
};
思路: 双指针位于两侧,其中一个指针处于较短的锤线上,此时另外一个锤线(距离近了,面积会更小)无论是哪个所组成的面积都不会大于当前的组合,所以需要移动较短的那个指针。
3.三数之和
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 错误去重a方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
// 正确去重a方法 (已经排序过了,所以重复的在一起)
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) 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]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
思路:
首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
这里其实相当于用了三指针,一个指针负责遍历所有数据,另外两个指针负责寻找大小合适的数据。
4.接雨水
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
int left = 0, right = height.size() - 1;
int leftMax = 0, rightMax = 0;
while (left < right) {
leftMax = max(leftMax, height[left]);
rightMax = max(rightMax, height[right]);
if (height[left] < height[right]) {
ans += leftMax - height[left];
++left;
} else {
ans += rightMax - height[right];
--right;
}
}
return ans;
}
};
三:滑动窗口
总结:
1.使用双指针或者单指针加固定长度来表示滑动窗口;
2.滑动窗口中的内容可以采用哈希表结构来维护;
1.无重复字符的最长子串
// 使用滑动窗口的原因:我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!
// 使用双指针来表示滑动窗口,用哈希表来存放滑动窗口内的数据
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// 哈希集合,记录每个字符是否出现过
unordered_set<char> occ;
int n = s.size();
// 右指针rk,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
// 枚举左指针的位置,初始值隐性地表示为 -1
for (int i = 0; i < n; i++) {
if (i != 0) {
// 左指针向右移动一格,移除区间外的一个字符
occ.erase(s[i - 1]);
}
while (rk + 1 < n && !occ.count(s[rk + 1])) {
// 不断地移动右指针
occ.insert(s[rk + 1]);
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = max(ans, rk - i + 1);
}
return ans;
}
};
2.找到字符串中所有字母异位词
/*
根据题目要求,我们需要在字符串 s 寻找字符串 p 的异位词。因为字符串 p 的异位词的长度
一定与字符串 p 的长度相同,所以我们可以在字符串 s 中构造一个长度为与字符串 p 的长度相同
的滑动窗口,并在滑动中维护窗口中每种字母的数量(哈希表);当窗口中每种字母的数量与字符串 p 中每种
字母的数量相同时,则说明当前窗口为字符串 p 的异位词。
如何判断窗口中每种字母的数量是否与字符串 p 中每种字母的数量相同呢?
为了不需要单独使用一个for循环进行这个操作,所以最好使用数组这种哈希表结构
来进行维护,可以直接用 == 判断。
*/
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) {
return vector<int>();
}
vector<int> ans;
vector<int> sCount(26);
vector<int> pCount(26);
for (int i = 0; i < pLen; ++i) {
++sCount[s[i] - 'a']; //记录s中前pLen个字母的词频,代表滑窗中的哈希表,需要进行维护。
++pCount[p[i] - 'a']; //记录要寻找的字符串中每个字母的词频(只用进行一次来确定)
}
if (sCount == pCount) {
ans.emplace_back(0);
}
for (int i = 0; i < sLen - pLen; ++i) {
// 进行窗口滑动,减小一次首位的词频,增加一次末尾的词频
--sCount[s[i] - 'a'];
++sCount[s[i + pLen] - 'a'];
// 判断词频是否一致
if (sCount == pCount) {
ans.emplace_back(i + 1);
}
}
return ans;
}
};
四:子串
1.和为K的子数组
/* 前缀和+哈希表
使用前缀和的方法可以解决这个问题,因为我们需要找到和为k的连续子数组的个数。通过计算前缀和,
我们可以将问题转化为求解两个前缀和之差等于k的情况。
假设数组的前缀和数组为prefixSum,其中prefixSum[i]表示从数组起始位置到第i个位置的元素之和。
那么对于任意的两个下标i和j(i < j),如果prefixSum[j] - prefixSum[i] = k,即从第i个
位置到第j个位置的元素之和等于k,那么说明从第i+1个位置到第j个位置的连续子数组的和为k。
通过遍历数组,计算每个位置的前缀和,并使用一个哈希表来存储每个前缀和出现的次数。在遍历的过程中,
我们检查是否存在prefixSum[j] - k的前缀和,如果存在,说明从某个位置到当前位置的连续子数组的和为k,
我们将对应的次数累加到结果中。
这样,通过遍历一次数组,我们可以统计出和为k的连续子数组的个数,并且时间复杂度为O(n),其中n为数组的长度。
*/
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
mp[0] = 1; // 初始化前缀和为0的次数为1
int count = 0, sum = 0;
for (auto& x:nums) {
sum += x;
if (mp.find(sum - k) != mp.end()) {
count += mp[sum - k];
}
mp[sum]++; // 将前缀和记录在哈希表中,除非遍历到0,否则前缀和为sum的次数都为1。
}
return count;
}
};
2.滑动窗口最大值
3.最小覆盖子串
五: 普通数组
1.最大子数组和
六:矩阵
七:链表
1.相交链表
/**
* 使用哈希表存储其中一个链表中节点,遍历另一个链表找该节点是否在哈希集合中即可。
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
unordered_set<ListNode *> visited;
ListNode *temp = headA;
while (temp != nullptr) {
visited.insert(temp);
temp = temp->next;
}
temp = headB;
while (temp != nullptr) {
if (visited.count(temp)) {
return temp;
}
temp = temp->next;
}
return nullptr;
}
};
2.反转节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
// 使用迭代的方式,增加一个变量用来存储上一个节点即可
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr; // 存储上一个节点
ListNode* curr = head; // 需要迭代的当前节点
while (curr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
};
3.回文链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head) {
vector<int> vals;
// 遍历存放到容器(数组)中
while (head != nullptr) {
vals.emplace_back(head->val);
head = head->next;
}
for (int i = 0, j = (int)vals.size() - 1; i < j; ++i, --j) {
if (vals[i] != vals[j]) {
return false;
}
}
return true;
}
};
思路:1.复制链表值到数组列表中。2.使用双指针法判断是否为回文。
4.环形链表
/**
* 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*> myhash;
while (head != nullptr) {
if (myhash.count(head)) {
return true;
}
myhash.insert(head);
head = head->next;
}
return false;
}
};
思路:使用哈希表存储访问过的链表即可。
5.合并两个有序链表
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy = new ListNode();
ListNode* cur = dummy;
while(list1 && list2) {
if(list1->val < list2->val) {
cur->next = new ListNode(list1->val);
list1 = list1->next;
}else {
cur->next = new ListNode(list2->val);
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1? list1: list2;
return dummy->next;
}
};
思路:创建一个新的链表,准备一个虚拟头结点dummy指向新链表头结点方便返回合并后的链表,然后双指针指向list1和list2的表头对两个链表进行比较归并到新链表即可。
八:二叉树
1.二叉树的中序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void inorder(TreeNode* root, vector<int>& res) {
if (!root) {
return;
}
inorder(root->left, res); // 访问左节点
res.push_back(root->val); // 保存根节点
inorder(root->right, res); // 访问右节点
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
inorder(root, res);
return res;
}
};
思路:使用递归的方法, 按照访问左子树——根节点——右子树的方式遍历这棵树。
2.二叉树的最大深度
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};