LeetCode探索 之 数据结构卡片-数组和字符串 题目
编程语言:C++
第一部分-数组简介
自己做法总结:
- 考虑元素个数小于2的情况
- 索引下标考虑左边无元素和右边无元素的情况
- 利用变量累加值,而不是每一次都暴力对两侧sum进行求取
- 利用vector.size()函数计算容器元素个数
下面第一个程序存在bug,不过系统可以通过
class Solution {
public:
int pivotIndex(vector<int>& nums) {
//获取数组个数
int N=nums.size();
/**********下面这个说法是错误的,故需要更正**********/
//元素少于三个是没有中心索引的
if(N<=2)
return -1;
else
{
//前累加和、后累加和
int sumpre=0;
int sumpos=0;
for(int i=1;i<N;i++)
{
sumpos+=nums[i];
}
//从下标0扫描到下标N-1
int j;
for(j=0;j<N;j++)
{
if(sumpre==sumpos)
break;
else
{
//向后累加
sumpre+=nums[j];
//向后累减
sumpos-=nums[j+1];
}
}
if(j==N)
return -1;
else
return j;
}
}
};
更正程序
class Solution {
public:
int pivotIndex(vector<int>& nums) {
//获取数组个数
int N=nums.size();
//前累加和、后累加和
int sumpre=0;
int sumpos=0;
for(int i=1;i<N;i++)
{
sumpos+=nums[i];
}
//从下标0扫描到下标N-1
int j;
for(j=0;j<N;j++)
{
if(sumpre==sumpos)
break;
else
{
//向后累加
sumpre+=nums[j];
//向后累减
sumpos-=nums[j+1];
}
}
if(j==N)
return -1;
else
return j;
}
};
大佬做法总结:
- 充分利用了sum的不变性与中心索引两侧的对称性加速求解简化运算,能找到 sumleft*2 + nums[i] == sum 关系简直太秀了
- size(nums)可以得到数组长度
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int sum=0;
int sumleft=0;
int len=size(nums);
//计算sum
for(int i=0;i<len;i++)
{
sum+=nums[i];
}
//从前往后扫描
for(int j=0;j<len;j++)
{
if(sumleft*2+nums[j]==sum)
return j;
else
sumleft+=nums[j];
}
return -1;
}
};
个人思路总结:
- i下标的插入条件:target<=nums[i],否则就应该一直往后再比较
- 如果最后都没有找到插入位置,直接返回长度就行,无需进行if判断。同时此处也体会到return语句必须在书写的时候就应该保证都能执行到,而不是逻辑认为的执行到。
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
//获取数组个数
int len=size(nums);
//扫描
int i;
for(i=0;i<len;i++)
{
if(target<=nums[i])
return i;
}
return len;
}
};
大佬解法:二分查找
这里可以看一下这个 https://www.zhihu.com/question/36132386
- 直接记这个模板就可以
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
//求非降序范围[first, last)内第一个不小于value的值的位置
int len=size(nums);
int first=0;
int last=len;
int mid;
while(first<last)//搜索区间[first, last)不为空
{
mid=first+(last-first)/2;//防溢出
if(nums[mid]<target)
first=mid+1;
else
last=mid;
}
return first;//last也行,因为[first, last)为空的时候它们重合
}
};
个人思路总结
- 基于上锁-开锁的思想
- 建立二维数组temp
- 对原二维数组依据【L,R】中的L升序排列
- 首先往temp中增添一条【L,R】的信息(最小的L那条),然后进行上锁。上锁之后,如果没有解锁,是无法重新往temp中添加新信息【L,R】的。
- 下面是更新temp增添的新信息【L,R】的过程:往后遍历原数组intervals,此过程中记录出现的最大R,一直到目前为止最大的R小于下一条信息L的时候,中止。更新temp中信息的R,并进行解锁。
- 开始新一轮的temp插入【L,R】(intervals遍历位置下一条信息),并更新R的过程,循环往复。直至剩下最后一条信息。
- 如果最后一条信息为止还没解锁,则需要利用最后一条信息更新temp的R,如果已经解锁,则直接往temp中添加一条信息。
Tips
- 二维数组建立 vector<vector> temp
- 二维数组行数获取 intervals.size();
- 二维数组【L,R】中L升序排列 sort(intervals.begin(), intervals.end());
- 二维数组增添一行 temp.push_back({intervals[0][0],intervals[0][1]});
- 二维数组更新行信息 temp.back()[1]=maxR;
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
//二维数组
vector<vector<int>> temp;
//获取原数组行数
int row=intervals.size();
sort(intervals.begin(), intervals.end());
//扫描
int j=0;
int flag=0;
int maxR=0;
if(row==0)
{
return {};
}
else if(row==1)
{
temp.push_back({intervals[0][0],intervals[0][1]});
}
else
{
int i;
for(i=0;i<row-1;i++)
{
//保留第一元素,上锁
if(flag==0)
{
temp.push_back({intervals[i][0],intervals[i][1]});
flag=1;
}
//记录目前所有[L,R]最大的R
maxR=max(maxR,intervals[i][1]);
//如果存在数组存在间距,解锁并保留第二元素
if(flag==1)
{
if(maxR<intervals[i+1][0])
{
temp.back()[1]=maxR;
flag=0;
}
}
}
maxR=max(maxR,intervals[i][1]);
if(flag==1)//如果没有解锁,则需要针对最后一个
{
temp.back()[1]=maxR;
}
else
temp.push_back({intervals[i][0],intervals[i][1]});
}
return temp;
}
};
大佬做法
做法一总结:
- 与个人思路基本一致,都是往新数组temp中写入信息,然后遍历原数组intervals进行更新temp顶部的R。优点在于他是以遍历的intervals【L,R】去跟新数组temp的顶部元素去比较,以比较决定往temp中增加信息,而我是以比较来更新temp顶部的R,感觉他的思路更清晰也更简洁。
- 第一次和temp顶部R < intervals的L的时候往temp中增添信息,否则就一致更新temp顶部的R
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
//空数组直接返回
if(intervals.size()==0)
{
return {};
}
vector<vector<int>> temp;
//依据L排序
sort(intervals.begin(),intervals.end());
for(int i=0;i<intervals.size();i++)
{
int L=intervals[i][0],R=intervals[i][1];
if(!temp.size()||temp.back()[1]<L)//只有在第一次或者temp顶部信息R<L的时候写入temp
{
temp.push_back({L,R});
}
else//更新
{
temp.back()[1]=max(temp.back()[1],R);
}
}
return temp;
}
};
解法二总结
基本思路:排序+双指针
- 左指针指向原数组L,右指针指向原数组R,向后遍历,更新maxR,直至遇到间断。
- 将信息【L,maxR】插入新数组
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> temp;
sort(intervals.begin(),intervals.end());
for(int i=0;i<intervals.size();)
{
int maxR=intervals[i][1];
int j=i+1;
while(j<intervals.size()&&maxR>=intervals[j][0])
{
maxR=max(maxR,intervals[j][1]);
j++;
}
temp.push_back({intervals[i][0],maxR});
i=j;
}
return temp;
}
};
第二部分-二维数组简介
方法一 :使用辅助数组
个人思路总结
- 旋转矩阵就是第i行放到第size-1-i列
- 需要借助一个新二维数组进行旋转后元素的存放,最后再返还给原二维数组
- 时间复杂度O(N^2)
- 空间复杂度O(N^2)
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
//获取矩阵行数
int size=matrix.size();
//矩阵的拷贝
auto matrix_new=matrix;
//行扫描
for(int i=0;i<size;i++)
{
for(int j=0;j<size;j++)
{
matrix_new[j][size-1-i]=matrix[i][j];//i行变成size-1-i列
}
}
//矩阵的反拷贝
matrix=matrix_new;
}
};
官方其他的做法
方法二:原地旋转
个人思路总结:
利用旋转的对称性,一个大矩阵其实是由4个小矩阵组成的。可以通过遍历左上角的矩阵,利用旋转关系实现扫描整个大矩阵。
- 时间复杂度O(N^2),N/2 * (N+1)/2 ~ N^2
- 空间复杂度O(1),只额外多了一个变量temp
下面的旋转关系看似复杂,其实就是通过整个顺序进行赋值的
- temp=左上角
- 左上角=左下角
- 左下角=右下角
- 右下角=右上角
- 右上角=temp
下标关系还是利用了第i行放到第size-1-i列
后面的利用整体思想, [表达式1] [表达式2] -> [表达式2][size-1-表达式1]
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
//获取矩阵的行数
int size=matrix.size();
//需要旋转的单位长度
int i_size,j_size;
//奇数
if(size%2)
{
i_size=size/2;
j_size=size/2+1;
}
else
{
i_size=size/2;
j_size=size/2;
}
//中间变量
int temp;
//左上角区域
for(int i=0;i<i_size;i++)
{
for(int j=0;j<j_size;j++)
{
temp=matrix[i][j];
matrix[i][j]=matrix[size-1-j][i];
matrix[size-1-j][i]=matrix[size-1-i][size-1-j];
matrix[size-1-i][size-1-j]=matrix[j][size-1-i];
matrix[j][size-1-i]=temp;
}
}
}
};
看了官方的写法的体会:
4 * 4矩阵的小旋转矩阵是22的矩阵
5 * 5的矩阵的小旋转矩阵是23的矩阵
所以行都是size/2,列则是(size+1)/2
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < (n + 1) / 2; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
};
方法三:翻转矩阵
旋转矩阵实际上是进行一次行对称翻转+一次对角线翻转
- 时间复杂度O(N^2),两次 N^2 的扫描
- 空间复杂度O(1),原地旋转
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
//获取矩阵的行数
int size=matrix.size();
//上下翻转
for(int i=0;i<size/2;i++)
{
for(int j=0;j<size;j++)
{
swap(matrix[i][j],matrix[size-1-i][j]);
}
}
//对角线翻转
for(int i=0;i<size;i++)
{
for(int j=0;j<i;j++)
{
swap(matrix[i][j],matrix[j][i]);
}
}
}
};
个人naive做法总结
- 扫描+过渡矩阵
- 时间复杂度O(N^2), N^2 的扫描
- 空间复杂度O(N^2)
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
//过渡矩阵
vector<vector<int>> matrix_new;
matrix_new=matrix;
//获取矩阵的行数和列数
int row=matrix.size();
int col=matrix[0].size();
int flag; //0标志
//scan
for(int i=0;i<row;i++)
{
for(int j=0;j<col;j++)
{
if(!matrix[i][j])
{
flag=1;
}
if(flag)
{
//行赋值0
for(int k=0;k<col;k++)
{
matrix_new[i][k]=0;
}
//列赋值0
for(int k=0;k<row;k++)
{
matrix_new[k][j]=0;
}
flag=0;
}
}
}
matrix=matrix_new;
}
};
改进思路:前面赋值0的行和列无需再进行扫描
- 时间复杂度O(N^2), N^2 的扫描
- 空间复杂度O(N),2个N元素一维数组
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
//获取行数和列数
int mrow=matrix.size();
int mcol=matrix[0].size();
if(mrow==0||mcol==0)
return;
//两个一维数组
vector<bool> row(mrow,false);
vector<bool> col(mrow,false);
//扫描
for(int i=0;i<mrow;i++)
{
for(int j=0;j<mcol;j++)
{
if(!matrix[i][j])
{
row[i]=true;
col[j]=true;
}
}
}
//矩阵输出
for(int i=0;i<mrow;i++)
{
for(int j=0;j<mcol;j++)
{
if(row[i]||col[j])
matrix[i][j]=0;
}
}
}
};
个人思路总结:
- 基本思路:按照对角线扫描的顺序去复现,将元素存放至一维数组
- 需要有一个flag变量区分奇数和偶数,分清是向上扫描还是向下扫描
- 非边界:向上扫描整体应该是i自减,j自加
- 非边界:向下扫描整体应该是i自加,j自减
- 边界处理:向上扫描至右上角,下一步应该i自加,j不变;向上扫描至上边界,i不变,j自加;向上扫描至右边界,下一步应该j不变,i自增
- 边界处理:向下扫描至左下角,下一步应该j自加,i不变;向下扫描至下边界,i不变,j自加;向下扫描至左边界,下一步应该j不变,i自增
一些做题的雷区
- 获取行数之后需要直接做0判断处理,不可以不进行处理去获取列数。因为没有行的时候,是不存在matrix[0]的
class Solution {
public:
vector<int> findDiagonalOrder(vector<vector<int>>& matrix) {
//建立一维数组
vector<int> nums;
//获取行数和列数//一定要分开书写,因为没行的时候是不会有matrix[0]
int row=matrix.size();
if(row==0)
return nums;
int col=matrix[0].size();
if(col==0)
return nums;
int flag=1;//奇数偶数标志位
for(int i=0,j=0; ;)
{
nums.push_back(matrix[i][j]);
if((i==row-1)&&(j==col-1))
break;
//转折点处i与j的更新
if(flag%2)//奇数上走
{
if((i==0)&&(j==col-1))//遇到右上角点
{
i++;
flag++;
}
else if(i==0)//遇到上边界点
{
j++;
flag++;
}
else if(j==col-1)//遇到右边界点
{
i++;
flag++;
}
else
{
i--;
j++;
}
}
else//偶数下走
{
if((j==0)&&(i==row-1))//遇到左下角点
{
j++;
flag++;
}
else if(i==row-1)//遇到下边界点
{
j++;
flag++;
}
else if(j==0)//遇到左边界点
{
i++;
flag++;
}
else
{
i++;
j--;
}
}
}
return nums;
}
};
看了官方解析后的一些改进:
主要是针对边界处理,以上行为例,之前是分为上边界、右边界、右上角三种情况进行处理。
缩减为上边界和右边界两种情况,但是if判断时必须右边界先进行判断,再判断上边界
class Solution {
public:
vector<int> findDiagonalOrder(vector<vector<int>>& matrix) {
//建立一维数组
vector<int> nums;
//获取行数和列数//一定要分开书写,因为没行的时候是不会有matrix[0]
int row=matrix.size();
if(row==0)
return nums;
int col=matrix[0].size();
if(col==0)
return nums;
int flag=1;//奇数偶数标志位
for(int i=0,j=0; ;)
{
nums.push_back(matrix[i][j]);
if((i==row-1)&&(j==col-1))
break;
//转折点处i与j的更新
if(flag%2)//奇数上走
{
if(j==col-1)//遇到右边界点
{
i++;
flag++;
}
else if(i==0)//遇到上边界点
{
j++;
flag++;
}
else
{
i--;
j++;
}
}
else//偶数下走
{
if(i==row-1)//遇到下边界点
{
j++;
flag++;
}
else if(j==0)//遇到左边界点
{
i++;
flag++;
}
else
{
i++;
j--;
}
}
}
return nums;
}
};
大佬解法
- 具体详见C++题解
- 基本思想:寻找到了x+y=对角线被扫描的次数。其实还是分奇数和偶数两种大情况,但是最终利用一套代码进行了实现。
class Solution {
public:
vector<int> findDiagonalOrder(vector<vector<int>>& matrix) {
//一维数组
vector<int> result;
//获取行数和列数
int row=matrix.size();
if(row==0) return result;
int col=matrix[0].size();
if(col==0) return result;
int i=0;//第几条对角线
bool flag=true;
while(i<row+col-1)//对角线扫描次数
{
int pm=flag ? row:col;
int pn=flag ? col:row;
int x=i<pm ? i:pm-1;//选取最大的x
int y=i-x;//x+y=对角线次数
while(x>=0 && y<pn)
{
result.push_back(flag ? matrix[x][y]:matrix[y][x]);
x--;
y++;
}
i++;
flag=!flag;
}
return result;
}
};
第三部分-字符串简介
个人做法总结
- 以第一个字符串为基准,后面的依次与其进行比较,每次记录目前为止最大前缀的下标k,之后的扫描只比较前k个字符。
- 时间复杂度O(MN), M个字符串,平均长度N,每个字符串的每个字符都会被比较
- 空间复杂度O(1),使用的额外空间为常数
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
//最大前缀字符串
string strpre;
//如果少于1个字符串,输出空。
if(strs.size()<1)
return "";
else if(strs.size()==1)//如果等于1个字符串,输出这个字符串。
return strs[0];
int len=strs[0].size();
//寻找最大前缀子串
for(int i=1,k=0;i<strs.size();i++)
{
for(k=0;k<len;k++)
{
if(strs[i][k]==strs[0][k])
{
strpre+=strs[0][k];
}
else
break;
}
len=k;
/新添入,最大子串空的时候提前结束//
if(len<1)
break;
/
if(i!=strs.size()-1)
strpre="";
}
return strpre;
}
};
官方做法一复现:横向比较
总结:
- 主要技巧在于写了一个函数比较两个字符串(当前最大前缀子串 与 第i个字符串)。
- 字符串的比较while这种写法估计会很重要可以记住。
- prefix=str1.substr(0,index)这种截取字符串的写法
- 中间过程中会依据当前最大子串是否为空而提前退出
- 时间复杂度O(MN), M个字符串,平均长度N,每个字符串的每个字符都会被比较
- 空间复杂度O(1),使用的额外空间为常数
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
//没有字符串时
if(strs.size()<1)
return "";
//大于等于1个字符串的情况
string prefix=strs[0];
for(int i=1;i<strs.size();i++)
{
prefix=longestCommonPrefix(prefix, strs[i]);
if(!prefix.size())
break;
}
return prefix;
}
string longestCommonPrefix(const string& str1, const string& str2)
{
//取两个字符串较短的长度
int len=min(str1.size(),str2.size());
//while比较
int index=0;
while(index<len && str1[index]==str2[index])
{
index++;
}
//妙处
return str1.substr(0,index);
}
};
官方做法二-纵向比较
- 以第一个字符串为基准,每次取一个字符c
- 扫描后面的字符串对应位置的字符每次与上面的字符c进行比较
- 时间复杂度O(MN), M个字符串,平均长度N,每个字符串的每个字符都会被比较
- 空间复杂度O(1),使用的额外空间为常数
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
//没有字符串时
if(strs.size()<1)
return "";
//获取第一个字符串的长度
int length=strs[0].size();
//获取字符串个数
int count=strs.size();
//纵向比较
for(int i=0;i<length;i++)
{
int c=strs[0][i];
for(int j=1;j<count;j++)
{
//当字符串长度跟i相等或者字符串的字符不等于c字符的时候就直接返回字符串前缀
if(i==strs[j].size() || strs[j][i]!=c)
{
return strs[0].substr(0,i);
}
}
}
return strs[0];
}
};
官方做法三-分而治之
总结:
- 主要在于递归的终止条件要记得先写
- 时间复杂度:O(mn)。其中 m 是字符串数组中的字符串的平均长度,n 是字符串的数量。时间复杂度的递推式是T(n)=T(n/2)+O(m),通过计算可得 T(n)=O(mn)
- 空间复杂度:O(m logn),其中 m 是字符串数组中的字符串的平均长度,n 是字符串的数量。空间复杂度主要取决于递归调用的层数,层数最大为logn,每层需要 m的空间存储返回结果
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if(!strs.size())
return "";
else
return longestCommonPrefix(strs, 0, strs.size()-1);
}
string longestCommonPrefix(vector<string>& strs, int start, int end) {
if(start==end)
return strs[start];
else
{
int mid=(start+end)/2;
string leftLCP=longestCommonPrefix(strs,start,mid);
string RightLCP=longestCommonPrefix(strs,mid+1,end);
return CommonPrefix(leftLCP,RightLCP);
}
}
string CommonPrefix(const string& leftLCP, const string& RightLCP){
int minlength=min(leftLCP.size(),RightLCP.size());
for(int i=0;i<minlength;i++)
{
if(leftLCP[i]!=RightLCP[i])
return leftLCP.substr(0,i);
}
return leftLCP.substr(0,minlength);
}
};
自己没有做出
官方做法一动态规划
总结:
- 最大的教训:s.substr(begin,maxlen)这个函数的用法真的恶心到了,与java的写法不太一样。表示从begin下标开始的maxlen个字符。
- 一个字符肯定是回文串,所以先赋值对角线1。之后按列填表,先看两个端点i和j,如果不相等肯定不是回文串,如果相等则需要分两种情况。第一种情况去掉两个端点i和j之后就剩一个字符,则肯定是回文串;第二种情况,去掉两个端点i和j之后,依据子串是否是回文串来决定是否是回文串。
- 动态规划的核心在于寻找状态转移方程和从小到大计算简化。
- 先列后行的处理简直太妙了,简化计算过程,后面的计算直接调用前面的计算结果。
- 时间复杂度O(N^2)其中 n 是字符串的长度。动态规划的状态总数为 O(N^2) ,对于每个状态,我们需要转移的时间为 O(1)
- 空间复杂度O(N^2),二维数组N*N
class Solution {
public:
string longestPalindrome(string s) {
//特别处理,字符串长度为0或者1时直接返回s
int len=s.size();
if(len<2)
return s;
//定义最大长度和下标起始点
int maxlen=1;
int begin=0;
//初始化dp二维数组
vector<vector<int>> dp(len, vector<int>(len,0));
for(int i=0;i<len;i++)
{
dp[i][i]=1;
}
//动态规划-先列后行
for(int j=1;j<len;j++)//先列
{
for(int i=0;i<j;i++)//后行
{
if(s[i]!=s[j])//如果左右端点不相等肯定不是回文串
{
dp[i][j]=0;
}
else
{
if(j-i<3)//长度为3或者2的时候肯定是回文串
{
dp[i][j]=1;
}
else//看除端点之外的子串
{
dp[i][j]=dp[i+1][j-1];
}
}
if(dp[i][j] && j-i+1>maxlen)
{
begin=i;
maxlen=j-i+1;
}
}
}
return s.substr(begin,maxlen);
}
};
官方做法二-中心扩散
总结:
- **while (left >= 0 && right < s.size() && s[left] == s[right])**这三个条件left和right的限制一定要在前面,防止出现溢出的情况
- 所有的回文字符串最根本的东西,要么是一个字符,要么是两个相同的字符。所以针对每一个i点依据这两种情况进行中心扩散,一直扩散到不相等的i和j出现或者到达数组边界。在此过程中每一次i中心扩散,记录左端点和右端点计算长度,这两种情况取最大的情况。遍历完成,应该是最大回文串。
- 时间复杂度O(N^2),其中 n 是字符串的长度。长度为 1和 2的回文中心分别有 n 和 n-1个,每个回文中心最多会向外扩展 O(n)次
- 空间复杂度O(1)
class Solution {
public:
pair<int, int> expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return {left + 1, right - 1};
}
string longestPalindrome(string s) {
int start = 0, end = 0;
for (int i = 0; i < s.size(); ++i) {
auto [left1, right1] = expandAroundCenter(s, i, i);
auto [left2, right2] = expandAroundCenter(s, i, i + 1);
if (right1 - left1 > end - start) {
start = left1;
end = right1;
}
if (right2 - left2 > end - start) {
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1);
}
};
自己做法总结:
- 双指针。begin指针用于从后往前寻找单词的首字符,end指针在begin指针的位置上向后一直到此单词结束。
- C++中string.size()函数没有C语言中’\0’之说,故得到的是字符串的真实长度,最后一个下标应该是size()-1
- 先看头指针寻找单词首字符:当前字符不为空并且下标等于0 或者 当前字符不为空,前一个字符为空
- 再看尾指针寻找单词尾字符:当前字符不为空并且下标小于等于size()-1
- 每一个单词读取完成之后就加一个空格,最后整个字符串完成之后删除空格。
- 时间复杂度:O(n)。假设字符串长度为n,头指针应该扫描n次O(n)
- 空间复杂度:O(n)。拷贝一个字符串
class Solution {
public:
string reverseWords(string s) {
string temp;
//字符串长度
int length=s.size();
//从后往前扫描
//双指针。begin指针从后向前寻找单词首字符,end指针从首字符依次向后用于字符复制
int begin,end;
for(int begin=length-1;begin>=0;begin--)
{
if((s[begin]!=' ' && begin==0)||(s[begin]!=' ' && s[begin-1]==' '))//单词的首字符
{
end=begin;
while((s[end]!=' ')&&(end<=s.size()-1))//到达字符串末尾或者单词的尾字符
{
temp.push_back(s[end++]);
}
temp.push_back(' ');
}
}
//去一个末尾空字符
temp.erase(temp.size()-1);
return temp;
}
};
官方做法之自己编写的翻转函数
总结:
- 整个字符串进行一次大的翻转reverse(s.begin(),s.end());
- 从前往后进行扫描
- first指针寻找单词首字符,若找到后则end指针指向first指针并不断后移,将其字符前移至idx指针代表的新字符串。(两个指针指向原字符串的单词,新指针指向新建立的字符串)
- 对新单词进行翻转
- 去除冗余字符
- 时间复杂度:O(N),字符串长度为n,进行一次扫描
- 空间复杂度:O(1),没有额外开销
class Solution {
public:
string reverseWords(string s) {
//先对整个字符串进行一次大翻转
reverse(s.begin(),s.end());
//获取字符串的长度
int length=s.size();
//从前往后遍历
//一个单词头指针,一个单词尾指针
int start,end=0;
//新字符串的指针
int idx=0;
for(start=0;start<length;start++)
{
//寻找不是空格的字符,也即单词的首字符
if(s[start]!=' ')
{
//每一个单词后面跟的空格
if(idx!=0)
s[idx++]=' ';
//把每一个单词的字符前移复制
end=start;
while(end<length && s[end]!=' ')
{
s[idx++]=s[end++];
}
//对每一个单词进行一次翻转
reverse(s.begin()+idx-(end-start),s.begin()+idx);
//头指针移到尾指针位置
start=end;
}
}
//擦除后面的冗余字符
s.erase(s.begin()+idx,s.end());
return s;
}
};
官方做法三-数据结构栈的使用
总结:
- 通过使用数据结构栈,进出每一个单词实现解题。
- 核心还是在于word单词的筛选。遇到空格则表示一个word的结束,可以入栈。最后一个单词单独处理。
- 时间复杂度:O(N),字符串长度n
- 空间复杂度:O(N),所有单词的存储长度n
- 前面的去段首和段尾的算法值得借鉴
class Solution {
public:
string reverseWords(string s) {
int left=0;
int right=s.size()-1;
//去掉字符串前部空格
while(s[left]==' ')
left++;
//去掉字符串尾部空格
while(s[right]==' ')
right--;
stack<string> temp;
string word;
while(left<=right)//扫描
{
//word不空且遇到空格,说明读完一个单词
if(word.size() && s[left]==' ')
{
temp.push(word);
word="";
}
else if(s[left]!=' ')
{
word+=s[left];
}
left++;
}
temp.push(word);//最后一个单词没有空格需要单独处理
string ans;
while(!temp.empty())
{
//取栈首元素
ans+=temp.top();
//出栈
temp.pop();
if(!temp.empty())
ans+=' ';
}
return ans;
}
};
个人思路总结:
- 具阅读左神的程序员面试指南,加深了这一知识点的理解。
- 核心是两个知识点:一是如何建立前缀后缀匹配表,二是如何让模式字符串与原字符串进行匹配
class Solution {
public:
void buildMatch(string needle, int *match)
{
int M=needle.size();
//字符串为1需要单独处理
if(M==1)
match[0]=-1;
else
{
match[0]=-1;
match[1]=0;
int cn=0;
int pos=2;
while(pos<M)
{
//pos-1字符与cn字符相等,则match[pos]=match[pos-1]+1=cn+1
if(needle[pos-1]==needle[cn])
{
match[pos++]=++cn;
}
else if(cn>0)
{
cn=match[cn];
}
else
{
match[pos++]=0;
}
}
}
}
int strStr(string haystack, string needle) {
//获取字符串的长度
int N=haystack.size();
int M=needle.size();
//模式串为空返回0,模式串长度大于原字符串则返回-1
if(M==0)
return 0;
if(M>N)
return -1;
//模式串的前后缀匹配表建立
int *match=(int *)malloc(sizeof(int)*M);
buildMatch(needle,match);
//模式串与原字符串的匹配过程
int n=0,m=0;
while(n<N && m<M)
{
//匹配成功,下标则一同进步
if(haystack[n]==needle[m])
{
n++;
m++;
}
//匹配不成功,模式串需要转到match[]位置
else if(m>0)
{
m=match[m];
}
//匹配不成功,模式串下标且已经退回到模式串0地址
else
{
n++;
}
}
//释放动态数组
free(match);
//依据模式串是否走到末尾决定返回下标
return m==M ? n-m : -1;
}
};
第四部分-双指针技巧
1.反转字符串
时间复杂度:O(N)。N/2次交换
空间复杂度:O(1)
class Solution {
public:
void reverseString(vector<char>& s) {
int begin=0,end=s.size()-1;
while(begin<end)
{
swap(s[begin],s[end]);
begin++;
end--;
}
}
};
个人思路总结
- sort函数总结。sort(a,a+10)两个参数是从小到大排序,从大到小排序是三个参数sort(a,a+10, compare);
- 为什么可以用排序做呢?可以这么理解,最小的元素只有和除它以外最小的元素进行组合,才不会影响较大的元素成为min(),这样总和就是最大的。
- 时间复杂度:O(NlogN)。排序需要O(NlogN),遍历需要O(N)
- 空间复杂度:O(1)。
class Solution {
public:
int arrayPairSum(vector<int>& nums) {
int n=nums.size();
sort(nums.begin(),nums.end());
int sum=0;
for(int i=0;i<n;i+=2)
{
sum+=nums[i];
}
return sum;
}
};
官方哈希表做法
总结:
- 建立哈希表,以空间换取时间。
- 关键点在于找好对应关系和累加值确定
- 累加值确定的过程中d十分巧妙。计算d的过程中需要加2避免出现负数,计算i+10000的个数的时候(+1-d)/2向上取整
- 时间复杂度:O(N)。哈希表扫描O(n)
- 空间复杂度:O(N)。哈希表的空间为O(n)
class Solution {
public:
int arrayPairSum(vector<int>& nums) {
//建立哈希表初始化
int arr[20001]={0};
//i元素的个数存放在arr[i+10000]位置
for(int i=0;i<nums.size();i++)
{
arr[nums[i]+10000]++;
}
//扫描哈希表
int d=0;
int sum=0;
for(int i=0;i<20001;i++)
{
sum+=(arr[i]+1-d)/2*(i-10000);//除2向上取整
d=(2+arr[i]-d)%2;//避免出现负数
}
return sum;
}
};
这道题可以使用 两数之和 的解法,使用 O(n^2)的时间复杂度和 O(1) 的空间复杂度暴力求解,或者借助哈希表使用O(n) 的时间复杂度和 O(n) 的空间复杂度求解。但是这两种解法都是针对无序数组的,没有利用到输入数组有序的性质。利用输入数组有序的性质,可以得到时间复杂度和空间复杂度更优的解法
总结:个人做法没有A过,超出时间限制
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
vector<int> temp;
for(int i=0;i<numbers.size()-1;i++)
{
for(int j=i+1;j<numbers.size();j++)
{
if(numbers[i]+numbers[j]==target)
{
temp.push_back(i+1);
temp.push_back(j+1);
break;
}
}
if(temp.size())
break;
}
return temp;
}
};
官方做法一双指针
总结
- 双指针真的非常爽了,有效降低了时间复杂度。一个指向头部,一个指向尾部。
- 时间复杂度:O(N)。字符串长度N
- 空间复杂度:O(1)。常数
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int low=0,high=numbers.size()-1;
int sum=0;
while(low<high)
{
sum=numbers[low]+numbers[high];
if(sum==target)
{
return {low+1,high+1};
}
else if(sum>target)
{
high--;
}
else
{
low++;
}
}
return {-1,-1};
}
};
个人做法-二分查找
可以看看本人总结的二分查找模板
https://blog.youkuaiyun.com/qq_39309050/article/details/109097745
总结
- 这种sum和的形式肯定可以拆分成一个已知和一个查找的问题。
- 熟练使用应用二分查找的模板
- 时间复杂度:O(N logN)。其中O(N)的遍历第一个元素,O(logN)的二分查找,两者相乘
- 空间复杂度:O(1)
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
for(int i=0;i<numbers.size()-1;i++)
{
int findnum=target-numbers[i];
int low=i+1;
int high=numbers.size();//左闭右开
int mid=0;
//二分查找
while(low<high)
{
mid=low+(high-low)/2;
if(findnum>numbers[mid])
{
low=mid+1;
}
else
{
high=mid;
}
}
if(low==numbers.size())//如果最后找到的下标是右端点,则失效
continue;
if(numbers[low]==findnum)//判断找到的元素是否正确
return {i+1,low+1};
}
return {-1,-1};
}
};
使用了大佬的模板之后
https://blog.youkuaiyun.com/qq_39309050/article/details/109101552
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
for(int i=0;i<numbers.size()-1;i++)
{
int findnum=target-numbers[i];
int low=i+1;
int high=numbers.size()-1;//左闭右开
int mid=0;
//二分查找
while(low<=high)
{
mid=low+(high-low)/2;
if(findnum>numbers[mid])
{
low=mid+1;
}
else if(findnum<numbers[mid])
{
high=mid-1;
}
else if(findnum==numbers[mid])
return {i+1,mid+1};
}
}
return {-1,-1};
}
};
总结:
- 理解了双指针,此题相当容易。slow慢指针只有在fast快指针指向的元素不等于目标值的时候,才进行被替换和slow指针后移。
时间复杂度:O(N)。假设数组总共有 n个元素,i 和 j 至多遍历 2n 步
空间复杂度: O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow=0;
int len=nums.size();
for(int fast=0;fast<len;fast++)
{
if(nums[fast]!=val)
nums[slow++]=nums[fast];
}
return slow;
}
};
比较喜欢官方说的那个适用于删除较少元素的双指针操作
- 时间复杂度 :O(N)。最多n步
- 空间复杂度 : O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int len=nums.size();
int fast=0;
while(fast<len)
{
if(nums[fast]==val)
{
nums[fast]=nums[len-1];
len--;
}
else
{
fast++;
}
}
return len;
}
};
总结
- 快慢指针的基本套路
- 注意点:最后一次计算的slow值要跟maxslow值做一次比较
- 时间复杂度:O(N)
-空间复杂度:O(1)
class Solution {
public:
int findMaxConsecutiveOnes(vector<int>& nums) {
int slow = 0;
int maxslow = 0;
for(int i = 0; i < nums.size(); i++)
{
if(nums[i]!=1)
{
if(slow > maxslow)
{
maxslow = slow;
}
slow = 0;
}
else
{
slow++;
}
}
if(slow > maxslow)
{
maxslow = slow;
}
return maxslow;
}
};
总结:
- 慢指针用于从前往后扫描数组,快指针用于计算从慢指针位置向后扫描。
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int len = nums.size();
if(len == 0)
return 0;
long minlen = 1410065407;
long sum = 0;
int fast = 0;
int slow = 0;
int i = 0;
for(int fast = 0; fast < len; fast++)
{
sum += nums[fast];
if(sum >= s)
{
if(fast - slow + 1 < minlen)
{
minlen = fast - slow + 1;
}
sum = 0;
fast = slow++;
}
//cout << "sum:" << sum << "minlen:" << minlen << endl;
}
if(minlen == 1410065407)
return 0;
return minlen;
}
};
官方一:暴力解法
总结:
- INT_MAX的用法
//暴力解法
class Solution {
public:
int min(int a, int b)
{
return a > b ? b : a;
}
int minSubArrayLen(int s, vector<int>& nums) {
int len = nums.size();
if(len == 0)
return 0;
int sum = 0;
int minlen = INT_MAX;
for(int i = 0; i < len; i++)
{
sum = 0;
for(int j = i; j < len; j++)
{
sum += nums[j];
if (sum >= s)
{
minlen = min (minlen, j - i + 1);
break;
}
}
}
return minlen == INT_MAX ? 0 : minlen;
}
};
官方二:前缀和+二分 解法
总结:
- 这种做法我只能说很恶心了,主要是那个下标那里弄的傻傻的分不清。
- 假设原数组长度为len,即nums[0],nums[1]……nums[len-1]
- 建立len+1个元素的sum[]数组,其中sum[i]表示前i个元素的和
- 二分查找里面右端点是设置为len+1,也即最后查找结果如果是len+1则说明不符合要求。
class Solution {
public:
int min(int a, int b)
{
return a > b ? b : a;
}
int twoDivid(vector<int>& sum, int target)
{
int left = 0;
int right = sum.size();
while(left < right)
{
int mid = left + (right - left) / 2;
if(sum[mid] < target)
{
left = mid + 1;
}
else
{
right = mid;
}
}
return left;
}
int minSubArrayLen(int s, vector<int>& nums) {
int len = nums.size();
if(len == 0)
return 0;
vector<int> sum(len + 1, 0);
//sum[i]存放[0]+[1]……+[i-1]
//sum[i]表示前i个元素和
for(int i = 1; i <= len; i++)
{
sum[i] = sum[i - 1] + nums[i - 1];
cout << "sum[%d]-"<< i << " "<<sum[i] <<endl;
}
int target = 0;
int bound = 0;
int minlen = INT_MAX;
for(int i = 1; i <= len + 1; i++)
{
target = sum[i - 1] + s;
bound = twoDivid(sum, target);
//auto bound = lower_bound(sum.begin(), sum.end(), target);
if (bound != sum.size())
minlen = min(bound - i + 1,minlen);
}
return minlen == INT_MAX ? 0: minlen;
}
};
官方的可取之处
- lower_bound 函数的使用
- bound的类型不是int,需要特殊处理
for (int i = 1; i <= n; i++) {
int target = s + sums[i - 1];
auto bound = lower_bound(sums.begin(), sums.end(), target);
if (bound != sums.end()) {
ans = min(ans, static_cast<int>((bound - sums.begin()) - (i - 1)));
}
}
class Solution {
public:
int min(int a,int b)
{
return a<b ? a:b;
}
int minSubArrayLen(int s, vector<int>& nums) {
int len = nums.size();
if(len==0)
return 0;
int start = 0;
int end = 0;
int sum = 0;
int minlen = INT_MAX;
while(end < len)
{
sum += nums[end];
while(sum >= s)
{
minlen = min(end - start + 1, minlen);
sum -= nums[start++];
}
end++;
}
return minlen==INT_MAX ? 0 : minlen;
}
};
官方双指针做法
总结:
- 两个指针,start指针指向最小数组的首位,end指针指向最小数组的尾位,初始值都是0
- end指针不断地后移,一直到sum和大于s.循环:记录此时的长度,start指针后移,sum和减去首位元素值,一直到sum和小于s退出循环……
class Solution {
public:
int min(int a,int b)
{
return a<b ? a:b;
}
int minSubArrayLen(int s, vector<int>& nums) {
int len = nums.size();
if(len==0)
return 0;
int start = 0;
int end = 0;
int sum = 0;
int minlen = INT_MAX;
while(end < len)
{
sum += nums[end];
while(sum >= s)
{
minlen = min(end - start + 1, minlen);
sum -= nums[start++];
}
end++;
}
return minlen==INT_MAX ? 0 : minlen;
}
};