文章目录
- 1、反转字符串
- 2、反转字符串||
- 3、字符串最后一个单词的长度
- 4、找字符串中第一个只出现一次的字符
- 5、仅仅反转字母
- 6、验证一个字符串是否是回文
- 7、反转字符串中的单词【⭐】
- (1)移除给出字符串中的多余空格
- (2)反转整个字符串
- (3)反转单个单词
- 8、反转字符串中的单词|||
- 9、字符串相加
- 10、字符串相乘【⭐⭐】
- 代码走读🏃
- 调试观察💻
- 📚总结与提炼
1、反转字符串
首先我们来看第一道,先从简单一点的开始做起✍
① 题目描述:
力扣原题
class Solution {
public:
void reverseString(vector<char>& s) {
}
};
② 思路分析:
- 本题很简单,就是将题目中给出的字符串做一个前后逆置的操作。这边首先想到的就是双指针的一个思路,让一个指针
i
在前,一个指针j
在后,相对而行,不断交互二者位置上的字符,直到二者相遇为止
③ 代码展示:
- 代码很简洁,一个for循环就能搞定了
void reverseString(vector<char>& s) {
for(int i = 0, j = s.size() - 1; i < j; ++i, --j)
{
swap(s[i], s[j]);
}
}
④ 运行结果:
- 来看看执行结果,发现效率中一般,不过能AC就行😁
2、反转字符串||
接下去再来看第二道,稍稍复杂一些
① 题目描述:
力扣原题
class Solution {
public:
string reverseStr(string s, int k) {
}
};
② 思路分析:
-
本题相对上一题来说就发生了一些变化,虽然都是在反转字符串,但本题呢不是一个一个地在遍历,而是2k个2k个地在遍历,在题目给到的参数中还有一个k,我们在遍历这个字符串时是 一次遍历2k个,然后反转前k个
-
在不断执行的过程中总会碰到结束,此时题目又给出了两种结果。
- 也就是当剩余的字符少于2k个,但是大于等于k个的话,则反转前k个,这个其实我们可以和题干中的意思做一个结合,多加一个是否到达结尾即可
- 当剩余的字符连k个都不足的话,此时将剩余的全部反转即可
-
很多同学在写本题的时候都会纠结于这个2k,所以在循环遍历的时候会选择拿一个计数器去计数,然后当这个计数器到达的 2k 的时候就对前k个去做反转,其实没必要这样,我们在这里直接去修改循环遍历的次数即可,即
i += 2 * k
,让i
每次移动的距离就是 2k 个,那当我们在遍历的时候也无需去考虑那么多了
接下去呢我们就要在循环内部去反转对应的字符串了,这里大家可以自己实现一个(就是我们上一题所讲的代码),或者是直接使用库函数【reverse】
- 不过记住了在库函数这里我们要传递的是 迭代器,注意这里【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个, 翻转完后继续遍历
}
- 那在之后呢我们还要去考虑到这个剩余的字符问题,小于2k但是大于等于k已经包含在了上面的代码中,而我们此时要考虑的就是不足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;
}
④ 运行结果:
- 最后再来看下执行结果吧
3、字符串最后一个单词的长度
然后是第三题,我们来看看和 string类 API相关的一些题目
① 题目描述:
牛客原题
② 思路分析:
- 好,我们来分析一下本题该如何去进行求解。题目的意思很明确,就是给你一个字符串,然后计算出这个字符串最后一个单词的长度
- 我们知道单词都是以【空格】作为分割,此时我们只需要去找到最后一个空间即可,然后计算出从这个空格开始到结尾有多少字符
此时我们可以使用到的是string类中 rfind() 接口,从一个字符串开始从后往前进行寻找,找第一个空格
size_t pos = s.rfind(' ');
- 那如果找到了这个空格的话就可以去输出最后一个单词的长度的,因为这是【左闭右开】的,而我们不能计算上
pos
这个位置空格的长度,所以要从pos + 1
的位置开始计数
cout << s.size() - (pos + 1) << endl;
- 不过呢我们还要考虑到给到的字符串没有空格的情况,这个时候返回这个字符串的【size】即可
// 如果不存在空格, 直接返回当前字符串的长度
cout << s.size() << endl;
- 这里我们先执行一下试试看,发现取到的长度是5,这是为什么呢?
- 面对这个问题而言我们可以到VS上去调试一下,发现当使用
cin >>
流插入在进行读取的时候只读到了前面的【hello】,但是呢后面的【newcoder】却没有读取到,这个我们在讲解 STL中的string类 时就有说到过,对于像cin >>
、scanf()
这些都是会去缓冲区里面读内容,但是呢在读取到空格的时候就会自动截止了,而无法完成整行的读取
- 而我们若是要进行一整行读取的话可以使用
get()
,不过在这里呢我更推荐 getline() 函数,传递进流对象cin
和所要读取的string对象,就可以去读取到整行的信息
- 然后再去读取的时候就发现确实读到了这一整行
③ 代码展示:
- 代码如下,具体见运行结果
#include <iostream>
#include <string>
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;
}
}
④ 运行结果:
4、找字符串中第一个只出现一次的字符
第四题的话我们再来看看与其他数据结构相结合的题目
① 题目描述:
力扣原题
② 思路分析:
- 本题的题意很清晰,我们要去找的就是在一个字符串中第一次出现的,也是唯一一次出现的字符
- 那么也就意味着我们需要去每一个字符所出现的次数,这立马让我想到了【哈希表】,不知读者一看到此题是什么反应呢?
首先我们考虑先去定义一个哈希表出来,使用到的是
unordered_map
,【key】的类型是char
、【value】的类型是int
unordered_map<char, int> count;
- 接下去我们就遍历这个字符串s然后统计出每一个字符所出现的次数即可
for(char c: s)
{
count[c]++; // 统计每个字符出现的次数,放入哈希表中存起来
}
- 最后一步,我们再去遍历这个字符串,然后根据对应的字符到哈希表中去寻找即可,一旦找到一个字符所对应的【value】值是1的话,那就返回这个字符在字符串中所在下标。若是找了一圈还没有发现为1的话,则表示在字符串中并没有只出现一次的数字
// 遍历该统计数组,若是找到第一个只出现一次的,返回其坐标
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;
}
④ 运行结果:
5、仅仅反转字母
接下去我们进阶地要来做一做反转相关的题目
① 题目描述:
力扣原题
② 思路分析:
- 首先来解读一下本题的意思,和第一小题反转字符串很类似,但是呢它在反转的时候不是全部的字符都会反转,而是只反转那些 大小写的字母,除此之外都会别忽略
- 那首先我们双指针的做法还是可行的,只是在从两头往中间遍历的时候需要去判断一下当前字符是不是大小写字母。库里虽然是有API给我们调用,但是呢最好自己写一下,这样比较好控制
bool IsUpperOrLowerLetter(char ch)
{
if((ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z'))
return true;
return false;
}
- 下面我画了一个算法分解图,以题目中的示例3为例
- 上面这个交换的过程中最重要的逻辑就是下面的和这个对当前所在字符的判断,如果当前这个字符不是大小写中英文的话,就继续 前移或者后移
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;
}
④ 运行结果:
- 来看看执行结果,发现还不错哦!
6、验证一个字符串是否是回文
然后我们再来试试验证回文串,本题和上面一题部分类似,可做借鉴
① 题目描述:
力扣原题
② 思路分析:
- 首先来解释一下为什么本题可以借鉴上一题,原因就在于在进行某些判断的时候都需要去忽略一些除指定字符以外的其他字符,而且在遍历的时候也是采取双指针的做法,
- 下面就是对于当前字符是否为字母或者数字的一个判断
bool IsLetterOrDigit(char ch)
{
if((ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z')
|| (ch >= '0' && ch <= '9'))
return true;
return false;
}
然后我们来从头分析一下
- 首先的话题目给出了要求,我们在对这些字符进行判断的时候首先要将它们都转换为小写,可以看到我这里使用到了C++11的一个新语法【范围for】,而且对字符串中的每一个字符都做了引用,这样就可以使得内部的修改带动外部
// 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;
}
④ 运行结果:
7、反转字符串中的单词【⭐】
看了这么多简单题,我们来看一道中等题🚗
① 题目描述:
力扣原题
② 思路分析:
-
首先我们可以知道的是本题也是在反转一些东西,但反转的不是整个字符串,而是字符串中的每个单词,这就使有些同学感到些许疑惑了(・∀・(・∀・(・∀・*),让我反转整个字符串还行,就单体地反转里面的一部分,而且还得保持这个单词的顺序不能错乱
-
不仅如此,题目中还给出了这么一句话
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格
-
也就是下面这种,因此呢我们还要去考虑到字符串的前面、后面以及各个单词中间存在空格的情况
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
💬 那经过我上面这一说相信很多同学都懵逼了,那到底这道题该如何去做呢?
- 我大概把具体的步骤分为以下几步:
- 去除给出的目标串中多余的空格
- 将整个字符串做一个反转
- 逐个去将其中的子串单词一一翻转
- 完成单词的翻转
接下去我们就来分步骤地讲解一下
(1)移除给出字符串中的多余空格
- 首先就是第一步,移除给出字符串中的多余空格,这是第一步也是最关键的一步🔑因为只有在没有多余其他空格的干扰下才可以做整体翻转和局部翻转的可能
- 【题外话】在力扣中这个题目中并给出这个空间复杂度的限制,这样就可以自己再开辟一个字符串做放置移位操作。而且随着现在字符串库函数的越加丰富,对于字符串的操作几乎不用自己思考,只需要调用一下API即可,所以大家都调侃自己时API调用工程师,确实,有些API在开发的时候是可以给我们带来便利,但是在刷题的过程中,我们看到一个操作就立马想到用API去解决,而且这道题的核心代码用API可直接解决,那就失去了刷题的意义,像这题你完全可以用Java中String类的
split()
,去分割单词,然后定义一个新的string字符串,最后再把单词倒序相加,你要这样做那这就是道水题,没有任何意义 - 好的,言归正传,插了一些小话题,我们规定空间复杂度就只能是O(1),那么你就不可以再去使用辅助空间了,只能在原字符串中下功夫,这就可以想到我们的双指针解法了,还记得吗,我在力扣27 - 移除元素【双指针】中说到如何使用双指针循环交替换位去做消除元素的操作,这里其实也是一样的,那一题是消除元素,那这一题就是把元素换成空格而已
- 但是有些小伙伴很单纯,就是用一个for循环去遍历字符串s,然后遇到空格就用erase()删除一下。哈哈,确实,这就是很直白的写法,我也可以给代码,从如下代码可以看出,我是将移除空格分为了三个部分,分别是
①移除字符串当中的多余空格
②移除前导空格
③移除尾随空格 - 但是你以为这只是O(n)的时间复杂度吗,不,erase()这个API的底层实现就已经是O(n),然后外面在用一个for循环去遍历字符串,按就是O(n2)的时间复杂度,虽然是很清晰地分步骤解决了问题,但是却无故中增加了时间复杂度,而你自己可能还不知道
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);
}
- Ok,不多讲,我们的重点在于双指针,如何用双指针去除空格呢,上面说了,这就和移除元素是一个道理
- 快指针fast:用来获取新数组中的元素,即不为空格的位置
- 慢指针slow:获取新数组中需要更新的位置
先行给出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)
- 由于此时慢指针slow已经不是处于位置0了,所以将会进入这个if分支的判断,这个写法是C++中的写法,上述也有用到过,可以把slow++单独再拿出来,这样写只是为了美观简练而已,具体的意思是将慢指针slow当前位置置为空,然后慢指针后移一位,是为了解决每个单词之间的空格问题,
if(slow != 0) //解决每个单词之间的空格
s[slow++] = ' ';
⑥接着就是下一个单词的交替赋位,从下图可以看出,#hello#和#world#之间是有一个空格的,很好的解决了前面的所有空格
⑦最后一步,就是解决最后面的后置空格了,此时在字母d
赋位完后,双指针同时向后移动,快指针fast便指向了空,所以不进入任何分支判断,fast指针继续后移,超出字符串边界,自动结束外层for循环的遍历,此时进行这一步操作,使用resize()函数重新开始数组空间,慢指针slow当前所指位置即为新数组大小
s.resize(slow); //慢指针当前所指位置即为去空格后数组大小
这就是去除所有空格后的结果,大家在做完一个功能之后就可以点击【执行代码】看看是否成功
- 这还只是第一步,感受到这道题的魅力了吗🔥
(2)反转整个字符串
好,接下来我们进入第二步,也就是在去除了多余空格后,我们需要将整个字符串进行一个反转,其实这个就是开头讲到的那道题
void reverseString(string& s,int start,int end)
{
for(int i = start,j = end;i < j;++i,--j)
swap(s[i],s[j]);
}
- 当然大家也可以直接用reverse()算法,交换,这个算法我在C++ STL【常用算法】详解中有过详细介绍,如果不清楚的小伙伴可以去看看
这是反转后的样子,可以看出,就差把单个单词进行逐一翻转了
(3)反转单个单词
好的,最后就来到了我们反转单个单词的部分,加油,快爬到山顶了⛰
// 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; // 更新下一个单词的起始位置
}
}
- 首先我们需要定义一个变量去保存每次下一个单词的起始位置,因为这个起始位置随着指针的移动每次都会发生变化
- 然后就是去循环中遍历这个目标串s,这里的for循环为什么要遍历到s.size()呢,因为我们遍历到最后一个单词时后方已经没有空格了,所以这个情况需要单独判断,而分隔每个单词的条件就是遍历到s[i] == ’ ',这个时候表示一个单词已经遍历完毕,调用我们上面的
reverseString(s,start,i - 1);
- 去反转这个子串即可实现我们所要的效果,然后为什么要start = i + 1这个操作呢,因为此时你反转子串单词的时候i是处于空格的位置,但是下一个单词要从首字母开始,所以
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;
}
};
④ 运行结果:
- 从运行结果来看本算法也比较高效
8、反转字符串中的单词|||
本题的话如果你在做了上一题的话是完全没问题的,看看就知道了👀
① 题目描述:
力扣原题
② 思路分析:
- 相信你在看了题目之后已经知道怎么做了,因为这就是我们上一题所完成的第三步,此处便不做详解
③ 代码展示:
- 怎么样,看着代码是不是很眼熟
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;
}
};
④ 运行结果:
9、字符串相加
接下去的两道可能会比较复杂一些,因为涉及字符串的加减乘除
① 题目描述:
力扣原题
② 思路分析:
- 可以看到,题目的意思很简单,就是将两个字符串看做数值进行相加,但是呢最后又是两个字符串,那这怎么搞呢?很多同学一时半会没辙了
- 这里的话就要涉及到字符串的分割技术了。因两个数在相加的时候是需要从个位开始相加,所以我们肯定要把两个字符串中的的位数一个一个地取出来,将它们都转换为数值之后再进行相加,每一位上的数都是如此
- 但是呢,上面这一种只是普通的情况,我们来看看下面这一种,虽然也是各个位上的数进行相加,但是呢出现了进位的情况,加法进位相信大家在小学阶段就已经学过了,不过呢这个进位要如何进行处理,每个位上的数字要如何进行拼接,且听我娓娓道来🍵
这里我们也是采取的双指针的思路,这么做下来的话你应该可以发现【双指针】在字符串的题目中出现的还是非常频繁的,所以我们要掌握这一快,在做题的时候才能事半而功倍
- 让这两个指针分别从末尾开始即可
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;
- 具体的话还是看图示吧 ⇒ 很清楚,如果我们要取到这个进制位1的话,就需要让这个
ret / 10
,如果我们需要取到这个除进制位外的余数的话,就需要让ret %= 10
才能取得到
- 那当我们有这个位上的数值后,就可以将其加入到
retStr
中了,不过呢这还是一个数值,我们还要将其转换为字符才可,+ '0'
即可
// 累加到新的string对象中去(尾插)
retStr += (ret + '0');
- 然后别忘了前移这两个指针,因为我们还要去计算下一位
end1--;
end2--;
- 最后的话再来写一下循环结束的条件,很多同学都给出
end1 >= 0 && end2 >= 0
,但是这可行吗?我们知道循环的条件是继续的条件,对于逻辑与&&
来说只有表达式两边都为真的时候才为真,那么当有一个为假的时候就不对了。 - 那么当一个字符串遍历结束后我们就结束相加吗?
while(end1 >= 0 || end2 >= 0)
- 我们来看一下这个案例就可以了,【999 + 1 = 1000】,如果我们执行完【9 + 1 = 10】后就结束了,那么最后返回的就是0了,这很明显是不符合实际的。那么当一个结束了之后还不能结束我们需要使用的是 按位或
||
然后我们就返回这个
retStr
去执行一下吧,但是先计算的结果刚好反了
- 如果对 string类 中的
operator+=()
接口了解的话就可以清楚我们这里其实是做了一个尾插的操作,所以这个顺序才会是倒着的,那我们要获取到正确的顺序的话采取reverse()
做一个颠倒即可
reverse(retStr.begin(), retStr.end());
- 这回再去执行的话确实是没什么问题了,但是呢提交之后可以发现,有一些测试用例的话我们是跑不过(示例能跑过不代表所有测试用例都能跑过)
- 这个测试用例其实大家可以带入到我们上面的这个循环中,就发现它跑完一遍的话就结束了,因为位数只有一位,那么此时我们在循环结束之后应该再去做一个判断才行,如果这个进制位不为0的话(正常结束进制位一定为0),我们就要再把这个进制位给追加上去
// 如果在出了循环后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;
}
};
④ 运行结果:
- 来看看执行结果,发现效率中一般,不过能AC就行😁
10、字符串相乘【⭐⭐】
看完了 字符串相加 后,我们再来看 字符串相乘,本题要在上一题的基础上难很多,做好准备,发车了🚗
① 题目描述:
力扣原题
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)
{
// ...
}
- 下面有两个string类的对象,
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]
则指的是通过循环获取到的每一位被乘数,二者的乘积还要再加上进制位,因为乘法和加法一定也会产生进制位,那么接下去两句在更新 当位结果和 进制位 的时候,读者就不会那么生疏了 - 那随着循环的一步步进行,num2中当前的这一位乘数和被乘数就计算出了结果
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;
}
- 我们可以到VS下来测试一下这种情况,可以看到当这个
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】和【456】带读者来看看
- 我们通过动图来观察,此时第一次进入循环,即要拿被乘数
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
中去
接下去开始第二轮
- 首先我们来看看在第二次的相乘前这个添加0的逻辑,记住此时个位上的数已经乘完了,开始计算十位上的数。通过调试我们可以观察到,此时会往这个小结果集
curr
中添加一个0,而且只添加一个
- 被乘数始终都是
123
,此轮的乘数变成了5
,继续开始从低位往高位进行相乘
- 第二次相乘,需要考虑到进制位的问题
- 第三次遍历,
1 * 5 = 5
,加上进制位1
后变成了6
,继续将其加入到小结果集中
- 那么现在的话第二轮相乘就结束了,如果观察仔细的同学可以发现,每次当循环结束的时候,这个
carry
的值都会是【0】,这算作是正常结束。如果当本轮结束后carry != 0
的话,此时就需要考虑到特殊情况了,不过一般的话是不存在的,carry
都会等于0
- 还是一样,在一轮的乘积之后我们还是要去对其作一个反转,可以观察到反转之后的结果为
6150
,最后的这个【0】便是我们在最前面加上的,
- 所以接下去我们将这个小结果集加入到大结果集时,相当于是在做一个相加的操作,这一块大家可以自行去调试。下面我们看一个结果,它们相加之后即为
6888
接下去我们开始第三轮
- 首先还是一样,我们要去执行这个添加0的操作,此时是与百位上的数进行相乘,所以通过调试我可以看到此时会往【curr】添加2个0,这也是为后面做反转的时候作准备
- 接下去开始相乘的逻辑,此时的
[y]
固定为乘数部分的百位4
,然后内部通过循环来使其与被乘数的每一位进行相乘。可以看到此时的[x]
为3
,所以在相乘之后为【12】,此刻我们对10取余,然后一样将余数加入到小结果集中
- 接下去第二次相乘,
2 * 4 = 8
,但是不要忘记加上一个进制位1哦,所以结果即为9
- 第三次相乘:
1 * 4 = 4
,不过呢此次的进制位为0,所以在加上之后还是为4
- 那么第三轮的相乘算是完成了,还是一样去进行一个反转,此时我们可以观察到反转后的结果为
49200
- 最后的话再将第三轮执行后的结果累加到【ans】中去,最最后的结果即为
56088
再与我们上面的结果进行对比的话可以发现结果是一致的
💬 通过上面的调试相信读者对这段代码的执行一定很清楚了,可以试着自己再去调调看哦😉
④ 运行结果:
- 最后再来看看执行结果吧
更新中。。。
📚总结与提炼
最后来总结一下本文所学习的内容📖
- 在本文中,我们对校招面试当中可能会出现的字符串相关算法题进行了一个汇总,当然还不止这些,后续如果遇到了更好的题目我还会继续再放上来
- 上述的题目中有关【字符串】与【双指针】进行配合以到达反转目的 的题目比较重要,读者需要牢牢掌握