1、续双指针
1.1 leetcode 977
第一次代码:(第一天做过)
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> result;
int start = 0;
int end = nums.size() - 1;//两边待排中最大
while(start <= end) {//有等号把最后一个值包含在内,先按倒序排列(push_back()插入在队尾)
if(nums[start]*nums[start] <= nums[end]*nums[end]) {
result.push_back(nums[end]*nums[end]);
end--;
}
else {
result.push_back(nums[start]*nums[start]);
start++;
}
}
reverse(result.begin(), result.end());
return result;
}
};
其实可以使用insert()(见1.3)解决从后往前插入的问题。
使用暴力排序,每个数平方之后,快排
class Solution {
public:
vector<int> sortedSquares(vector<int>& A) {
for (int i = 0; i < A.size(); i++) {
A[i] *= A[i];
}
sort(A.begin(), A.end()); // 快速排序
return A;
}
};
这个时间复杂度是 O(n + nlogn)
1.2 双指针总结
双指针指向:
1、一快一慢(一个指向目标位置一个指向元素位置)
2、两头(如本题:数组其实是有序的, 只不过负数平方之后可能成为最大数了。那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。此时可以考虑双指针法了,i指向起始位置,j指向终止位置。)
注意可以使用vector result(A.size(), 0);对result数组初始化这样就可以直接从后面加入了
3、滑动窗口(见 2.1-2.5 leetcode 209)
1.3 vector.insert()
参考博客:vector中insert()的用法详解 C++ vector用法详解
insert() 函数有以下三种用法:
1、在指定位置loc前插入值为val的元素,返回指向这个元素的迭代器
2、在指定位置loc前插入num个值为val的元素
3、在指定位置loc前插入区间[start, end)的所有元素
使用insert()方式插入元素:
insert() //往vector任意位置插入一个元素,指定位置或者指定区间进行插入,
//第一个参数是个迭代器,第二个参数是元素。返回值是指向新元素的迭代器
vector<int> vA;
vector<int>::iterator it;
//指定位置插入
//iterator insert(const_iterator _Where, const _Ty& _Val)
//第一个参数是个迭代器位置,第二个参数是元素
it = vA.insert(vA.begin(),2); //往begin()之前插入一个int元素2 (vA={2,1}) 此时*it=2
//指定位置插入
//void insert(const_iterator _Where, size_type _Count, const _Ty& _Val)
//第一个参数是个迭代器位置,第二个参数是要插入的元素个数,第三个参数是元素值
it = vA.insert(vA.end(),2,3);//往end()之前插入2个int元素3 (vA={2,1,3,3}) 此时*it=3
//指定区间插入
//void insert(const_iterator _Where, _Iter _First, _Iter _Last)
vector<int> vB(3,6); //vector<类型>标识符(最大容量,初始所有值)
it = vA.insert(vA.end(),vB.begin(),vB.end()); //把vB中所有元素插入到vA的end()之前 (vA={2,1,3,3,6,6,6})
//此时*it=6,指向最后一个元素值为6的元素位置
//删除元素操作:
pop_back() 从vector末尾删除一个元素
erase() 从vector任意位置删除一个元素,指定位置或者指定区间进行删除,第一个参数都是个迭代器。返回值是指向删除后的下一个元素的迭代器
clear() 清除vector中所有元素, size=0, 不会改变原有capacity值
示例:
//创建一个vector,置入字母表的前十个字符
vector <char> Avector;
for( int i=0; i < 10; i++ )
Avector.push_back( i + 65 );
//插入四个C到vector中
vector <char>::iterator theIterator = Avector.begin();
Avector.insert( theIterator, 4, 'C' );
//显示vector的内容
for( theIterator = Avector.begin(); theIterator != Avector.end(); theIterator++ )
cout < < *theIterator;
这段代码将显示:CCCCABCDEFGHIJ
2、双指针 滑动窗口
2.1 leetcode 209(Time Limit Exceeded)
第一遍暴力法报错:Time Limit Exceeded
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
//思路是由于从某一元素开始的最大序列之和固定,因为元素均为正,从前往后找即可(再设一个队尾指针),超过target就停止,超不过记为-1
vector<int> re(nums.size(), -1);//存储对应元素开始的符合要求的最大长度
for(int start = 0; start < nums.size(); start++) {
int sum = 0;
for(int end = start; end < nums.size(); end++) {
sum += nums[end];
if(sum >= target) {
re[start] = end - start + 1;
break;
}
}
}
int min = re.size() + 1;
for(int i = 0; i < re.size(); i++) {
cout << re[i] << " ";
if(re[i] < min && re[i] > 0) {
min = re[i];
}
}
if(min == re.size() + 1) {
return 0;
}
else {
return min;//可以简写为return result == re.size()+1? 0 : result;
}
}
};
滑动窗口
双指针数组操作中第三个重要的方法:滑动窗口。
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。
滑动窗口用一个for循环来完成这个操作。
2.2 滑动窗口使用前提
1、需要在遍历可能性时下一个答案的 连续 的一部分(不能有间隔)为上一个答案中的连续的一部分
2、随着结束位置的移动起始位置一定是停止或者随着向右移动的(不会左右晃)
2.3 滑动窗口思考过程
首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。
如果只用一个for循环来表示 滑动窗口的起始位置,那么如果考虑遍历剩下的终止位置(第一次错误想法的来源)此时难免再次陷入 暴力解法的怪圈。
其实以开始位置为for唯一循环也可以,见后文2.5。与终止位置为循环索引不同的是,每次定下初始位置后移动终止位置时均为不符合要求的情况,直到符合条件再移动初始位置,出while循环为符合要求的情况,附加的,end也不再多走一步了。当然由于移动的是终止位置所以保证终止位置在出while循环时不越界。
只用一个for循环,那么这个循环的索引,可以是表示 滑动窗口的终止位置。
以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:
终止位置为循环索引动图 代码随想录
滑动窗口,主要确定如下三点:
1、窗口内是什么?
2、如何移动窗口的起始位置?
3、如何移动窗口的结束位置?
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
2.4 leetcode 209:以结束位置为循环索引(for唯一循环)
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。(因为随着结束位置的移动起始位置一定是停止或者随着向右移动的(不会左右晃))可以通过找个变量记录结果(result),这样不用把结果填入数组一起比大小
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = __INT32_MAX__;
int start = 0;
int sum = 0;
for(int end = 0; end < nums.size(); end++) {
int len = end - start + 1;
sum += nums[end];//结束指针往后移别忘了加上加入的元素
while(sum >= target) {
//while是关键,不断调整start直至最短,如果start不再满足了,多走了一位,因为随之end也会走,所以多走的一位并不会需要start往回走
result = len >= result ? result : len;
sum -= nums[start];
start++;
len--;
}
}
return result == __INT32_MAX__ ? 0 : result;
}
};
自己实现:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int start = 0;
int sum = 0;
int res = __INT32_MAX__;
for (int end = 0; end < nums.size(); end++) {
sum += nums[end];
while (sum >= target && start <= end) {
int len = end - start + 1;
if (res > len)
res = len;
sum -= nums[start];
start++;
}
}
return res == __INT32_MAX__ ? 0 : res;
}
};
只要 sum >= target 就不可能出现 start <= end,多余了
2.5 leetcode 209:以开始位置为循环索引(for唯一循环)
以开始位置为for循环索引:(麻烦,滑动窗口还是以终止位置为索引)
窗口的开始位置如何移动:如果当前窗口的值小于s了,窗口就要向前移动了(也就是该放大了)。(因为随着开始位置的移动结束位置一定是停止或者随着向右移动的(不会左右晃))
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = __INT32_MAX__;
int end = 0;
int sum = nums[0];//开始包含第一个(start = end = 0)因为后面不会加了
int len = 2;//这里是一种写法:len不在过程中用end-start+1,后面的实现会一直用end - start + 1计算
//start=0减一所以初始值为2
for(int start = 0; start < nums.size(); start++) {
len--;
if(start > 0) {
sum -= nums[start-1];//减前一个位置的nums,根据start的理解容易得出
}
while(sum < target && end < nums.size() - 1) {
end++;
len++;
sum += nums[end];//考虑end = nums.size() - 1的情况应该end先加一再加sum
}//出去除了end超范围其余符合条件
if(sum >= target) {
result = result > len ? len : result;
}
}
return result == __INT32_MAX__ ? 0 : result;
}
};
自己同样思路的实现(一直用end - start + 1计算)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int end = 0;
int sum = nums[0];
int res = __INT32_MAX__;
for (int start = 0; start < nums.size(); start++) {
if (start > 0)
sum -= nums[start - 1];
while (sum < target && end != nums.size() - 1) { // 注意可能找到最后还没凑够,end就越界了
end++;
sum += nums[end];
}
if (sum >= target) { // 注意不能在因为没凑够出循环的时候 更新
int len = end - start + 1;
if (res > len)
res = len;
}
}
return res == __INT32_MAX__ ? 0 : res;
}
};
2.6 leetcode 904:滑动窗口使用,哈希表unordered_map使用
采用上文leetcode 209中的以结束位置为循环索引。因为随着结束位置的右移,开始位置停止或右移,同时对于下一段可能的区间而言是由上一次的区间的连续的子区间组成的。
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int cater_num = 0;//记录水果的种类数
vector<int> cater(fruits.size(), 0);//记录水果的种类以及每种种类对应的数量,因为0 <= fruits[i] < fruits.length
//以结束位置为for循环索引
int res = 0;//最终结果
int num = 0;
int start = 0;
for(int end = 0; end < fruits.size(); end++) {
if(cater[fruits[end]] == 0) {//end变更新变量
cater_num++;
}
num++;
cater[fruits[end]]++;
while(cater_num > 2) {//start变更新变量,注意限制条件出while循环为符合条件的情况(与leetcode 209以开始位置为循环索引的情况一致)
cater[fruits[start]]--;
if(cater[fruits[start]] == 0) {
cater_num--;
}
start++;
num--;
}
res = num > res ? num : res;
}
return res;
}
};
可以使用哈希表(与上面代码的vector<int> cater 以及 int cater_num 的组合体
一致)存储这个窗口内的数以及出现的次数。
我们每次将 right移动一个位置,并将 fruits[right]加入哈希表。如果此时哈希表不满足要求(即哈希表中出现超过两个键值对),那么我们需要不断移动 left,并将 fruits[left]从哈希表中移除,直到哈希表满足要求为止。
需要注意的是,因为使用了哈希表的大小作为种类数量,所以将fruits[left]从哈希表中移除后,如果fruits[left]在哈希表中的出现次数减少为0,需要将对应的键值对从哈希表中移除。
unordered_map支持通过 下标加入元素;size成员函数 求的是键值对的个数;erase参数为迭代器 删除键值对;find参数为迭代器,返回对应key的迭代器
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int n = fruits.size();
unordered_map<int, int> cnt;
int left = 0, ans = 0;
for (int right = 0; right < n; ++right) {
++cnt[fruits[right]];
while (cnt.size() > 2) {
auto it = cnt.find(fruits[left]);
--it->second;
if (it->second == 0) {
cnt.erase(it);
}
++left;
}
ans = max(ans, right - left + 1);
}
return ans;
}
};
对于unordered_map值的减少 不使用 迭代器,直接使用下标,跟加入键值对一样
思路过程 其余部分都一致
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int start = 0;
unordered_map<int, int> tmp_fruit;
int total_num = 0;
int res = 0;
for (int end = 0; end != fruits.size(); end++) {
total_num++;
tmp_fruit[fruits[end]]++;
while (tmp_fruit.size() > 2) {
tmp_fruit[fruits[start]]--;
if (tmp_fruit[fruits[start]] == 0)
tmp_fruit.erase(tmp_fruit.find(fruits[start]));
start++;
total_num--;
}
if (total_num > res) res = total_num;
}
return res;
}
};
2.7 leetcode 76:确定字符串 包含所有字符
第一遍代码错误,注意不仅是要覆盖所有字母,个数也要一致,所以错了,而find每次只能找第一个
如输入:s=“a”, t=“aa”, 错误输出:“a”, 正确输出:“”
但是了解了 string 和 unordered_map的很多成员函数:
(1) std::string::substr()
string x=s.substr() //默认时的长度为从开始位置到尾
string y=s.substr(5) //获得字符串s中 从第5位开始到尾的字符串
string z=s.substr(5,3); //获得字符串s中 从第5位开始的长度为3的字符串
//方法返回指向元素的迭代器,否则返回指向 map::end() 的迭代器
cnt.find(s[start]) != cnt.end()
std::string str("This is an example sentence.");
str.erase(10, 8); // 从第10位(下标为10)开始,长度为8的字符串
str.erase(str.begin() + 9); // 删除str.begin()+9迭代器对应的字符,即字符“n”
str.erase(str.begin() + 5, str.end() - 9); // 删除指定范围的字符串
(4)std::string::find() / rfind() / find_first_of() / find_last_of()
如果没有找到,那么会返回一个特别的标记npos,一般写作string::npos
string s, c;
int main() {
s = "laaaal";
c = "l";
int index = s.find(c,3);//从字符串s下标3的位置开始寻找
if (index != string::npos)
cout << index << endl;
}
整体错误代码:
class Solution {
public:
string minWindow(string s, string t) {
int start = 0;
int sub_len = __INT32_MAX__;
int res_start = 0;
unordered_map<char, bool> cnt; // 注意不仅是要覆盖所有字母,个数也要一致,所以错了,而find每次只能找第一个
//如输入:s="a", t="aa", 错误输出:"a", 正确输出:""
for (auto it = t.begin(); it != t.end(); it++) {
cnt[*it] = true;
}
unordered_map<char, bool> cnt_tmp = cnt;
for (int end = 0; end != s.size(); end++) {
auto if_in = cnt_tmp.find(s[end]);
if(if_in != cnt_tmp.end())
cnt_tmp.erase(if_in);
while (cnt_tmp.size() == 0) {
int len = end - start + 1;
if (len < sub_len) {
sub_len = len;
res_start = start;
}
string sub_tmp = s.substr(start + 1, end - start);
if (cnt.find(s[start]) != cnt.end() && sub_tmp.find(s[start]) == string::npos)
cnt_tmp[s[start]] = true;
start++;
}
}
return sub_len == __INT32_MAX__ ? "" : s.substr(res_start, sub_len);
}
};
本题题目中 强调了连续(考虑滑动窗口的条件) 以及 实际测试中发现存在重复字符(不存在重复字符错误代码就对了)
关键是 如何确定包含所有字符?其实就是看其子串元素的频数大于等于目标串(错误代码没实现的地方),而且由于每次 边界的移动 实际上只会更改一个频数,其实distance就是衡量 s字符串 与 t字符串的接近程度 的一个变量
distance这样设计的原因是 多是可以接受的
对比 暴力解法,左右边界交替移动的操作 节省了很多不必要的子区间的比较
头尾指针交替为循环索引,右边界优先
短的看了更长的就不用看了,右边界移动的时候左边界固定,左边界移动的时候右边界固定
先向右移动让字符串区间包含所有字符,再移动左边界直至不满足是子串为止;再向右移动,向左移动…
题解链接
自己实现代码:
其实对于左右边界移动的时候 操作是对称的,一加一减,无非就是左边界移动之前 要记录一下 符合条件的字符串长度 和 起始位置
class Solution {
public:
string minWindow(string s, string t) {
int left = 0;
int distance = 0;
int res_len = __INT32_MAX__;
int res_left = 0;
unordered_map<char, int> cnt;
for (auto it = t.begin(); it != t.end(); it++) {
cnt[*it]++;
}
unordered_map<char, int> cns;
for (int right = 0; right < s.size(); right++) {
if (cnt.find(s[right]) != cnt.end() && cns[s[right]] < cnt[s[right]]) { // 对应图片中右边界向右滑动的部分
distance++;
}
if (cnt.find(s[right]) != cnt.end())
cns[s[right]]++;
while (distance == t.size()) {
int len = right - left + 1;
if (res_len > len) {
res_len = len;
res_left = left;
}
if (cnt.find(s[left]) != cnt.end() && cns[s[left]] == cnt[s[left]]) 对应图片中左边界向右滑动的部分
distance--;
if (cnt.find(s[left]) != cnt.end())
cns[s[left]]--;
left++;
}
}
return res_len == __INT32_MAX__ ? "" : s.substr(res_left, res_len); // 不符合条件就返回空字符串
}
};
3、模拟过程(找规律)
3.1 leetcode 59:模拟 正方形矩阵 绕圈遍历
第一遍代码(思路跟下面其实差不多):
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
//找规律填充,每一行填充都要与loop/loop_num有关(可以改变行数),每一行左闭右开
vector<vector<int>> re(n, vector<int>(n, 0));//注意二维数组初始化方法
int loop_num = n;//每一圈每一条边长度
int loop = 1;//第几圈,可以与元素放置的位置产生联系
int num = 1;//待放入的元素
while(loop_num>1) {//1/2=0,如果是loop_num=1,特别是奇数的时候最后一圈为1不能再放入循环,因为num++会覆盖掉之前的
loop_num--;//因为左闭右开,实际走的时候需要-1
for(int i = loop - 1; i < loop_num + loop - 1; i++) {
re[loop - 1][i] = num++;
}
for(int j = loop - 1; j < loop_num + loop - 1; j++) {
re[j][loop_num + loop - 1] = num++;//注意利用正方形边长相等性质简化坐标的书写
}
for(int i = loop_num + loop - 1; i > loop - 1; i--) {
re[loop_num + loop - 1][i] = num++;
}
for(int j = loop_num + loop - 1; j > loop-1; j--) {
re[j][loop - 1] = num++;
}
loop_num = (loop_num+1)-2;//边长为差为2的等差数列
cout << loop_num;
loop++;
}
if(n%2 == 1) {
re[loop-1][loop-1] = num;
}
return re;
}
};
模拟顺时针画矩阵的过程:
1、填充上行从左到右
2、填充右列从上到下
3、填充下行从右到左
4、填充左列从下到上
由外向内一圈一圈这么画下去,画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
第一次代码中矩阵的赋值没有条理,可以与双指针一样,研究填充每一条边开头和结尾。每一圈结尾-1(这里使用边的长度,因为正方形边长相等,这样每次统一-1即可),开始(分成行开始和列开始startx/starty(其实可以共用,但是代码随想录这样更清晰))+1,无论是行还是列,这样再确认一个圈数和每圈初始行列位置就行了。中间单独的一个值还是单独给。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
3.2 leetcode 54:模拟 长方形矩阵 绕圈遍历
对于二维vector变量vv,vv.size()的值vector的行数,vv[i].size()的值是vector的列数
用3.1的套路,左闭右开,就是最后补一行 变成 可能补一行 / 一列,看行列谁小了
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
int loop_num = min(m, n) / 2; //圈数,当然去掉了中间的一行
int cnt = 0; //行列每圈开始的位置,以及使用它构造结束位置
vector<int> res;
while (cnt < loop_num) {
for (int j = cnt; j < n - cnt - 1; j++) {
res.push_back(matrix[cnt][j]);
}
for (int i = cnt; i < m - cnt - 1; i++) {
res.push_back(matrix[i][n - cnt - 1]);
}
for (int j = n - cnt - 1; j > cnt; j--) {
res.push_back(matrix[m - cnt - 1][j]);
}
for (int i = m - cnt - 1; i > cnt; i--) {
res.push_back(matrix[i][cnt]);
}
cnt++;
}
if (min(m, n) % 2 == 1) { // //需要补中间一行 / 一列
if (min(m, n) == m) {
for (int j = cnt; j < n - cnt; j++) {
res.push_back(matrix[cnt][j]);
}
}
else {
for (int i = cnt; i < m - cnt; i++) {
res.push_back(matrix[i][cnt]);
}
}
}
return res;
}
};
另一种左闭右闭的思路(先把某行全部遍历完,再把某列(去掉第一个元素了)遍历完),每一条边循环完后要重新设定边界,若上边界大于下边界 或者 左边界大于右边界,则遍历遍历完成
解题思路:
这里的方法不需要记录已经走过的路径,所以执行用时和内存消耗都相对较小
1、首先设定上下左右边界
2、其次向右移动到最右,此时第一行因为已经使用过了,可以将其从图中删去,体现在代码中就是重新定义上边界
3、判断若重新定义后,上下边界交错,表明螺旋矩阵遍历结束,跳出循环,返回答案
4、若上下边界不交错,则遍历还未结束,接着向下向左向上移动,操作过程与第一,二步同理
5、不断循环以上步骤,直到某两条边界交错,跳出循环,返回答案
本方法不需要单独处理 最里面的一行 / 列
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
//先定义上下左右边界,遍历完(左闭右闭)就把那一行/一列去了,直到没有遍历的元素了就结束
int left = 0;
int right = matrix[0].size() - 1;
int up = 0;
int down = matrix.size() - 1;
vector<int> re;
while(true) {
for(int j = left; j <= right; j++) {
re.push_back(matrix[up][j]);
}
up++;//一行完全遍历完
if(up > down) break;
for(int i = up; i <= down; i++) {
re.push_back(matrix[i][right]);
}
right--;
if(left > right) break;
for(int j = right; j >= left; j--) {
re.push_back(matrix[down][j]);
}
down--;
if(up > down) break;
for(int i = down; i >= up; i--) {
re.push_back(matrix[i][left]);
}
left++;
if(left > right) break;
}
return re;
}
};