首先我们来看第一道,先从简单一点的开始做起✍
① 题目描述:
力扣原题
class Solution {
public:
void reverseString(vector<char>& s) {
}
};
② 思路分析:
i
在前,一个指针j
在后,相对而行,不断交互二者位置上的字符,直到二者相遇为止③ 代码展示:
void reverseString(vector<char>& s) {
for(int i = 0, j = s.size() - 1; i < j; ++i, --j)
{
swap(s[i], s[j]);
}
}
④ 运行结果:
接下去再来看第二道,稍稍复杂一些
① 题目描述:
力扣原题
class Solution {
public:
string reverseStr(string s, int k) {
}
};
② 思路分析:
本题相对上一题来说就发生了一些变化,虽然都是在反转字符串,但本题呢不是一个一个地在遍历,而是2k个2k个地在遍历,在题目给到的参数中还有一个k,我们在遍历这个字符串时是 一次遍历2k个,然后反转前k个
在不断执行的过程中总会碰到结束,此时题目又给出了两种结果。
很多同学在写本题的时候都会纠结于这个2k,所以在循环遍历的时候会选择拿一个计数器去计数,然后当这个计数器到达的 2k 的时候就对前k个去做反转,其实没必要这样,我们在这里直接去修改循环遍历的次数即可,即i += 2 * k
,让i
每次移动的距离就是 2k 个,那当我们在遍历的时候也无需去考虑那么多了
接下去呢我们就要在循环内部去反转对应的字符串了,这里大家可以自己实现一个(就是我们上一题所讲的代码),或者是直接使用库函数【reverse】
i + k
这个位置reverse(s.begin() + i, s.begin() + i + k);
i + k <= s.size()
,因为i + k
这个位置是不包含的,所以我们在判断条件中要加上continue
呢,原因就在于我们只是反转前k个,而后k个是不动的,所以在反转完后直接继续向后比较遍历 2k 个即可if(i + k <= s.size())
{
reverse(s.begin() + i, s.begin() + i + k);
continue; // 只翻转前k个, 翻转完后继续遍历
}
// 考虑到最后的字符少于k个的情况
reverse(s.begin() + i, s.end());
③ 代码展示:
string reverseStr(string s, int k) {
for(int i = 0;i < s.size(); i += 2 * k)
{
if(i + k <= s.size())
{
reverse(s.begin() + i, s.begin() + i + k);
continue; // 只翻转前k个, 翻转完后继续遍历
}
// 考虑到最后的字符少于k个的情况
reverse(s.begin() + i, s.end());
}
return s;
}
④ 运行结果:
然后是第三题,我们来看看和 string类 API相关的一些题目
① 题目描述:
牛客原题
② 思路分析:
此时我们可以使用到的是string类中 rfind() 接口,从一个字符串开始从后往前进行寻找,找第一个空格
size_t pos = s.rfind(' ');
pos
这个位置空格的长度,所以要从pos + 1
的位置开始计数cout << s.size() - (pos + 1) << endl;
// 如果不存在空格, 直接返回当前字符串的长度
cout << s.size() << endl;
cin >>
流插入在进行读取的时候只读到了前面的【hello】,但是呢后面的【newcoder】却没有读取到,这个我们在讲解 STL中的string类 时就有说到过,对于像cin >>
、scanf()
这些都是会去缓冲区里面读内容,但是呢在读取到空格的时候就会自动截止了,而无法完成整行的读取get()
,不过在这里呢我更推荐 getline() 函数,传递进流对象cin
和所要读取的string对象,就可以去读取到整行的信息③ 代码展示:
#include
#include
using namespace std;
int main() {
string s;
getline(cin, s);
//cin >> s;
size_t pos = s.rfind(' ');
if (pos) {
cout << s.size() - (pos + 1) << endl;
}
else {
// 如果不存在空格, 直接返回当前字符串的长度
cout << s.size() << endl;
}
}
④ 运行结果:
第四题的话我们再来看看与其他数据结构相结合的题目
① 题目描述:
力扣原题
② 思路分析:
首先我们考虑先去定义一个哈希表出来,使用到的是
unordered_map
,【key】的类型是char
、【value】的类型是int
unordered_map<char, int> count;
for(char c: s)
{
count[c]++; // 统计每个字符出现的次数,放入哈希表中存起来
}
// 遍历该统计数组,若是找到第一个只出现一次的,返回其坐标
for(int i = 0;i < s.size(); ++i)
{
if(count[s[i]] == 1)
return i;
}
③ 代码展示:
int firstUniqChar(string s) {
unordered_map<char, int> count;
for(char c: s)
{
count[c]++; //统计每个字符出现的次数,放入哈希表中存起来
}
//遍历该统计数组,若是找到第一个只出现一次的,返回其坐标
for(int i = 0;i < s.size(); ++i)
{
if(count[s[i]] == 1)
return i;
}
return -1;
}
④ 运行结果:
接下去我们进阶地要来做一做反转相关的题目
① 题目描述:
力扣原题
② 思路分析:
bool IsUpperOrLowerLetter(char ch)
{
if((ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z'))
return true;
return false;
}
while(begin < end && !IsUpperOrLowerLetter(s[begin]))
begin++;
while(begin < end && !IsUpperOrLowerLetter(s[end]))
end--;
③ 代码展示:
bool IsUpperOrLowerLetter(char ch)
{
if((ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z'))
return true;
return false;
}
string reverseOnlyLetters(string s) {
int begin = 0, end = s.size() - 1;
while(begin < end)
{
while(begin < end && !IsUpperOrLowerLetter(s[begin]))
begin++;
while(begin < end && !IsUpperOrLowerLetter(s[end]))
end--;
swap(s[begin++], s[end--]);
}
return s;
}
④ 运行结果:
然后我们再来试试验证回文串,本题和上面一题部分类似,可做借鉴
① 题目描述:
力扣原题
② 思路分析:
bool IsLetterOrDigit(char ch)
{
if((ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z')
|| (ch >= '0' && ch <= '9'))
return true;
return false;
}
然后我们来从头分析一下
// 1.将字符串中的所有字符转换为小写
for(auto& c: s)
{
if(c >= 'A' && c <= 'Z')
c += 32;
}
return false
。如果在遍历结束了之后没发现不同的话这就是个回文串// 通过循环略过非字母或 数字的字符
while(begin < end && !IsLetterOrDigit(s[begin])){
begin++;
}
while(begin < end && !IsLetterOrDigit(s[end])){
end--;
}
return true
即可if(s.size() == 0)
return true;
③ 代码展示:
bool IsLetterOrDigit(char ch)
{
if((ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z')
|| (ch >= '0' && ch <= '9'))
return true;
return false;
}
bool isPalindrome(string s) {
if(s.size() == 0)
return true;
// 1.将字符串中的所有字符转换为小写
for(auto& c: s)
{
if(c >= 'A' && c <= 'Z')
c += 32;
}
// 2.前后指针遍历判断
int begin = 0, end = s.size() - 1;
while(begin < end)
{
// 通过循环略过非字母或 数字的字符
while(begin < end && !IsLetterOrDigit(s[begin])){
begin++;
}
while(begin < end && !IsLetterOrDigit(s[end])){
end--;
}
if(s[begin] != s[end])
{
return false;
}else{
begin++;
end--;
}
}
return true;
}
④ 运行结果:
看了这么多简单题,我们来看一道中等题
① 题目描述:
力扣原题
② 思路分析:
首先我们可以知道的是本题也是在反转一些东西,但反转的不是整个字符串,而是字符串中的每个单词,这就使有些同学感到些许疑惑了(・∀・(・∀・(・∀・*),让我反转整个字符串还行,就单体地反转里面的一部分,而且还得保持这个单词的顺序不能错乱
不仅如此,题目中还给出了这么一句话
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格
也就是下面这种,因此呢我们还要去考虑到字符串的前面、后面以及各个单词中间存在空格的情况
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
那经过我上面这一说相信很多同学都懵逼了,那到底这道题该如何去做呢?
接下去我们就来分步骤地讲解一下
split()
,去分割单词,然后定义一个新的string字符串,最后再把单词倒序相加,你要这样做那这就是道水题,没有任何意义void removeExtraspaces(string& s)
{
//移除字符串当中的多余空格
for(int i = s.size() - 1;i > 0; --i)
{
if(s[i] == s[i - 1] && s[i] == ' ')
s.erase(s.begin() + i);
}
//移除前导空格
if(s.size() > 0 && s[0] == ' ')
s.erase(s.begin());
//移除尾随空格
if(s.size() > 0 && s[s.size() - 1] == ' ')
s.erase(s.begin() + s.size() - 1);
}
先行给出C++代码
void removeExtraSpaces(string& s)
{
int slow = 0;
for(int fast = 0;fast < s.size(); ++fast)
{
if(s[fast] != ' ') //当快指针指向不为空格时,进行互相替换
{
if(slow != 0) //解决每个单词之间的空格
s[slow++] = ' ';
//若slow当前为0,则直接替换,为了解决前导空格
while(fast < s.size() && s[fast] != ' ')
s[slow++] = s[fast++]; //指针元素互换,直到一个单词结束
}
}
s.resize(slow); //慢指针当前所指位置即为去空格后数组大小
}
交替过程展示
还是一样,一张张分部图解手撕算法,为的是能让大家看清指针走的每一步,所以不要怕麻烦,跟着我一步一步来
① 首先,只有当fast快指针指向不为空时才做,交替,但一开始快指针指向首处,因此不进if(s[fast] != ' ')
的判断,fast快指针直接后移
② 若快指针遍历到不为空,与慢指针进行替换,此处进入的是这个while循环,而不是if(slow != 0)这个判断,因为此时慢指针是指向位置0的,所以直接进行交替即可,也就是这样的操作,可以代替erase()的那一小部分的前置空格的操作
while(fast < s.size() && s[fast] != ' ')
s[slow++] = s[fast++]; //指针元素互换,直到一个单词结束
③ 接下来注意了,这是一个while循环,此时并没有跳出这个while循环,因为快指针还没有遍历到空格而且快指针还没到达末尾,所以在上一次替换之后,双指针后移,继续进行一个替换操作,这时候第二个字母e就被继续替换
这个单词while循环后面是一样的操作,不做赘述,进入关键一步
④ 在交替放置完字母o之后,双指针同时后移,这时候快指针碰到了空格,即跳出while循环
⑤ 这个时候回到最初的大循环,fast指针后移一位,发现后面还是空格,所以再移动一位,这个时候边碰到了字母w,进入if(s[fast] != ’ ') 这个if分支的判断
for(int fast = 0;fast < s.size(); ++fast)
if(slow != 0) //解决每个单词之间的空格
s[slow++] = ' ';
⑥接着就是下一个单词的交替赋位,从下图可以看出,#hello#和#world#之间是有一个空格的,很好的解决了前面的所有空格
⑦最后一步,就是解决最后面的后置空格了,此时在字母d
赋位完后,双指针同时向后移动,快指针fast便指向了空,所以不进入任何分支判断,fast指针继续后移,超出字符串边界,自动结束外层for循环的遍历,此时进行这一步操作,使用resize()函数重新开始数组空间,慢指针slow当前所指位置即为新数组大小
s.resize(slow); //慢指针当前所指位置即为去空格后数组大小
这就是去除所有空格后的结果,大家在做完一个功能之后就可以点击【执行代码】看看是否成功
好,接下来我们进入第二步,也就是在去除了多余空格后,我们需要将整个字符串进行一个反转,其实这个就是开头讲到的那道题
void reverseString(string& s,int start,int end)
{
for(int i = start,j = end;i < j;++i,--j)
swap(s[i],s[j]);
}
这是反转后的样子,可以看出,就差把单个单词进行逐一翻转了
好的,最后就来到了我们反转单个单词的部分,加油,快爬到山顶了⛰
// 3.反转单个单词
int start = 0; // 标记每个单词的起始位置
for(int i = 0;i <= s.size(); ++i)
{
// 直到遍历到一个空格位置才算结束一个单词
if(i == s.size() || s[i] == ' ')
{ //i == s.size() - 遍历到最后一个单词的末尾要单独判断,因为其后无空格
reverseString(s,start,i - 1); //反转单词
start = i + 1; // 更新下一个单词的起始位置
}
}
reverseString(s,start,i - 1);
i + 1
就是移动到下一个单词的初始位置,将其保存在start中,每次反转子串时我们传入的初始位置就是start③ 代码展示:
class Solution {
public:
void removeExtraSpaces(string& s)
{
int slow = 0;
for(int fast = 0;fast < s.size(); ++fast)
{
if(s[fast] != ' ') //当快指针指向不为空格时,进行互相替换
{
if(slow != 0) //解决每个单词之间的空格
s[slow++] = ' ';
//若slow当前为0,则直接替换,为了解决前导空格
while(fast < s.size() && s[fast] != ' ')
s[slow++] = s[fast++]; //指针元素互换,直到一个单词结束
}
}
s.resize(slow); //慢指针当前所指位置即为去空格后数组大小
}
void reverseString(string& s,int start,int end)
{
for(int i = start,j = end;i < j;++i,--j)
swap(s[i],s[j]);
}
string reverseWords(string s) {
//1.移除给出字符串中的多余空格
removeExtraSpaces(s);
//2.反转整个字符串
reverseString(s, 0, s.size() - 1);
//3.反转单个单词
int start = 0; //标记每个单词的起始位置
for(int i = 0;i <= s.size(); ++i)
{
//直到遍历到一个空格位置才算结束一个单词
if(i == s.size() || s[i] == ' ')
{ //i == s.size() - 遍历到最后一个单词的末尾要单独判断,因为其后无空格
reverseString(s,start,i - 1); //反转单词
start = i + 1; //更新下一个单词的起始位置
}
}
return s;
}
};
④ 运行结果:
本题的话如果你在做了上一题的话是完全没问题的,看看就知道了
① 题目描述:
力扣原题
② 思路分析:
③ 代码展示:
class Solution {
public:
void ReverseStr(string& s, int begin, int end)
{
for(int i = begin, j = end; i < j; ++i, --j)
{
swap(s[i], s[j]);
}
}
string reverseWords(string s) {
int start = 0;
for(int i = 0;i <= s.size(); ++i)
{
if(i == s.size() || s[i] == ' ')
{
ReverseStr(s, start, i - 1);
start = i + 1;
}
}
return s;
}
};
④ 运行结果:
接下去的两道可能会比较复杂一些,因为涉及字符串的加减乘除
① 题目描述:
力扣原题
② 思路分析:
这里我们也是采取的双指针的思路,这么做下来的话你应该可以发现【双指针】在字符串的题目中出现的还是非常频繁的,所以我们要掌握这一快,在做题的时候才能事半而功倍
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
// 首先获取到两个字符串的尾数
int val1 = end1 >= 0 ? num1[end1] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2] - '0' : 0;
carry
呢就是我们上面所说的进制,一开始无需理会,因为一定是为0的,然后在下面继续更新这个进制位int ret = val1 + val2 + carry;
// 更新进位值
carry = ret / 10;
// 取出当前位上的余数
ret %= 10;
ret / 10
,如果我们需要取到这个除进制位外的余数的话,就需要让ret %= 10
才能取得到retStr
中了,不过呢这还是一个数值,我们还要将其转换为字符才可,+ '0'
即可// 累加到新的string对象中去(尾插)
retStr += (ret + '0');
end1--;
end2--;
end1 >= 0 && end2 >= 0
,但是这可行吗?我们知道循环的条件是继续的条件,对于逻辑与&&
来说只有表达式两边都为真的时候才为真,那么当有一个为假的时候就不对了。while(end1 >= 0 || end2 >= 0)
||
然后我们就返回这个
retStr
去执行一下吧,但是先计算的结果刚好反了
operator+=()
接口了解的话就可以清楚我们这里其实是做了一个尾插的操作,所以这个顺序才会是倒着的,那我们要获取到正确的顺序的话采取reverse()
做一个颠倒即可reverse(retStr.begin(), retStr.end());
// 如果在出了循环后carry为1的话, 则表示二者只有1位的长度
if(carry == 1)
{
retStr += '1';
}
③ 代码展示:
class Solution {
public:
string addStrings(string num1, string num2) {
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
string retStr;
int carry = 0; // 进位值
// 一个结束之后还不能结束
while(end1 >= 0 || end2 >= 0)
{
// 首先获取到两个字符串的尾数
int val1 = end1 >= 0 ? num1[end1] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2] - '0' : 0;
// 累加当前位置上的数字
int ret = val1 + val2 + carry;
// 更新进位值
carry = ret / 10;
// 取出当前位上的余数
ret %= 10;
// 累加到新的string对象中去(尾插)
retStr += (ret + '0');
end1--;
end2--;
}
// 如果在出了循环后carry为1的话, 则表示二者只有1位的长度
if(carry == 1)
{
retStr += '1';
}
// 最后再对尾插后的字符串做一个翻转
reverse(retStr.begin(), retStr.end());
return retStr;
}
};
④ 运行结果:
看完了 字符串相加 后,我们再来看 字符串相乘,本题要在上一题的基础上难很多,做好准备,发车了
① 题目描述:
力扣原题
class Solution {
public:
string multiply(string num1, string num2) {
}
};
② 思路分析:
例如下面的这个
738
不能直接和615
进行相加,而是要和6150
进行相加,因为这是上面的数与十位数【5】相乘所得的结果,那么下面也是一样,我们要和49200
进行相加
经过上面这么一分析,相信你一定觉得这个题目要考虑的因素有很多了。不用怕,我们立马来进行分析
num1
或者是num2
存在字符0相同的话,那不需要再进行相乘了,因为任何数与0相乘一定为0,那么我们直接返回“0”
if(num1 == "0" || num2 == "0")
return "0";
接下去我们要明确的一点是,我们要让那些数进行相乘?要乘几次?
num1
为被乘数,num2
是每一位乘数,它有几位我们就需要乘几次,所以我们这里拿【sz1】和【sz2】分别去做一个记录int sz1 = num1.size(); // 被乘数
int sz2 = num2.size(); // 次数
num2
的位数即为需要相乘的次数,那我们在这里给到的外层循环就是去遍历这个sz2
的大小for(int i = sz2 - 1; i >= 0; --i)
{
// ...
}
ans
表示最后累加总和后所需要存放的字符串;curr
则表示我们每乘完一次构后所需要累加到字符串string ans = "0";
string curr = "";
0
// 给此处其余位添加0
for(int j = sz2 - 1; j > i; --j){
curr.push_back(0 + '0');
}
那接下去呢我们就可以去获取到对应的字符,然后将它们转化为数值进行运算了
[y]
指的就是num2中的那一位乘数,而[x]
则指的是通过循环获取到的每一位被乘数,二者的乘积还要再加上进制位,因为乘法和加法一定也会产生进制位,那么接下去两句在更新 当位结果和 进制位 的时候,读者就不会那么生疏了int carry = 0; // 进制位
int ret = 0;
// 开始逐位累乘
int y = num2[i] - '0'; // 获取到当前这一位的数值
for(int k = sz1 - 1;k >= 0; --k)
{
int x = num1[k] - '0'; // 获取到被乘数的数值位
ret = x * y + carry;
curr.push_back(ret % 10 + '0');
carry = ret / 10; // 更新进制位
}
// 考虑到个位数的问题
if(carry != 0){
curr.push_back(carry % 10 + '0');
carry /= 10;
}
num1
和num2
都为个位数的时候,它们在相乘时只会进入一次循环,此时可以看到这个carry
是为3的,因此还会再进入下面的那个if判断,将其追加到【curr】中push_back()
即尾插,那里面的字符串都是颠倒的,还要调用一下reverse()
函数去做一个反转// 翻转字符串
reverse(curr.begin(), curr.end());
当然对于上面的这段逻辑只是被乘数与一个乘数之间的计算结果,我们要的是所有的结果之和
// 累加每一个计算完后的字符串
ans = AddStrings(ans, curr);
③ 整体代码展示:
class Solution {
public:
string multiply(string num1, string num2) {
if(num1 == "0" || num2 == "0")
return "0";
int sz1 = num1.size(); // 被乘数
int sz2 = num2.size(); // 次数
string ans = "0";
// 外层遍历次数
for(int i = sz2 - 1; i >= 0; --i)
{
string curr = "";
// 给此处其余位添加0
for(int j = sz2 - 1; j > i; --j){
curr.push_back(0 + '0');
}
int carry = 0; // 进制位
int ret = 0;
// 开始逐位累乘
int y = num2[i] - '0'; // 获取到当前这一位的数值
for(int k = sz1 - 1;k >= 0; --k)
{
int x = num1[k] - '0'; // 获取到被乘数的数值位
ret = x * y + carry;
curr.push_back(ret % 10 + '0');
carry = ret / 10; // 更新进制位
}
// 考虑到个位数的问题
if(carry != 0){
curr.push_back(carry % 10 + '0');
carry /= 10;
}
// 翻转字符串
reverse(curr.begin(), curr.end());
// 累加每一个计算完后的字符串
ans = AddStrings(ans, curr);
}
return ans;
}
// 两个字符串相加逻辑
string AddStrings(string& num1, string& num2)
{
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
int ret = 0;
int carry = 0;
string retStr;
while(end1 >= 0 || end2 >= 0 || carry != 0)
{
int val1 = end1 >= 0 ? num1[end1] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2] - '0' : 0;
ret = val1 + val2 + carry;
carry = ret / 10;
retStr += ret % 10 + '0';
end1--;
end2--;
}
reverse(retStr.begin(), retStr.end());
return retStr;
}
};
经过上面的代码走读,相信读者已经明白了一些原理,但是呢心中一定感觉还有些含糊。接下去我将会带着你一步步地去做调试,来看看这段代码究竟是如何
123
和第一个乘数位4
进行计算,此时呢我们是无需添加0的,所以这里的循环不进入是对的,记住这边的这个循环的结束条件是j < i
,而不是j <= i
,否则在第一次进入循环的时候就会多出来一个0了(博主在这里踩过坑,希望读者注意!!)3
与乘数的最低位6
展开的计算curr
中,更新进制位carry
2
,乘数y还是一样没有改变是6
,它们相乘后的结果是2 * 6 = 12
,不过呢还要再加上个位数进上来的进制位1,结果就是13
,那此时再把余数3
尾插进【curr】中1
,乘数y还是一样没有改变是6
,相乘后的结果为6
,不过呢还要加上十位的进制位,那么最后的结果就是7
,将其继续放到【curr】里即可ans
中去接下去开始第二轮
curr
中添加一个0,而且只添加一个123
,此轮的乘数变成了5
,继续开始从低位往高位进行相乘1 * 5 = 5
,加上进制位1
后变成了6
,继续将其加入到小结果集中carry
的值都会是【0】,这算作是正常结束。如果当本轮结束后carry != 0
的话,此时就需要考虑到特殊情况了,不过一般的话是不存在的,carry
都会等于06150
,最后的这个【0】便是我们在最前面加上的,6888
接下去我们开始第三轮
[y]
固定为乘数部分的百位4
,然后内部通过循环来使其与被乘数的每一位进行相乘。可以看到此时的[x]
为3
,所以在相乘之后为【12】,此刻我们对10取余,然后一样将余数加入到小结果集中2 * 4 = 8
,但是不要忘记加上一个进制位1哦,所以结果即为9
1 * 4 = 4
,不过呢此次的进制位为0,所以在加上之后还是为449200
56088
通过上面的调试相信读者对这段代码的执行一定很清楚了,可以试着自己再去调调看哦
④ 运行结果:
更新中。。。
最后来总结一下本文所学习的内容