885.螺旋矩阵III
885. 螺旋矩阵 III - 力扣(LeetCode)https://leetcode.cn/problems/spiral-matrix-iii/?envType=list&envId=ZCa7r67M模拟题的一种,说难也难,说简单也简单。模拟题有很多套路题,它们的题解差不多,但是也有一些是没做过,不容易想出来的,或者说能想到大概思路,但不容易想全面的。
这道模拟螺旋矩阵III是要我们返回,模拟矩阵的螺旋遍历时,每次经过的给定矩阵的坐标都是什么
题解思路:
根据题目中图的示例我们可以观察出,每两次调转方向内走的步数是相等的,且每次走完当前方向的最大步数,调转一次方向,且每调两次方向后,所能走的最大步数加1。
观察出这些后,问题才能迎刃而解
创建一个二维数组around来记录本次方向下一步往哪走。然后创建上面观察出的需要的各个变量,即当前调转了几次方向,当前的方向,当前走的步数,当前方向最大步数,还要有两个变量xy,它们代表了当前遍历到的位置。
注意xy是可以出矩阵边界的,我们只需要模拟就可以了,这和题目给出的图示完全相符,我们看当前xy位置是不是在矩阵里,如果是,将它录入到返回数组中,如果不是接着走就可以了。
循环结束条件:用一个变量num记录当前已经走过了矩阵的几个数,当num==矩阵行×列,即可退出循环,表明该矩阵所有部分全走完了,返回记录的数组。
难点:没做过,很难想到用around数组辅助记录各方向上x和y的变化取值,和如何用方向去使用around数组,循环结束条件是什么?如果没做过类似题,甚至都不会注意,每调转两次方向会使最大步数增加。总之不要小看模拟题,有时候模拟题,也不是很容易的想出
class Solution {
public:
vector<vector<int>> spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
vector<vector<int>>res;
int maxc=1,count=0,dir=0,cdir=0;
vector<pair<int,int>>around={{0,1},{1,0},{0,-1},{-1,0}};
int num=1,x=rStart,y=cStart;
while(num<=rows*cols){
if(x>=0&&x<rows&&y>=0&&y<cols){
res.push_back({x,y});++num;
}
if(count==maxc){
cdir++;count=0;dir++;
}
if(cdir==2){
cdir=0;maxc++;
}
x+=around[dir%4].first,y+=around[dir%4].second;count++;
}
return res;
}
};
方向只有上下左右四个方向,图示我们看的出来每次必定向右走然后向下走......所以dir自增后超出了4模4可以使它仍然正确的去遍历。
当然你也可以在调转三次方向后,把方向变成0,重新开始,然后弄出几个循环去连续判断,此时dir对应哪个方向,再去调整xy下一次坐标,很多其他题解采用了这样的思路,但本质上依旧脱离不开模拟,且是相同的思路。
递归应该也可以,相同的思路,然后把每次要用到的变量做参数传到下一次递归里,但是我没怎么写过递归,而且可能会很慢,这里不再展开说明。
14.最长公共前缀
14. 最长公共前缀 - 力扣(LeetCode)https://leetcode.cn/problems/longest-common-prefix/?envType=list&envId=ZCa7r67M官方题解的横向遍历纵向遍历实际上我认为就是广搜和深搜的思想。
这里都分析一下:
先说纵向遍历,我本人喜欢用这种方法,记的也是这种方法。(其实不用一道题非要记很多种解法,记住一种能方便刷题,也能加深印象,当然你是大佬当我没说吧)
纵向遍历的思想就是字面意思,以第一个字符串为模板,挨个拿第一个字符串的各个字符去和其他的字符串的对应下标字符去比较,也就是一列一列比较。如果不匹配,直接return即可,因为我们这道题求得是最长公共前缀,不是最长公共子串,所以以开头到这个字符为止只要有字符不等,则不该再遍历了,直接返回当前最大子串即可,这里要注意的是前缀这一特性。
说说剪枝:这道题是可以剪枝的,事实上也必须要这样做。
假如该字符串数组中有某一个字符串长度很短,至少它短于第一个字符串,那么我们在访问该字符串的这个对应下标很可能造成下标越界访问,所以判断一下如果此时要检查的下标i已经和该次要判断的字符串长度都相等了,那么也直接return就可以了。这里要注意的是公共这一特性。
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if(strs.size()==1)return strs[0];
string s="";
for(int i=0;i<strs[0].size();++i){
char ch=strs[0][i];
for(int j=1;j<strs.size();++j){
if(strs[j].size()==i||strs[j][i]!=ch){return s;}
}
s=strs[0].substr(0,i+1);
}
return s;
}
};
这里还可以直接拿一个字符串s2保存strs【0】,这样每次判断的是s2【i】对应的字符,有些题解就是这样写的,这只是把二维书写转为一维了思路是完全一样的。
再来看看横向遍历:
思想就是两个字符串两个字符串比较,那么是哪两个字符串呢?是滑窗那样的两个字符串依次比较吗?那肯定不是,我们还是拿第一个字符串做样例串,一个函数去判断该次遍历到的字符串和样例串截至到哪个字符不等,不等跳出函数循环,然后截取样例串作为下一次判断的样例串,样例串越截越短,它代表的是上一次的求得的最长前缀公共串的结果,将该结果与其他所有字符串相比较,最后把它们共有的公共前缀返回去即可。
class Solution {
public:
string get(string&s1,string &s2){
int c=min(s1.size(),s2.size());
int index=0;
while(index<c&&s1[index]==s2[index]){
index++;
}
return s1.substr(0,index);
}
string longestCommonPrefix(vector<string>& strs) {
string res=strs[0];
if(!strs.size())return "";
for(int i=1;i<strs.size();i++){
res=get(strs[i],res);
}
return res;
}
};
上面是官方代码的实现,再贴一个自己实现的横向遍历
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if(!strs.size())return "";
string res=strs[0];
for(int i=1;i<strs.size();i++){
int count=min(res.size(),strs[i].size());
int j=0;
for(;j<count;j++){
if(strs[i][j]!=res[j]){break;}
}
res=strs[i].substr(0,j);
}
return res;
}
};
//["ab", "a"]注意这个用例
思路是相同的,我们这里用的是count来帮助判断上一次最长前缀公共子串到哪截止,而不是像上一个题解一样用截取的来判断,但思路是一样的,不要忘记把j写外面,这样有助于我们保存答案,
两点需要注意:
里层循环更新数据,用来比较当前字符串字符各个位置和前缀字符串的
剪枝方法是以两者较小长度作为循环判断结束条件
第二点,一旦不符合,要break跳出来,再进行前缀字符串的更新!!!
这是避免测试用例【a,b】和【a】这种情况发生
这种情况由于我们使用最小长度判定,所以即使前缀是a也会返回ab
因为循环根本进不去,所以一定要在里层循环外面更新
最好按官方题解写更加简洁,且不容易出错,这种写在一起很容易出错
3.无重复字符的最长字串
3. 无重复字符的最长子串 - 力扣(LeetCode)https://leetcode.cn/problems/longest-substring-without-repeating-characters/?envType=list&envId=ZCa7r67M求最长子串并且是不含重复字符的最长字串。
我们一点点分析这道题,就会发现这道题没有想象中那么难
首先无重复如何实现?使用哈希表
最长字串如何实现?它不是前缀串,所以不能只有一个变量记录截至到哪符合条件,而且要求的是子串,子串是什么?连续的啊,自然而然想到滑窗
这是一道很好的典型的哈希表搭配滑窗解决的问题。
这里给出代码实现思路:
代码思路是用一个循环,循环变量是i,但我们并不让循环自动推进i,而是它只起到判断i是否超出字符串范围的作用,这里i相当于右窗口。其实这里写成一个while也一样。然后外面一个变量left记录此时子串从哪个下标开始,里层循环进来就判断,如果循环变量i大于0,也就是说不是刚走进来的,那么直接删掉哈希表中此时子串的头元素,也就是left指向的那个,然后left++,这是为什么呢?
这句判断后面跟的代码是一层里层循环,它是实现推进右窗口的,不停推右窗口,只有两种情况会停止,一是越界,二是此时我们的子串右窗口已经遍历到了一个重复的字符,这时候停止推进右窗口,取答案后,再进行外层循环的,if判断,删掉一个左窗口元素后,再推进右窗口,如果此时仍然是哈希表里能找到该右窗口的元素,那么继续缩减左窗口。
这道题我会有多个方法来说,让大家更深刻的理解这道题的各种代码写法
先解答一些疑问:
为什么我们不用判断窗口此时是否含有相等的字符就取答案?用了什么操作使答案能保持正确?
为什么思路看起来这么奇怪,如果是外层循环控制右窗口的扩大每次进行++,然后里层循环if判断如果此时子串遍历过程中存在了重复字符的问题,再使用上述的删除left字符的方法,left++行不行?
为什么我们删除的是最左边字符?重复的字符很有可能不在最左边啊?
比如对于测试样例【pwwkew】我们完全可以删除本次找到的那个重复字符之前的所有字符,然后以本次的w为左窗口之后子串扩大窗口,而且该字符之前的最长情况已经被答案所记录,也不用担心我们错过最长子串的情况,还能提高效率,为什么不这么写?
下面我将对以上问题做出一一解答,并告诉大家代码都可以怎么写,而怎么写看起来是对的,但实际上有大漏洞,听我慢慢刨析。
第一个问题,和我们的答案取值有关看代码
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int res=0;
if(s.size()<=1)return s.size();
unordered_set<char>set;int left=0;
for(int i=0;i<s.size();){
if(i>0){set.erase(s[left]);left++;}
while(i<s.size()&&set.find(s[i])==set.end()){
set.insert(s[i]);++i;
}
res=max(res,i-left);
}
return res;
}
};
这样的代码取值保证了我们只会取到最大子串长度,而我们第一次因为字符重复退出时,此时的右窗口下标对应的下标减去左窗口对应的下标刚好等于窗口长度而不用+1,这个大家应该都能理解,这就是恰好超过一个,你想想如果该窗口的答案正好是最长的不重复子串,你的i-left得到的值是不是比窗口大小小1?这是下标从0走导致的
再看,我们已知它会因为此时i向后遍历找到字符与此子串中某字符重复时候,就无法进入循环,直到上面的if删除把前面的重复字符删掉后,才能进来,所以不用担心,这一过程越取越小,不会吧错误答案加进来。
第二个问题外层循环控制右窗口每次++,而里层循环每次用if去判断是否有重复字符,有就删除这个方法是非常错误的,也是很容易陷入误区的。
我们可以用while来判断,但是绝不能用if,if有很大的副作用!
如果此时的用例中,当前的右窗口遍历的字符与该窗口现在的某字符相等需要去重时,if只能去除一个,如果此时去除掉的并不是我们应该去除的,而后右窗口接着往后走,这将导致此次遍历到的情况很有可能已经不重复了,然后答案记录下来,但是此时子串真的已经没有重复字符了吗?这很不一定,这里使用if会导致一个很隐晦的逻辑错误!
代码错误容易修改,逻辑错误很难察觉!
这里正确做法应该使用while去不断左移,直到此时子串一定没有重复字符为止。
第三个问题如果我们删除的字符是在左边,这很有可能不是我们要删除的字符,这是没毛病的。
但是不要忘了,我们手动控制右窗口滑动,这就是好处,当此时子串中还存在重复字符时候,窗口一定不会增大,直到此时窗口没有重复字符,这其实和上面说的思路是一样的。
第四个问题该方法的确能够提升一些效率,看起来好像没什么错误,但实际上有错误,该方法只能够处理答案在测试样例的开头和中间的情况,对于答案出现在末尾比如【dvdf】这样的案例无法处理。
对于第二点我还要说明一下,我举个例子说明对于测试用例【bbbbb】
你可以对照上面最开始的正确代码,你不要妄想把代码写成如果当前遍历字符是set出现过的,那么就直接删掉left然后left++,这种代码在这种思路里是是十分愚蠢的。
就拿用例【bbbb】而言,你去删掉set中元素,而此时又没有新加元素,则会导致当前字符被漏掉
那如果你写成else里是删除left对应字符此时恰好在set里,然后left++并且把当前数据加进来呢?
那我只能说,这个方法还算比之前聪明,但是它仅能通过以尾字母结尾的子串长度,和中间能找得到的子串答案的用例,以首部开头的你找不到,因为外层循环这里的思路是i控制右窗口一直在++,你这里还没删完,就很快跳出循环来
如果循环控制右窗口的前提下,你想解决这种问题,可以试着把删除变成循环,就是当前字母如果一直能在set里找到,一直删s【left】
要不然你就把思路实现为while循环自己手动控制右窗口滑动,
或者再有一种思路就是循环控制左窗口,然后r手动控制右窗口,也是循环,一直找,一直扩大右窗口直到发现set出现该数,删除左窗口数字,这时候外层循环i控制左窗口,i行动比我们慢得多,因为我们手动控制右窗口滑动,直到找到set里出现才停下的,此时虽然不是while删除左边,但是有很充裕时间会删完直到set里没有重复数
当然你用两个while分别控制左右滑窗也是行的
总之是必须要写循环控制窗口,这是重中之重、。
那如果你非要使用if来完成对于两个窗口的滑动就一定没有方法了吗?
那还是有的while(1)写在外面,然后一点点移动左右窗口吧
直到找完整个字符串后再break或者return跳出去
由于已经说了这么多了,而且这些实现只是代码细节有所不同,但是实现思路相似,这里不给出其他对应代码,感兴趣可以自己实现一下
都看到这里了如果对您有用的话别忘了一键三连哦,如果是互粉回访我也会做的!
大家有什么想看的题解,或者想看的算法专栏、数据结构专栏,可以去看看往期的文章,有想看的新题目或者专栏也可以评论区写出来,讨论一番,本账号将持续更新。
期待您的关注