代码随想录+leetcode学习笔记
11.13.2024
学习了数组,二分查找,攻克了leetcode 704,35,34,69题。
我的二分查找按如下模式写(左闭右闭的区间):
while(left<=right){
mid=left+(right-left)/2;
if(target<nums[mid]){
right=mid-1;
}
else if(target>nums[mid]){
left=mid+1;
}
else return mid;
}
35题 搜索元素插入位置:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
要插入的元素可能在数组整体的左、右,或者介于某两个元素之间,或者等于某元素。共四种情况,对于所有情况以下代码均适用:
return right+1;
因为最后退出while循环时,一定是right刚好在left的左边。
69题有坑:
int mid=(left+right)/2;
long long s=mid*mid;
并不对,mid过大时,mid*mid超出int的范围,直接会报错
解决方案:
long long mid=(left+right)/2;
或者:
long long s = (long long)mid * mid; // 防止溢出
11.16.2024
学习了数组的删除元素,双指针法,完成27,26,283,844,209题
977题 有序数组的平方
学习到了C++ STL的sort:
vector<int> A;
sort(A.begin(), A.end()); // 快速排序
使用的是快速排序和堆排序的混合优化版本,称为 IntroSort。
平均、最坏、最好时间复杂度: 均为O(nlogn)
最优做法是:
数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了,i指向起始位置,j指向终止位置。
定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。
如果A[i] * A[i] < A[j] * A[j] 那么result[k–] = A[j] * A[j]; 。
如果A[i] * A[i] >= A[j] * A[j] 那么result[k–] = A[i] * A[i]; 。
接触滑动窗口思想。
209题 长度最小的子数组
(1)思路——滑动窗口
- 我们在数组nums 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引区间 [left, right] 称为一个「窗口」。
- left不动,不断地增加 right 指针扩大窗口,直到窗口符合要求(窗口内数值之和>=target)。
- 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 ,直到窗口不再符合要求。
- 重复第 2 和第 3 步,直到 right 到达数组末尾。
(: 第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」。)
(2)时间复杂度
该算法的时间复杂度是O(n)。
不要见到两个循环嵌套就以为时间复杂度是O(n^2),
时间复杂度主要是看每一个元素被操作的次数,每个元素在滑动窗口往右扩大时进来一次,窗口缩小时出去一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
11.18.2024
继续研究滑动窗口问题
904题 水果成篮
在一个数组中找到最多包含两种不同数字的最长连续子数组。
C++ STL的set的特点:
- 唯一性:同一个set中不能有重复的元素。
- 有序性:所有元素会按升序(默认)或自定义规则排序。
- 底层实现:采用红黑树结构,插入、删除和查找的时间复杂度为 O(logn)。
如果你需要存储重复的元素,可以使用std::multiset。
如果不需要元素有序性,但需要快速查找和唯一性,可以使用std::unordered_set(基于哈希表实现)。
特性 | std::map | std::unordered_map |
---|---|---|
底层实现 | 红黑树 | 哈希表 |
存储顺序 | 按键排序 | 无序 |
插入、查找、删除 | O(logn) | 平均 O(1) ,最坏 O(n) |
内存使用 | 较少 | 较多(因需要维护哈希表) |
自定义排序 | 支持(通过比较函数) | 不支持 |
适用场景 | - 需要按键排序 - 数据量较小或操作稳定性要求 | - 高性能查找和插入 - 数据量较大或快速访问 |
特性 | std::set | std::multiset |
---|---|---|
底层实现 | 红黑树 | 红黑树 |
存储顺序 | 按键排序 | 按键排序 |
键的唯一性 | 只允许唯一键 | 允许重复键 |
插入、查找、删除效率 | O(logn) | O(logn) |
内存使用 | 较少 | 较少 |
适用场景 | - 需要存储唯一元素 - 自动排序 - 常用作集合 | - 需要存储重复元素 - 自动排序 - 多重集合 |
最后关于unordered_map的使用:
int totalFruit(vector<int>& fruits) {
unordered_map<int,int> um;
int i=0,ans=0;
for(int j=0;j<fruits.size();j++){
um[fruits[j]]++;
while(um.size()>2){
um[fruits[i]]--;
if(um[fruits[i]]==0){
um.erase(fruits[i]);
}
i++;
}
ans=max(ans,j-i+1);
}
return ans;
}
76题 最小覆盖字串
滑动窗口问题会有两个指针,一个用于向右侧延申当前窗口,另一个从左侧收缩当前窗口。在任意时刻,只有一个指针运动,而另一个保持静止。
本题的关键在于使用两个哈希表(C++可以使用unordered_map)来分别维护子串t,以及当前窗口中,各字符出现的次数。并且使用一个formed变量,记录此时满足覆盖条件的字符的个数。
//如果当前字符的出现次数符合 t 中该字符的需求,更新 formed
if(window_count[s[j]]==t_count[s[j]]){
formed++;
}
//收缩窗口,左指针向右移动
if(t_count.count(s[i])!=0){
window_count[s[i]]--;
if(window_count[s[i]]<t_count[s[i]]){
formed--;
}
}
11.19.2024
今天在做模拟的题。主要关于螺旋矩阵。
59题 螺旋矩阵II
本题在于对一个方阵的四个方向的遍历。可以考虑圈的概念。因为每一圈的遍历必定包含右、下、左、上四个方向的遍历。
所以可以分开写两个逻辑:
(1)每一圈内,右、下、左、上四个方向的遍历。
(2)写一个大的while用于判断n的方阵,需要遍历多少圈。(n/2)
最后,如果n为奇数,例如3*3的方阵,可以观察到:遍历完第一圈之后,a[1][1]将不会进入第二圈循环。所以单独为此时赋值即可。
54题 螺旋矩阵
这道题卡的有点久,后来在网上看了解析才明白,主要是陷在了59题的圈的思路里。
m行n列的矩阵,用 圈来做没有意义了。
该题的做法是维护up、bottom、left、right四个边界。
不断按右、下、左、上四个方向进行遍历,每段遍历过后,均更新边界(右向遍历过后,up++;下向遍历过后,right–;···)。
对于每段遍历,如果更新边界过后,该边界超出了另一侧的边界,代表结束,终止循环。
前缀和;二维数组分别按行列计算的前缀和。
过完《代码随想录》中数组这一篇章。
链表
给出C/C++的定义链表节点方式,如下所示:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
通过自己定义构造函数初始化节点:
ListNode* head = new ListNode(5);
使用C++来做leetcode,也要养成手动清理内存的习惯。
203题 移除链表元素
需要注意的地方:head节点也是有值的,可能需要删除。
方法一:虚拟头节点
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode *dummynode=new ListNode();
dummynode->next=head;
ListNode *temp1=dummynode;
//if(head==nullptr) return head;
ListNode *temp2=dummynode->next;
while(temp2!=nullptr){
if(temp2->val==val){
temp1->next=temp2->next;
delete temp2;
temp2=temp1->next;
}
else{
temp1=temp1->next;
temp2=temp2->next;
}
}
head=dummynode->next;
delete dummynode;//释放内存
return head;
}
};
方法二:递归
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head==nullptr) return head;
if(head->val==val){
ListNode *newhead=removeElements(head->next,val);
delete head;
return newhead;
}
else{
head->next=removeElements(head->next,val);
return head;
}
}
};
11.20.2024
707题 设计链表
本题的关键点:
(1)MyLinkedList() 初始化 MyLinkedList 对象后,链表仍不含任何数值元素;同时,删除的节点可以为头节点,新加入的节点也可以作为头节点,所以可以设置一个dummyhead。
(2)函数get、addAtIndex、deleteAtIndex均涉及索引index,所以可以维护一个链表的长度size。当index不在size范围内时,直接return掉,这样可以避免对很多边界条件的判断。
(3)用于遍历的temp,设置成dummyhead还是dummyhead->next?对于get函数设置为dummyhead->next,addAtTail和addAtIndex和deleteAtIndex设置为dummyhead。需要具体判断。
206题 反转链表
递归法
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==nullptr) return nullptr;
ListNode *newhead=reverseList(head->next);
if(newhead==nullptr) return head;
// ListNode *temp=newhead;
// while(temp->next!=nullptr){
// temp=temp->next;
// }
// temp->next=head;
head->next->next=head;//head此时还是指向第二个节点的,即新链表翻转后的最后一个节点
head->next=nullptr;//要给链表结尾赋nullptr
return newhead;
}
};
双指针法也可以做
24题 两两交换链表中的节点
用递归法一遍就写出来了。
链表题,先思考能不能用递归。
160题 相交链表
该题需要注意的点:
(1)某节点如果是两个链表的共用节点,那么从该节点往后所有节点必定也是全部公用的,所以只需要找到第一个相等的节点(并非val相等,而是节点相等)。
(2)如果暴力求解,是O(n2)的复杂度。进行优化(如果sizeA>sizeB):首先A串中超出B串长度的前面的部分没有意义,这部分不用搜索。第二,由于两串尾部必定相同的特点,可以把B串放在A串的尾部,只需从headB开始,进行同时更新tempA和tempB的搜索,直到找到相等的点。事实上,这是一种剪枝策略。对于B串中的每个tempB,他需要搜索的位置只有A中对应的相同长度的那个tempA。在此之前的点不会是相交点,在此之后的点也不会是相交点。
142题 环形链表 II
遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。借助哈希表可以很方便地实现。
unordered_set<ListNode *> visited;
while (head != nullptr) {
if (visited.count(head)) {
return head;
}
visited.insert(head);
head = head->next;
}
过完《代码随想录》链表的内容。
11.22.2024
哈希表。
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
349题 两个数组的交集
set和unordered_set均有.begin()和.end(),也均可以通过迭代器遍历:
unordered_set<int> nums1_set,nums2_set;
unordered_set<int>::iterator iter;
for(iter=nums1_set.begin();iter!=nums1_set.end();iter++)
{
if(nums2_set.count(*iter)) ans.push_back(*iter);
}
可以直接用一个vetcor数组对象,初始化set或unorderd_set对象:
也可以将set转换成vector数组对象:
vector<int> nums1;
unordered_set<int> nums_set(nums1.begin(), nums1.end());
return vector<int>(result_set.begin(), result_set.end());
set寻找某元素的索引,以及插入:
find() 是 std::set 提供的方法,用于查找指定的元素。
它返回一个迭代器:
- 如果 target 存在,则返回指向该元素的迭代器。
- 如果 target 不存在,则返回指向 mySet.end() 的迭代器(表示集合的尾部之后的位置,不指向任何有效元素)。
end() 返回一个哨兵迭代器,表示集合中最后一个元素之后的位置。
end() 是不可解引用的,常用于检查元素是否到达集合的末尾。
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
也可以判断两个set或map对象是否相等:
unordered_map<char,int> snum,tnum;
if(snum==tnum) return true;
11.27.2024
202题 快乐数
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
正如:关于哈希表,你该了解这些! (opens new window)中所说,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
1题 两数之和
这个题的暴力解法:两层for循环遍历,看两数之和是否等于target。
所以朴素的优化:
考虑将整个数组放入到一个unordered_map里。因为ordered_map(也即哈希表)查询、增删某元素的效率都是O(1),所以我在第一层for循环的同时,只需要用O(1)的时间来判断数组内是否存在值为target-nums[i]的元素。
注意此时map存储的key是元素的值,value是元素的下标。
本题其实有四个重点:
为什么会想到用哈希表
哈希表为什么用map
本题map是用来存什么的
map中的key和value用来存什么的
454题 四数相加 II
本题4个数组,但是不可能用O(n4)的复杂度解决问题。所以对称的想,能否用O(n2)的复杂度解决问题。
可以想到用O(n2)的时间遍历A,B两个数组,将其所有可能的和存入map1中。同理得到map2。
再对map1中每个元素进行遍历,用O(1)的时间在map2中查找对应元素。
要注意的是:
一开始我想将map的value用来存i,j两个下标,后面做出优化,其实只需要++,存储该sum值出现次数即可。
15题 三数之和
重要
该题未想到双指针的具体解法,看了随想录的题解。
首先注意该题的输出,只需要输出元素,而不需要对应的下标。所以可以对数组进行sort排序。
该题使用双指针法(其实三指针)。
首先一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
sort(nums.begin(),nums.end());
//if(nums==vector<int>(3,0)) return {{0,0,0}};
for(int i=0;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]) continue;//注意(-1,-1,2)这种情况,实际含义是下一个i的值不变的话,这种情况一定在前一个i计算出来过
int left=i+1,right=nums.size()-1;
while(left<right){
if(nums[i]+nums[left]+nums[right]>0) right--;
else if(nums[i]+nums[left]+nums[right]<0) left++;
else{
ans.push_back({nums[i],nums[left],nums[right]});
while(right>0&&nums[right]==nums[right-1]) right--;
while(left<nums.size()-1&&nums[left]==nums[left+1]) left++;
right--;
left++;
}
}
}
return ans;
}
};
18 四数之和
一样的思路。可以先把逻辑写出来,再想该如何去重。
自己被long卡了一次,修改后一遍通过。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ans;
sort(nums.begin(),nums.end());
for(int i=0;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]) continue;
for(int j=i+1;j<nums.size();j++){
if(j>i+1&&nums[j]==nums[j-1]) continue;
int left=j+1,right=nums.size()-1;
while(left<right){
if((long)nums[i]+nums[j]+nums[left]+nums[right]>target) right--;
else if((long)nums[i]+nums[j]+nums[left]+nums[right]<target) left++;
else{
ans.push_back({nums[i],nums[j],nums[left],nums[right]});
while(left<right&&nums[right]==nums[right-1]) right--;
while(left<right&&nums[left]==nums[left+1]) left++;
right--;
left++;
}
}
}
}
return ans;
}
};
49题 字母异位词分组
首先,我想尝试定义
unordered_map<unordered_map<char,int>,vector<string>> um;
但实际上无法这么定义,原因: std::unordered_map 的键类型要求能够计算哈希值并支持相等比较。std::unordered_map<char, int> 默认情况下并没有定义哈希函数和相等比较操作,因此无法作为 std::unordered_map 的键。
但是可以用更简单的类型作为哈希表的键。所以尝试用string。
修改后,一遍过。
class Solution {
public:
string getstring(string s){
sort(s.begin(),s.end());
return s;
}
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> ans;
unordered_map<string,vector<string>> um;
for(string s:strs){
um[getstring(s)].push_back(s);
}
for(auto iter:um){
ans.push_back(iter.second);
}
return ans;
}
};
传统的迭代器应该这样写:
for (auto iter = um.begin(); iter != um.end(); ++iter) {
ans.push_back(iter->second);
}
438题 找到字符串中所有字母异位词
本题我有一点误判,导致我将自己思路的O(n)复杂度,想成了O(n2)的复杂度。
注意:当需要对两个unordered_map判断是否相等的时候,复杂度取决于键的数量。
本题中,比较两个 unordered_map 的复杂度是 O(k),其中 k 是字符种类的数量。
接下来的思路,就是动态维护滑动窗口的字符出现次数哈希表,每次都判断一下是否与子串的哈希表相等。
注意对map的erase。
if(window_um[s[i-p.size()]]==0)
window_um.erase(s[i-p.size()]);
11.28.2024
进入《随想录》字符串这一章节。
541题 反转字符串 II
虽然只是一道简单题,但还是卡了我很久,我自己的思路(让i++)一直有bug。
本题学到的知识点:
(1)reverse函数
reverse 是 C++ 标准库 中的一个函数,用于反转指定范围内的元素。它的参数是两个迭代器,表示需要反转的范围 [first, last),其中 first 是起始位置的迭代器,last 是终止位置的迭代器(不包括终止位置的元素)。
// 只反转前 3 个字符
reverse(s.begin(), s.begin() + 3);
// 反转字符串的全部字符
reverse(s.begin(), s.end());
// 只反转最后 4 个字符
reverse(s.end() - 4, s.end());
在考虑reverse的参数时,起止位置的插值就是要反转的元素个数,在这个基础上,定好一端的参数即可。
(2)周期问题
for(int i=0;i<s.size();i+=2*k){
reverse(s.begin()+i,min(s.end(),s.begin()+i+k));
}
遇到这种周期问题,不需要让i一个个的遍历,然后不断判断边界条件将非常麻烦。可以让i每次递增的值均为周期2k。问题将极大简化。
一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。
其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。
所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。
卡码54题 替换数字
其实很多数组填充类的问题,其做法都是先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
151题 翻转字符串里的单词
class Solution {
public:
string reverseWords(string s) {
string ans;
vector<string> md;
string temp;
for(char c:s){
if(c!=' '){
temp+=c;
}
else{
md.push_back(temp);
temp="";
}
}
md.push_back(temp);
for(int i=md.size()-1;i>=0;i--){
if(!md[i].empty()) ans+=md[i],ans+=" ";
}
ans=ans.substr(0,ans.size()-1);
return ans;
}
};
substr函数包含在string头文件里。并且他的参数不应该是地址,而应该是下标。
string ssub=s.substr(0,s.size()-k);
28题 找出字符串中第一个匹配项的下标
也即实现strStr()函数。
KMP算法。
最长公共前后缀
文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
GPT:
在计算 模式串的 next 数组 时,j = next[j - 1] 的作用是:当当前字符不匹配时,将 j 回退到当前已匹配前缀的最长相等前缀的下一个位置,以继续尝试匹配。这一步是计算 next 数组的核心逻辑,保证了我们只需要基于模式串自身信息递推出整个 next 数组。
从定义上解释为何是 next[j - 1]
核心逻辑
j = next[j - 1] 的依据是 前缀和后缀的对称性:
当前已匹配长度为 j,表示子串 s[0:j) 是模式串的前缀。
如果 s[i] != s[j],需要缩短匹配长度。
缩短后的位置是 next[j - 1],因为:
next[j - 1] 是子串 s[0:j)中最长相等前缀后缀的长度。
回退到这个长度的位置,相当于尝试匹配较短的前缀,而不会漏掉任何可能的匹配。
我的示例:
i | 子串 P[0:i] | 此时的 j | next[j - 1] 回退点 |
---|---|---|---|
0 | A | - | -(无法回退) |
1 | AB | 0 | next[1] = 0 |
2 | ABA | 1 | next[2] = 1 |
3 | ABAB | 2 | / |
4 | ABABA | 3 | / |
5 | ABABAC | 3(匹配失败) | next[3-1] = 1 |
为什么要看next[j-1],因为此时相当于在ABA里找最长相等前后缀。
class Solution {
public:
void getnext(int *next,string s){
//获取next数组时为什么j从0开始,而i从1开始?
//因为j=0,i=1形成了一个最小最初始的长度大于1的窗口,只有长度大于1才能有前后缀。单个字符的前后缀没有。
next[0]=0;//next[i] :表示s[0...i]这个子串匹配的前后缀长度。
int j=0;
for(int i=1;i<s.size();i++){
//为什么j回退到next[j-1]?
//因为s[0...next[j-1]-1] 和 s[i-next[j-1]...i-1]是匹配的前后缀,直接让他们匹配完成,
//接下来只看s[i]和s[next[j-1]]即可。
while(j>0&&s[i]!=s[j]){
j=next[j-1];//核心
}
if(s[i]==s[j]) j++;
next[i]=j;
}
}
int strStr(string haystack, string needle) {
vector<int> next(needle.size(),0);
int j=0;
getnext(&next[0],needle);
for(int i=0;i<haystack.size();i++){
while(j>0&&haystack[i]!=needle[j]){
j=next[j-1];
}
if(haystack[i]==needle[j]) j++;
if(j==needle.size())
return i-needle.size()+1;
}
return -1;
}
};
459题 重复的子字符串
substr的第二个参数是长度,我当成了终止位置。
假设有字符串 s = “abcdef”,我们调用 s.substr(1, 3)。
起始位置 pos = 1,字符是 ‘b’。
长度 len = 3,表示从 ‘b’ 开始取 3 个字符,即 ‘b’、‘c’、‘d’。
调用 s.substr(1, 3) 会返回 “bcd”。
C++判断子串是否出现:
if(t.find(s)!=string::npos) return true;
//if(t.find(s)!=-1) return true;//一样的写法
else return false;
C++标准库的实现
C++ 标准库中的 std::string::find 函数通常会根据子字符串的大小和主字符串的大小,选择适当的算法来进行优化。例如,对于小的子字符串,它可能使用朴素查找;对于较大的字符串,可能使用 KMP 或 Boyer-Moore 算法等。并且,C++ 标准库在实现中常常会使用一些底层优化,如 SIMD(单指令多数据)等技术,来加速查找过程。
《随想录》字符串章节结束。
字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。
双指针法是字符串处理的常客。
KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易,我们已经写了五篇KMP的文章,不断总结和完善,最终才把KMP讲清楚。
11.29.2024
《随想录》栈与队列章节
基础知识:
栈和队列是STL(C++标准库)里面的两个数据结构。
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
SGI STL中 队列底层实现缺省情况下一样使用deque实现的。
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
我们也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
可以注意的是:
如果某类型提供迭代器,也相当于支持随机访问(例如以下标[]访问,比如map和unordered_map。)
特性 | stack | queue |
---|---|---|
是容器吗? | 否,是容器适配器 | 否,是容器适配器 |
默认底层容器 | std::deque | std::deque |
是否提供迭代器? | 否,不支持直接遍历 | 否,不支持直接遍历 |
实现方式 | 基于 deque 的后端封装 | 基于 deque 的前后封装 |
232题 用栈实现队列
本题思路在于两个栈分别用来输入和输出。
当pop时,如果stack_out不空,从stack_out输出并pop元素;如果stack_out空,这个时候要将stack_in内的元素全部push到stack_out中。
int pop() {
if(!stack_out.empty()){
int temp=stack_out.top();
stack_out.pop();
return temp;
}
else{
while(!stack_in.empty()){
int temp=stack_in.top();
stack_in.pop();
stack_out.push(temp);
}
int temp=stack_out.top();
stack_out.pop();
return temp;
}
}
225题 用队列实现栈
首先了解到,stack和queue都有size()函数,queue除了有front()还有back()函数。
对于queue:
函数名 | 功能 |
---|---|
size() | 返回队列中元素的数量 |
empty() | 检查队列是否为空 |
front() | 返回队列首部元素的引用 |
back() | 返回队列尾部元素的引用 |
push() | 向队列尾部添加元素 |
emplace() | 在队列尾部直接构造元素 |
pop() | 移除队列首部元素 |
swap() | 交换两个队列的内容 |
对于本题,可以用一个队列实现栈。
每当pop时,将前size()-1个元素弹出并再次加入到队列的末尾,这时队首元素即需要的元素。
1047题 删除字符串中的所有相邻重复项
匹配问题,用栈!
150题 逆波兰表达式求值
这个题没什么难点,但新学到的一点是stoi和stoll函数,均定义在 <string> 头文件中,用于将字符串转换为 int 或long long类型。
记忆该函数,可以理解为string to int/string to long long。
举例如下:
std::string str = "12345";
int num = std::stoi(str);//12345
std::string str = "123abc";
size_t idx;
int num = std::stoi(str, &idx);
std::cout << "Parsed integer: " << num << std::endl;//123
std::cout << "Unparsed part: " << str.substr(idx) << std::endl;//abc
12.02.2024
一点感想:来到十二月了。该给自己定好目标并且实现了。
十二月的大目标:
(1)按部就班完成开题答辩,这个只需要跟着走就行。
(2)把简历全面升级一下,然后凭借学到的数据结构与算法知识、做的开源项目,投大中公司日常实习的项目。
这对我很重要!!!
你要挣很多钱,要自由!!!
十二月的小目标:
(1)把代码随想录刷完,基础知识过一遍。
(2)把mcu15445项目这个月之内 ,一定做完,然后写到简历上。
(3)简历全面优化升级。
239题 滑动窗口中的最大值
首先了解一下优先队列priority_queue。
#include <queue>
#include <vector>
#include <functional> // 用于 std::greater
std::priority_queue<int> maxHeap; // 最大堆(默认)
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap; // 最小堆
//std::priority_queue<T, Container, Compare>
//T:队列中存储的元素类型。
//Container:底层容器类型(默认是 std::vector<T>)。
//Compare:比较函数,用于定义优先级(默认是 std::less<T>,即最大堆)
优先队列的应用场景:
- 任务调度:
操作系统中的进程调度,根据任务优先级执行任务。 - 路径规划算法:
如 Dijkstra 算法和 A* 算法使用优先队列选择当前最优节点。 - 事件驱动仿真:
事件按时间或优先级触发。 - 数据流处理:
维护动态数据中的最大值或最小值。
优先队列操作及复杂度
操作 | 函数 | 复杂度 | 说明 |
---|---|---|---|
插入元素 | push(value) | 𝑂(log𝑛) | 将新元素插入到优先队列中。 |
删除优先级最高的元素 | pop() | 𝑂(log𝑛) | 移除队列中优先级最高的元素。 |
访问优先级最高的元素 | top() | 𝑂(1) | 获取队列中优先级最高的元素,但不移除。 |
检查队列是否为空 | empty() | 𝑂(1) | 判断优先队列是否为空。 |
获取队列中元素的个数 | size() | 𝑂(1) | 返回优先队列的元素个数。 |
堆(Heap) 是一种特殊的二叉树数据结构,满足以下性质:
-
堆的定义
完全二叉树:堆是一棵完全二叉树,即除了最后一层,所有层的节点都被填满,最后一层的节点从左到右排列。
堆性质:
最大堆:每个节点的值都不小于其子节点的值(根节点是最大值)。
最小堆:每个节点的值都不大于其子节点的值(根节点是最小值)。 -
堆的存储
堆通常使用数组来表示,树形结构映射到数组中有以下特点:
对于索引为 i 的节点:
左子节点索引:2i+1
右子节点索引:2i+2
父节点索引:
(i-1)/2
这种结构使堆操作更加高效,无需显式地存储指针。
再了解一下双端队列deque。
栈和队列,在STL的默认的实现就是基于双端队列。
std::deque
常用函数
操作 | 函数 | 说明 |
---|---|---|
构造函数 | deque() | 创建一个空的 deque 。 |
deque(size_t n) | 创建一个包含 n 个元素的 deque ,元素为默认构造值。 | |
deque(size_t n, const T& val) | 创建一个包含 n 个元素的 deque ,每个元素初始化为 val 。 | |
deque(const deque& other) | 拷贝构造函数,创建一个与另一个 deque 相同的副本。 | |
deque(deque&& other) | 移动构造函数,从另一个 deque 移动元素。 | |
deque(std::initializer_list<T> init) | 使用初始化列表创建一个 deque 。 | |
迭代器相关函数 | begin() | 返回指向第一个元素的迭代器。 |
end() | 返回指向最后一个元素之后的位置的迭代器。 | |
rbegin() | 返回指向最后一个元素的反向迭代器。 | |
rend() | 返回指向第一个元素之前的位置的反向迭代器。 | |
cbegin() | 返回常量的 begin() 迭代器,不能修改元素。 | |
cend() | 返回常量的 end() 迭代器,不能修改元素。 | |
crbegin() | 返回常量的反向迭代器,不能修改元素。 | |
crend() | 返回常量的反向迭代器,不能修改元素。 | |
容量相关函数 | empty() | 判断 deque 是否为空,返回 true 或 false 。 |
size() | 返回 deque 中元素的个数。 | |
max_size() | 返回 deque 能容纳的最大元素数。 | |
resize(size_t n) | 调整 deque 的大小为 n ,新增元素用默认值填充。 | |
resize(size_t n, const T& val) | 调整 deque 的大小为 n ,新增元素用 val 填充。 | |
shrink_to_fit() | 请求减少容器容量以适应当前大小(可能无效果)。 | |
访问元素 | operator[] | 返回指定索引 i 处的元素,不进行边界检查。 |
at(size_t i) | 返回指定索引 i 处的元素,越界抛出 out_of_range 异常。 | |
front() | 返回队列中第一个元素的引用。 | |
back() | 返回队列中最后一个元素的引用。 | |
data() | 返回指向第一个元素的指针。 | |
修改操作 | push_front(const T& val) | 在前端插入元素 val 。 |
push_front(T&& val) | 在前端插入元素 val ,使用移动语义。 | |
push_back(const T& val) | 在尾部插入元素 val 。 | |
push_back(T&& val) | 在尾部插入元素 val ,使用移动语义。 | |
pop_front() | 移除前端元素。 | |
pop_back() | 移除尾部元素。 | |
insert(iterator pos, const T& val) | 在指定位置 pos 插入元素 val 。 | |
insert(iterator pos, T&& val) | 在指定位置 pos 插入元素 val ,使用移动语义。 | |
insert(iterator pos, size_t n, const T& val) | 在指定位置 pos 插入 n 个元素 val 。 | |
erase(iterator pos) | 移除指定位置的元素。 | |
erase(iterator first, iterator last) | 移除从 first 到 last 范围内的元素。 | |
clear() | 移除所有元素。 | |
其他功能 | swap(deque& other) | 交换当前 deque 与另一个 deque 的内容。 |
emplace_front(Args&&... args) | 在前端原地构造元素。 | |
emplace_back(Args&&... args) | 在尾部原地构造元素。 | |
emplace(iterator pos, Args&&... args) | 在指定位置 pos 原地构造元素。 |
deque
vs vector
性能比较
操作 | deque 性能 | vector 性能 |
---|---|---|
头部插入/删除 | 𝑂(1) | 𝑂(𝑛) |
尾部插入/删除 | 𝑂(1) | 𝑂(1) |
中间插入/删除 | 𝑂(𝑛) | 𝑂(𝑛) |
随机访问 | 𝑂(1) | 𝑂(1) |
做任何pop操作之前,都要满足!q.empty()这个条件,我在这里没有注意,报错,debug了很久。
本题关键在于维护一个单调队列,保证队列里的数值从大到小。
但并不是保存滑动窗口里的每个值,而是保存每个有可能成为最大元素的值。
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
void pop(int x){
if(!q.empty()&&q.front()==x){
q.pop_front();
}
}
void push(int x){
while(!q.empty()&&q.back()<x)
q.pop_back();
q.push_back(x);
}
int getmaxvalue(){
return q.front();
}
347题 前k个高频元素
首先来看一下该问题的一个子问题:
给定一个数组,返回该数组中前k个最大的数。
该如何解决?
(1)可以将数组扫描一遍,每个元素都加入大顶堆之中,然后弹出k次,即前k个最大的值。
(2)用一个大小为k的小顶堆扫描数组,每个元素加入小顶堆之中。如果小顶堆size大于k,就弹出front元素,最后小顶堆中剩下的,就是k个最大的值。
前置知识:
在 C++ 中,std::pair<int, int> 是标准库中的一个模板类实例,属于 std::pair 的一种特定应用形式。它允许将两个类型相关或无关的值组合在一起,形成一个对象。以下是 std::pair<int, int> 的主要特点和使用方式:
std::pair 是一个模板类,定义在头文件 中:
template <typename T1, typename T2>
struct pair {
T1 first; // 第一个元素
T2 second; // 第二个元素
// 构造函数
pair();
pair(const T1& x, const T2& y);
pair(T1&& x, T2&& y);
template <typename U1, typename U2>
pair(U1&& x, U2&& y);
};
在容器中存储 pair 是常见的用法,特别是在需要关联两个值时。例如,可以用 std::vector<std::pair<int, int>> 存储一组键值对。
结合 STL 容器
在关联容器(如 std::map 和 std::set)中,std::pair 常被用来表示键值对。例如,std::map<int, int> 的元素实际上是 std::pair<const int, int>。
pair常用操作表
操作 | 说明 | 示例 |
---|---|---|
pair<T1, T2> p | 定义一个 pair 对象 | std::pair<int, int> p = {1, 2}; |
p.first | 获取 pair 的第一个元素 | std::cout << p.first; |
p.second | 获取 pair 的第二个元素 | std::cout << p.second; |
std::make_pair(x, y) | 快速创建一个 pair | auto p = std::make_pair(10, 20); |
比较操作符 <, >, ==, != | 按字典序比较 pair | if (p1 < p2) { /* ... */ } |
class Solution {
public:
//大顶堆
//该类定义了一个函数调用运算符(operator()),用来比较两个 std::pair<int, int> 对象。
class mycomparison{
public:
bool operator()(const pair<int,int>& l,const pair<int,int>& r){
return l.second<r.second;
}
};
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
vector<int> ans;
unordered_map<int,int> mymap;
//pair<int, int>: 队列中的元素类型,每个元素是一个 std::pair<int, int>。
//vector<pair<int, int>>: 底层容器类型,用于存储元素,默认为 std::vector。
//mycomparison: 自定义的比较器,用来确定优先级顺序。
priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison> q;
for(int num: nums){
mymap[num]++;
}
for(auto iter=mymap.begin();iter!=mymap.end();iter++){
q.push(*iter);
//if(q.size()>k) q.pop();
}
while(k--){
auto temp=q.top();
ans.push_back(temp.first);
q.pop();
}
return ans;
}
};
12.13.2024
继续对于算法的学习
贪心算法
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
贪心没有套路,说白了就是常识性推导加上举反例。
455题 分发饼干
该题最优解法是先用sort对孩子和饼干两个数组进行排序
再用双指针,从后往前扫描
我的做法出现的问题:
我尝试对每个饼干,找到孩子集合中胃口小于等于该饼干的第一个孩子
因为要找的条件是小于等于,所以只能反向找第一个大于该饼干的元素
如果没有找到,迭代器指向begin()
如果每个元素都小于该饼干,迭代器指向end()
multiset<int>::iterator temp=myset.upper_bound(s[i]);
if(temp!=myset.begin()){
temp--;
ans++;
myset.erase(temp);
}
376题 摆动序列
这种类型的题,要从样例入手,自己画一画。
并且样例可能是有坑的,不要被样例坑,影响思路。
一开始我写出了如下代码计算中间节点的波峰波谷个数:
int prediff=nums[i]-nums[i-1];
int curdiff=nums[i+1]-nums[i];
if(prediff*curdiff<0){}
然后再打算单独考虑数组的头和尾。
但这样无法正确处理有平坡的情况,遂放弃。
gpt给出了最优解法
分别用up和down表示以该元素结尾,且最后一个差值分别为正和负的最长子序列长度
那么只需要如下代码:
int up=1,down=1;
for(int i=1;i<nums.size();i++){
if(nums[i]>nums[i-1]) up=down+1;
if(nums[i]<nums[i-1]) down=up+1;
}
53题 最大子数组和
自己几乎秒解。如果up小于0,重新选当前的元素开始,否则加上前面的up。其实叫sum更合适。
//用up表示以该元素结尾,必包含该元素的子数组最大和
int up=0;
int ans=-100000;
for(int i=0;i<nums.size();i++){
up=max(up+nums[i],nums[i]);
ans=max(up,ans);
}
122题 买卖股票的最佳时机 II
个人理解是求解所有的波峰和波谷
写的代码用于分别判断波峰和波谷,整体较复杂
贪心做法:只要i+1和i两个差值是正的,就放入和之中
相当于将一次交易,拆解为多次交易。
for (int i = 1; i < n; ++i) {
ans += max(0, prices[i] - prices[i - 1]);
}
55题 跳跃游戏
这道题一开始没有做出来。
我想清楚了只需要维护最大可达下标,但是在更新这个dp的过程中一开始没有想清楚i<=dp;这个条件。用i<=nums.size()就不对了。
int dp=0;//最远可达下标
for(int i=0;i<=dp;i++){
if(dp>=nums.size()-1){
return true;
}
dp=max(dp,i+nums[i]);
}
问题的关键在于变更这个i遍历的范围。
45题 跳跃游戏 II
思路还是好想清楚的,也是需要更新最大可达下标。
不同点在于,使用尽可能少的跳跃次数,也即每次跳跃后,在新的可达范围中,寻找最大的i+nums[i](下次跳跃的最大可达下标),然后只更新一次最大可达下标,同时count++。
随想录:所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!
134题 加油站
135题 分发糖果
如果有两个维度均需要贪心,一定要两个维度分开的去贪心,固定一个维度考虑另一个维度
两个维度一起去考虑,一定会顾此失彼
左到右扫描一遍,右到左扫描一遍
406题 根据身高重建队列
总体来说,排序规则是一个二维的规则:
- 每个人,只需要考虑在他前面的,且比他身高要高的人。这是两个维度上的规则。
所以可以进行拆解:
- 先按身高从高到低(身高相同时,k更大的在右边)排序整个二维数组
- 对整个数组进行从左到右的扫描,每个人,根据其k值,插入到左侧下标为k的对应位置
使用vector的insert操作,复杂度O(n2)
class Solution {
public:
static bool cmp(const vector<int>&a,const vector<int> & b){
if(a[0]>b[0]) return true;
if((a[0]==b[0])) return a[1]<b[1];
return false;
}
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
// sort(people.begin(),people.end(),cmp);
sort(people.begin(),people.end(),[](const vector<int>&a,const vector<int> & b){
if(a[0]>b[0]) return true;
if((a[0]==b[0])) return a[1]<b[1];
return false;
});
vector<vector<int>> ans;
for(const auto& person:people){
ans.insert(ans.begin()+person[1],person);
}
return ans;
}
};
新学习到的是vector的insert操作,虽然是O(n)的复杂度,但是也需要知道,某些情况下可能会用到。
iterator insert(iterator pos, const T& value);
参数:
- pos: 插入的位置,是一个迭代器,指向向量中要插入元素的位置。
- value: 要插入的元素。
返回值: - 返回一个迭代器,指向新插入的元素的位置。
使用deque操作:
deque<vector<int>> que;
for(const auto& person:people){
que.insert(que.begin()+person[1],person);
}
return vector<vector<int>>(que.begin(),que.end());
452题 用最少数量的箭引爆气球
排序好之后,以气球为单位遍历,搞清楚什么时候需要弓箭加1。
如果i的左边界大于i-1的右边界,ans++;
否则更新i的有边界为i和i-1的右边界之中,最小的那个。(这里需要注意所有情况,一开始我直接设置为了i-1的右边界)
points[i][1]=min(points[i-1][1],points[i][1]);
435题 无重叠区间
和上一题思路非常相似,这道题问需要移除的区间的数量,以让所有区间都不相交。
所以当扫描遇到重叠区间时,ans++;
然后该移除哪个区间呢,肯定是移除右边界更长的那个区间,移除后,左边区间的作用域只到min(points[i-1][1],points[i][1])
位置。
763题 划分字母区间
这个题是自己想到的。
用哈希表存储每个字符最后出现的位置下标;
然后从左向右遍历,用一个临时变量temp记录左侧所有扫描过的元素的最后出现位置下标
temp=max(temp,mymap[s[i]]);
然后每当temp==i
即当前元素下标时,说明在此处切割的话,左侧所有字符都已不会在右侧再出现,记录这个位置。
需要注意边界记录:
int temp=0,last_temp=-1;
if(temp==i){
ans.push_back(temp-last_temp);
last_temp=temp;
temp=0;
}
56题 划分字母区间
100%
发现对于区间题,总是要先排序,按左边界递增排序。
本题的话,依旧按区间进行扫描,动态维护left和right,每当当前区间与之前的不重叠,保存之前的left与right构成的区间。
最后注意边界情况,最后一个[left, right]区间没有保存,需要单独保存。
738题 单调递增的数字
#include<algorithm>
#include<string>
string s{1,2,3,4};
vector<int> vec{1,2,3,4};
deque<int> dq{1,2,3,4};
// std::reverse可以翻转支持随机访问的容器
std::reverse(s.begin().s.end());
std::reverse(vec.begin().vec.end());
std::reverse(dq.begin().dq.end());
std::list<int> lst = {100, 200, 300, 400, 500};
lst.reverse(); // std::list 直接提供了 reverse() 成员函数
题目整体上不难,需要关注以下写法:
// string与int的互相转化
string strNum = to_string(N);
return stoi(strNum);
2.16.2025 二叉树
递归三部曲:
- 确定递归的参数和返回值
- 确定递归终止条件
- 确定单层递归的逻辑
94、144、145题 二叉树的前中后序遍历
迭代法
前序遍历
- 对于前序遍历,需要使用一个stack来压入节点node;总体来讲,就是先压入root根节点,然后在stack非空的情况下,先处理当前节点,在处理右、左节点(因为stack是逆序,倒着读就是左、右子树)
if(root==nullptr) return vector<int>{};
vector<int> ans;
stack<TreeNode*> mystack;
mystack.push(root);
while(!mystack.empty()){
TreeNode *node=mystack.top();
ans.push_back(node->val);
mystack.pop();
if(node->right!=nullptr) mystack.push(node->right);
if(node->left!=nullptr) mystack.push(node->left);
}
return ans;
后序遍历
- 后序遍历,只需要在前序遍历的基础上改三行代码,做两个操作:
- 交换处理子树的顺序,按照先左后右的方式
- 最后对ans做一个reverse
原因:前序遍历是中左右的顺序;如果交换子树处理顺序,就变成了中右左的顺序,此时reverse过来,正好是后序遍历的顺序:左右中。
102题 二叉树的层次遍历
队列。
两层循环,维护二叉树每一层节点总数的size;在第二层循环中,处理这一层每一个节点的同时,让左右两个子节点入队列。
101题 对称二叉树
做二叉树的题,首先可以以递归作为前提进行思考。
这个题可以拆解为子问题:给两颗树,判断两棵树是否镜像对称。
那么这个时候需要判断的是:
首先看当前节点是否相等,再看左树的左(对称于)右树的右&&左树的右(对称于)右树的左。
111题 二叉树的最小深度
这个题需要注意,不能直接用上一题的方法;
若某节点左子树为空,右子树不空,那么应该return minDepth(root->right)+1
。
110题 平衡二叉树
要注意平衡二叉树、完全二叉树、二叉搜索树的定义!!!
- 平衡二叉树:
- 每个节点的左右两个子树高度差的绝对值不超过1(空树也算)。
- 性质:左右两个子树也是平衡二叉树。
- 完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
- 二叉搜索树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
上述定义可以看到,平衡二叉树和二叉搜索树具有递归性质,即左右子树具有一样的性质。
对应数据结构:
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树
110题的解题思路呢,就是条件的交:
既要左右子树高度差不超过1,也需要左右子树的高度差不超过1。
bool isBalanced(TreeNode* root) {
if(root==nullptr) return true;
return abs(maxdepth(root->left)-maxdepth(root->right))<=1&&isBalanced(root->left) \
&&isBalanced(root->right);
257题 二叉树的所有路径
- 二叉树的题,写递归,要多去理解这个函数的输入和输出分别是什么
- 想象自己已经有了左右子节点作为输入的输出,那么该如何得到当前节点传入函数的输出结果
这个题就是在vector的合并上做文章
572题 另一棵树的子树
暴力深搜法:写一个判断树是否相同的函数。
树哈希:考虑把每个子树都映射成一个唯一的数,如果t对应的数字和s中任意一个子树映射的数字相等,则t是s的某一棵子树。
113题 路径总和II
什么时候push_back的,就什么时候pop_back(且不能在中间return!!!!);
如果在dfs的一开始,添加了current node到path,dfs结束时,也要pop_back current node。
千万注意不要在pop_back前return,等到函数自然结束就好。
class Solution {
public:
void dfs(TreeNode* root, int targetSum ,vector<int>& path, vector<vector<int>>& ans){
if(root==nullptr) return;
path.push_back(root->val);
if(root->left==nullptr&&root->right==nullptr){
if(root->val==targetSum){
ans.push_back(path);
}
// 不能在这里return!!!!
}
dfs(root->left,targetSum-root->val,path,ans);
dfs(root->right,targetSum-root->val,path,ans);
path.pop_back();
}
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> ans;
vector<int> path;
dfs(root, targetSum,path, ans);
return ans;
}
};
106题 从中序与后序遍历序列构造二叉树
可以直接使用迭代器从原vector中选取子区间
// 原始 vector
std::vector<int> original = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 选择连续的部分,从索引 start 到 end-1
int start = 2; // 起始索引
int end = 6; // 结束索引,注意 end 不包括在内
// 使用迭代器从原 vector 中选取子区间
std::vector<int> subVector(original.begin() + start, original.begin() + end);
分治思想
leetcode题解上,利用哈希表进行了优化,查找某元素位置使用了哈希表
654题 最大二叉树
留意这个题的单调栈的解法
当学习到单调栈后,需要回顾
98题 验证二叉搜索树
搜索二叉树,等价于:
中序遍历保存节点值,得到的序列是单调递增的。
501题 二叉搜索树中的众数
二叉搜索树,遍历起来一定要中序遍历
二叉搜索树,相邻的双指针遍历写法,模板如下:
class Solution {
public:
vector<int> ans;
int maxcount=1;
int count=1;
TreeNode *pre;
void dfs(TreeNode* cur){
if(cur==nullptr) return;
dfs(cur->left);
// 先把count变化的逻辑处理完,在去判断现在的count和maxcount的关系,否则会忽略极端情况
if(pre==nullptr) count=1;
else if(pre->val==cur->val) count++;
else count=1;
if(count>maxcount){
maxcount=count;
ans.clear();
ans.push_back(cur->val);
}
else if(count==maxcount) ans.push_back(cur->val);
pre=cur;
dfs(cur->right);
}
vector<int> findMode(TreeNode* root) {
dfs(root);
return ans;
}
};
// 双指针法
int ans=std::numeric_limits<int>::max();
236题 二叉树的最近公共祖先
想自下而上的寻找或者扫描,要用后序遍历。
这个题要摒弃写递归的传统逻辑:这个dfs遍历树的函数,一定是带着某种意义的
本题的dfs是这个含义:以root为根节点的子树中,尽可能的包含p或者q的最深节点(最近祖先)
但这个含义是从结果导出的,不能这样思考,应该想着自底向上我该如何一步一步返回节点值,能使得在根节点处获取到想要的结果。同时要考虑上所有情况。
TreeNode* dfs(TreeNode* root,TreeNode* p,TreeNode* q){
if(root==nullptr) return nullptr;
if(root==p||root==q) return root;
TreeNode* left=dfs(root->left,p,q);
TreeNode* right=dfs(root->right,p,q);
if(left!=nullptr&&right!=nullptr) return root;
if(left==nullptr) return right;
return left;
}
return root
的两句话分别包含了两种情况。
回溯算法
216题 组合总和III
本题有剪枝操作:
1.总的sum大于目标sum时。
2.剩余的可搜索元素个数(最多到9)不足满足综合k个条件时。
if(sum>n) return;
for(int i=index;i<=10-(k-path.size());i++)
5题 电话号码的字母组合
对于组合问题:
如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合
46. 全排列
组合问题分为:
- 每个数字只能选取一次
- 代码:
for(int i=index;i<nums.size();i++){ int num=nums[i]; path.push_back(num); dfs(nums,i+1); path.pop_back(); }
- index<=i<j
- 输出:
[[1,2,3]]
- 每个数字可选取多次
- 代码:
for(int i=index;i<nums.size();i++){ int num=nums[i]; path.push_back(num); dfs(nums,i); path.pop_back(); }
- index<=i<j
- 输出:
[[1,1,1],[1,1,2],[1,1,3],[1,2,2],[1,2,3],[1,3,3],[2,2,2],[2,2,3],[2,3,3],[3,3,3]]
全排列问题分为:
- 每个数字只能选取一次
- 用used数组来记录哪些位置已经添加到了path中 - 每个数字可以选取多次
图论算法
图的基本概念:
- 无向图/有向图
- 度:入度/出度
- 连通块(无向图和有向图的连通性不同)
图的存储方式:
- 邻接矩阵
- 邻接表unordered_map
vector<vector<int>> graph;
图的遍历方式基本是两大类:
- 深度优先搜索(dfs)
- 广度优先搜索(bfs)
拓扑排序
对于有向图
797题 所有可能路径
下面的这个代码是反例:
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void dfs(vector<vector<int>>& graph, int start, int target){
path.push_back(start);
if(start==target){
ans.push_back(path);
return;
}
for(int neighbor: graph[start]){
dfs(graph,neighbor,target);
}
path.pop_back();
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
int n=graph.size();
dfs(graph,0,n-1);
return ans;
}
};
/*
这个代码不对,我既然把path的push_back放在dfs函数的一开始,那么我在pop_back之前肯定不能return掉
总的来说,就是push_back和pop_back之间不能让函数结束
*/
200题 岛屿数量
这个题很常见,但作为一道模板题,条件比较多:
对于dfs:
- 如果当前的坐标超出边界,return掉
- 坐标对应的点已经标记为visited,return掉
- 该点对应的字符是0,return掉
class Solution {
public:
int n,m;
int dx[4]={-1,0,0,1};
int dy[4]={0,-1,1,0};
void dfs(vector<vector<char>>& grid,vector<vector<bool>>& visited,int i, int j){
if(i<0||j<0||i>n-1||j>m-1||visited[i][j]||grid[i][j]=='0') return;
visited[i][j]=true;
for(int k=0;k<4;k++){
int next_x=i+dx[k],next_y=j+dy[k];
dfs(grid,visited,next_x,next_y);
}
}
int numIslands(vector<vector<char>>& grid) {
n=grid.size();
m=grid[0].size();
vector<vector<bool>> visited(n,vector<bool>(m,false));
int ans=0;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++){
if(visited[i][j]||grid[i][j]=='0') continue;
ans++;
dfs(grid,visited,i,j);
}
return ans;
}
};