本章记录一些有关数组的一些较为经典或者自己第一次做印象比较深刻的算法以及题型,包含自己作为初学者第一次碰到题目时想到的思路以及网上其他更优秀的思路,本章持续更新中......
本章题目涉及到的一些算法技巧:二分查找,双指针,直接模拟,滑动窗口
目录
No 34. 在排序数组中查找元素的第一个和最后一个位置 (中等)
No 34. 在排序数组中查找元素的第一个和最后一个位置 (中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
题目描述:
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
示例 1:输入:nums = [5,7,7,8,8,10] target = 8 输出:[3,4]
示例 2:输入:nums = [5,7,7,8,8,10] target = 6 输出:[-1,-1]
示例 3:输入:nums = [], target = 0 输出:[-1,-1]
涉及技巧:二分查找
二分法是非常重要的基础算法,二分查找算法逻辑上相对容易理解,但是在细节方面较难把握。二分查找算法的关键之处在于下一轮查找区间的确定。这其中包含了多种多样的划分区间的方式,不同的区间在最终实现效果上有着巨大的不同。二分查找的前提是元素有序(一般是升序),由于每次都会将查询区间减半,所以二分查找算法的时间复杂度一般为O(log n)。二分查找算法可以通过循环实现,也可以通过递归实现。
思路:这是本人出此接触到该类题型的最初思路,可能写法不够精简,网上有更加精简的写法,这个题目当然可以通过循环遍历来实现,但是时间复杂度无法满足进阶的要求。题目中给定了升序数组,所以我们可以想到用二分查找来实现。
如何找到第一个出现的target:首先设置循环条件,当left==right时,说明二分查找结束了,如果nums [left] 或者nums [right] 与 target 相等,说明 left 或者 right 就是第一个 target 的位置,否则数组中没有 target 。下面看如何确定分区,本二分查找是找左边界,并不是普通的寻找确定值,所以在查找到该nums[mid]=target时要继续向左查找。求左边界:mid向下取整,等号归右边界,左边界加一
如何找到最后一个出现的target:同理,找最后一个出现的target就是找第一个大于 target 的 mid 值再-1,同样有规律:求右边界:mid向上取整(计算时+1),等号归左边界,右边界减一
结合代码和注释,应该可以比较清楚的理解思路,当然还有其它更好的方法,这里不做总结。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()){
return vector<int>{-1,-1};
}
int left=0;
int right=nums.size()-1;
int pos_l=-1;
int pos_r=-1;
// 寻找第一个出现的target
while(left<right){
int mid=(left+right)/2;
// 只能保证mid位置右边的字数不可能是第一个target
if(nums[mid]>=target){
right=mid;//mid本身可能是第一个target,故不用-1
}
// 只能保证mid本身和mid左边的数字不可能是第一个target
else{
left=mid+1;//mid本身不可能是第一个target,故需要+1
}
}
if(nums[left]==target){
pos_l=left;
}
//寻找最后一个出现的target
left=0;
right=nums.size()-1;
while(left<right){
int mid=(left+right+1)/2;
// 只能保证mid本身和mid右边的字数不可能是最后一个target
if(nums[mid]>target){
right=mid-1;//mid本身不可能是最后一个target,故需要-1
}
// 只能保证mid左边的数字不可能是最后一个target
else{
left=mid;//mid本身可能是最后一个target,故不需要+1
}
}
if(nums[left]==target){
pos_r=left;
}
return vector<int> {pos_l,pos_r};
}
};
No 26.删除有序数组中的重复项 (简单)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/
题目描述:
给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。将最终结果插入 nums 的前 k 个位置后返回 k 。
要求:不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
涉及技巧:双指针
双指针是一种比较常用的技巧。利用一个快指针和一个慢指针来共同操作数组,运用得当可以很好的优化算法。
思路:利用双指针实现。每当快指针移动到非慢指针所指元素时,就将慢指针+1,然后将快指针所指元素复制到慢指针处。此题非常简单,仅作为初步感受双指针而整理。
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(nums.empty()) return 0;
int sign=-1;
int fastIndex=0;
int slowIndex=0;
//利用双指针来写
//1.先将快指针遍历到的当前元素记录到sign中
//2.比较nums[slowIndex]和sign,相同则将sign赋给nums[slowIndex],否则先讲慢指针+1,再将sign赋给nums[slowIndex]
for(fastIndex=0;fastIndex<nums.size();fastIndex++){
sign=nums[fastIndex];
if(nums[slowIndex]!=sign){
slowIndex++;
}
nums[slowIndex]=sign;
}
//slowIndex是下标,返回的是长度,所以要+1
return slowIndex+1;
}
};
No 844. 比较含退格的字符串 (简单)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/backspace-string-compare/
题目描述:
给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。注意:如果对空文本输入退格字符,文本继续为空。进阶:你可以用 O(n)
的时间复杂度和 O(1)
的空间复杂度解决该问题吗?
示例 1:
输入:s = "ab#c", t = "ad#c"
输出:true
解释:s 和 t 都会变成 "ac"。
示例 2:
输入:s = "ab##", t = "c#d#"
输出:true
解释:s 和 t 都会变成 ""。
示例 3:
输入:s = "a#c", t = "b"
输出:false
解释:s 会变成 "c",但 t 仍然是 "b"。
涉及技巧:双指针
思路:依然想到利用双指针求解。设置两个指针,快指针一直向右遍历,每当快指针遇到 非# 时,则将快指针所指元素复制给慢指针,快指针和慢指针都+1;每当快指针遇到 # 时,则将慢指针回退一位,即慢指针-1。初次遇到本题时,遇到了一点小麻烦,是对于双指针的理解不够深刻导致的,故在此记录。
class Solution {
public:
//每当快指针移动到#时,就将慢指针-1退回,若慢指针此时已经为0则不处理;
//每当快指针移动到非#时,就将该处的值赋给慢指针处的值,慢指针+1
//最后得到的慢指针的数值就是处理后的字符串的长度
bool backspaceCompare(string s, string t) {
//处理字符串s,得到s的长度和处理后的字符串
int S_fastIndex = 0;
int S_slowIndex = 0;
while (S_fastIndex < s.size()) {
if (s[S_fastIndex] != '#') {
s[S_slowIndex] = s[S_fastIndex];
S_slowIndex++;
S_fastIndex++;
}
else {
if (S_slowIndex > 0) {
S_slowIndex--;
}
S_fastIndex++;
}
}
//处理字符串t,得到t的长度和处理后的字符串
int T_fastIndex = 0;
int T_slowIndex = 0;
while (T_fastIndex < t.size()) {
if (t[T_fastIndex] != '#') {
t[T_slowIndex] = t[T_fastIndex];
T_slowIndex++;
T_fastIndex++;
}
else {
if (T_slowIndex > 0) {
T_slowIndex--;
}
T_fastIndex++;
}
}
// 判断是否相同
if (S_slowIndex != T_slowIndex) return false;
for (int i = 0; i < S_slowIndex; i++) {
if (s[i] != t[i]) {
return false;
}
}
return true;
}
};
No 54.螺旋矩阵(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/spiral-matrix/
题目描述:
给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
涉及技巧:没有特殊技巧,就是用数组模拟矩阵螺旋读取数据。
思路:本题的关键在于坚持循环不变量原则。所谓循环不变量原则就是要有相同的划分区间的规则。我们用3*3和4*4的矩阵来举例说明。红色称为上边,蓝色部分称为右边,橙色部分称为下边,紫色部分称为左边,绿色部分称为中间。红色+蓝色+橙色+紫色正好能够围成一圈,称为loop。如果我们使用不一样的区间划分规则,很有可能会在行列交界处造成混乱,比如在3*3矩阵中,如果不坚持统一的划分规则,那么很有可能造成红色部分取1,2,3;蓝色部分取3,6的情况发生。
大家可能也发现了,绿色部分时有时无,而且绿色部分的大小也是不同的,这就需要分类讨论。
在方阵中,如果边长是奇数次,那么最中间的部分一定是最后一个遍历到的;如果边长是偶数,那么正好可以构成整数圈,不存在绿色部分。
在矩阵中,如果行数小于列数,那么中间部分一定是一行,反之如果行数大于列数,那么中间一定是一列。而我的思路就是将矩阵分为能够绕成一圈的部分和不能够绕成一圈的绿色部分两大块处理。红色+蓝色+橙色+紫色部分的处理方式大家通过代码和注释能够很容易理解。绿色部分的处理有几个关键之处。绿的部分的大小:通过观察可以发现绿色部分的大小是(行和列差值的绝对值+1)绿色部分最后一个数字的坐标:变化部分:行数(或列数)-圈数(loop)-1;不变部分:列数(或行数)/ 2。有了这两个数据我们就可以从最后一个数字倒着往回遍历,这样的好处是比较容易确定数组下标,因为矩阵的最后一个数字也是展开数组的最后一个数字。通过绿色部分大小来控制倒着遍历几个。
这一题其实官方还有网友有很多更加简洁的实现方式,我也去学习了。这里整理的思路是我最原始的思路,没有什么技巧,就是找到矩阵和展开数组的对应关系,并分类讨论。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int row=matrix.size();
if(row==0){
return {};
}
int col=matrix[0].size();
int min=row<col?row:col;
int max=row>col?row:col;
int loop=min/2;//用小的来计算循环的圈数
int loop_res=loop;//用于记录loop,后面计算中间部分时计算螺旋最后一个数组的横或纵坐标使用
int total=row*col;//结果数组大小
int total_res=total-1;//用于最后中间部分的处理,倒着遍历
int len=1;//用于控制区间大小,圈数不同大小不同
int mid=row/2;//用于计算奇数边长的方阵最中间的数字坐标
int startX=0;
int startY=0;
int sign_res=0;// 结果数组的下标
vector<int> res(total);
//特殊情况
if(row==1){
return matrix[0];
}
if(col==1){
for(int i=0;i<row;i++){
res[i]=matrix[i][0];
}
return res;
}
//一般情况
while(loop--){
//注意要统一边界条件,否则容易混乱
int i=startX;
int j=startY;
//上边
for(j=startY;j<col+startY-len;j++){
res[sign_res]=matrix[startX][j];
sign_res++;
}
//右边
for(i=startX;i<row+startX-len;i++){
res[sign_res]=matrix[i][j];
sign_res++;
}
//下边
for(;j>startY;j--){
res[sign_res]=matrix[i][j];
sign_res++;
}
//左边
for(;i>startX;i--){
res[sign_res]=matrix[i][j];
sign_res++;
}
//下一轮开始前进行初始化
startX++;
startY++;
len+=2;//每次绕完一圈后,下一圈处理的区间长度会少两个
//sign=total时说明已经处理完成,没有无法构成一圈的部分
if(sign_res==total){
return res;
}
}
//单独处理无法构成一圈的部分,也就是中间部分
if(row==col){
if(row%2!=0){
res[total_res]=matrix[mid][mid];
return res;
}
}
else{
int len_mid=fabs((row-col))+1;//中间无法构成一圈的部分的长度
int noChangeSign=min/2;//中间部分,坐标不需要变的部分,由行和列的大小决定
int changeSign=(max-1)-loop_res;//中间部分,坐标需要变化的部分,由行和列的大小决定
//螺旋遍历的最后一个数组的坐标,然后倒着往前赋值
if(row<col){
//中间部分是行
while(len_mid--){
res[total_res]=matrix[noChangeSign][changeSign];
changeSign--;
total_res--;
}
}
else{
//中间部分是列
while(len_mid--){
res[total_res]=matrix[changeSign][noChangeSign];
changeSign--;
total_res--;
}
}
}
return res;
}
};
No 209. 长度最小的子数组(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-size-subarray-sum/
题目描述:
给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
涉及技巧:滑动窗口。滑动窗口其实可以算作是双指针的一种,但是因为其过程很像一个滑动的窗口,随意一般都称为滑动窗口。
思路:窗口起始位置其实就是左指针,窗口结束位置其实就是右指针。根据题目要求,只要窗口内的数字之和大于等于target,那么此时就是一个有效的连续数组长度。我们只需要不断的滑动窗口并记录有效长度,取最大的有效长度即可。滑动窗口的关键之处在于如何来移动起始位置和结束位置。
起始位置:每当窗口内之和大于等于target时,说明此时就应该缩小窗口了,起始位置要右移。需要注意的是起始位置的右移需要用循环来做,不能一轮只做一次。因为有可能起始位置右移了一次以后仍然大于等于target的情况出现。
结束位置:结束位置就是遍历整个数组的指针
本题也有双层for循环暴力的解法,后面的注释中就是暴力解法,时间复杂度是O(n^2)。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
//优化:滑动窗口
int windowsRight = 0;
int windowsLeft = 0;
int windowsLen = 0;//滑动窗口的动态长度
int sum = 0;
int len_res = nums.size()+1;//最终的结果
while (windowsRight < nums.size()) {
sum += nums[windowsRight];
//只要滑动窗口的值大于等于target,则满足条件,那么就将起点元素从sum中减去,同时起点右移+1
//同时计算当前的窗口大小,如果是最小的则记录,否则不变
while (sum >= target) {
windowsLen = windowsRight - windowsLeft + 1;
sum -= nums[windowsLeft];
windowsLeft++;
if (len_res > windowsLen) {
len_res = windowsLen;
}
}
windowsRight++;
}
if (len_res == nums.size() + 1) {
return 0;
}
return len_res;
// //暴力解法
// if (nums.empty()) {
// return 0;
// }
// if (nums.size() == 1) {
// if (nums[0] == target) {
// return 1;
// }
// else {
// return 0;
// }
// }
// int fastIndex = 0;
// int slowIndex = 0;
// int sum = 0;
// int len = nums.size()+1;//保证第一次记录len的分支可以进入
// //vector<int> res(len);
// //快指针从慢指针处开始向右移动,同时进行累加
// //一旦sum第一次>=target则记录当前len,同时进行初始化,包括sum置0,慢指针+1,快慢指针同步
// //后续sum>=target时,若当前的len小于之前记录的len则重新给len赋值,保证len获得最小值
// while (fastIndex < nums.size()) {
// sum += nums[fastIndex];
// if (sum < target) {
// fastIndex++;
// }
// else {
// if(len > (fastIndex - slowIndex + 1)){
// len = fastIndex - slowIndex + 1;
// // int start = slowIndex;
// // int end = fastIndex;
// // for (int i = 0; i < len; i++) {
// // res[i] = nums[start];
// // start++;
// // }
// }
// sum = 0;
// slowIndex++;
// fastIndex = slowIndex;
// }
// }
// if (len < nums.size() + 1) {
// // for (int i = 0; i < len; i++) {
// // res[i] = nums[i];
// // }
// return len;
// }
// else {
// return 0;
// }
}
};
No 76. 最小覆盖子串(困难)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-window-substring/
题目描述:
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
示例 2:
输入:s = "a", t = "a"
输出:"a"
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。
涉及技巧:滑动窗口
思路:这是第一次遇到困难的题型,没有完全解答出来。在参考了网友的思路以后才解答出来。这一题还是使用滑动窗口解答,我的问题出现在不知道如何对在字符串 S 中出现的字符串 T 中的字符进行记录,导致不知道应该如何设置滑动窗口的起始位置移动条件。
首先利用一个数组来保存字符串 T 中的所有出现的字符的数量,数组的下标就是字符,其实是字符的ascII码,这也是为什么要设置128大小的数组。设置一个计数器,初始大小为字符串T的大小。
vector<int> need(128, 0);
for (char c : t) {
need[c]++;
}
然后设置滑动窗口的起始位置和结束位置,也就是左右指针。循环遍历,每当字符串 S 中的字符在need数组中的数值大于0时,表示该字符是我们需要的字符,这时候将计数器 -1,该字符在need数组中的值 -1。如果不是我们需要的字符,则表示当前窗口中有多余字符,将该字符在数组中的值 -1,此时将为负数;
if (need[c] > 0) {
count--;//找到一个字符
}
need[c]--;//若为负,则表示need不需要字符,是多余的
接下来如果计数器为0,表示字符串 T 中的字符已经全部在字符串 S 中了。但是此时的窗口不一定是最小的,因此要开始进行缩小窗口的操作。那么要缩小到什么程度呢?要缩小到如果再缩小一个位置就会导致字符串 T 中的字符不能全部出现在窗口中。这时候窗口的长度就是符合条件的长度。但是此时不一定是最小的,因为还有没有遍历到的字符,可能后面还有更小的。
所以最后我们应该将左指针的位置右移一位,将当前窗口再缩小一位,从这个位置作为起始位置重新再找一遍符合的窗口,如此循环直到右指针遍历到末尾。结合代码和注释能够更好的理解。
class Solution {
public:
string minWindow(string s, string t) {
vector<int> need(128,0);
for(char c:t){
need[c]++;
}
int count=t.length();
int windowsRight=0;
int windowsLeft=0;
int windowsLen=s.length()+1;
int startIndex=0;
while(windowsRight<s.length()){
char c=s[windowsRight];
// 字符s中的字符c在need中有需要
if(need[c]>0){
count--;//找到一个字符
}
need[c]--;//若为负,则表示need不需要字符,是多余的
if(count==0){
//windowsLeft缩减时必须保证窗口内包含字符串t,不可取等,因为此时t所含字符也是0
while(windowsLeft<windowsRight&&need[s[windowsLeft]]<0){
need[s[windowsLeft]]++;
windowsLeft++;
}
//取最小长度
if(windowsRight-windowsLeft+1<windowsLen){
windowsLen=windowsRight-windowsLeft+1;
startIndex=windowsLeft;//记录此时的位置,将来是字串的起始位置
}
//不一定是最小的,要重新再开始一次
need[s[windowsLeft]]++;//这个位置的字符将要被移除,而且是t中包含的所以在need中要+1
windowsLeft++;
//因为要重新开始,而刚刚又找到了最小长度,所以此时右移会导致t中的一个字符被移除,所以要+1
count++;
}
windowsRight++;
}
if(windowsLen==s.length()+1){
return "";
}
else{
return s.substr(startIndex,windowsLen);
}
}
};