前言
作为一直使用C的新手, 对字符串的处理方式一直局限于字符数组和#include<string.h>
, 虽然知道C++中的string用来处理字符串更为方便, 但一直没机会系统学习. 最近仍是希望通过 洛谷题单 用string的方式来编写程序, 加强熟练性和对"类"的概念的理解.
基本知识若干
- 若要使用C++中的string类, 要包含
#include<string>
头文件, 要注意的是, 该同文件的作用是可以使用string类, 而#include<cstring>
的作用是使用如memset(), strcmp(), strcat(), 之类的函数, 并不代表同一个东西 - (应该)需要包含命名空间
using namespace std;
- string类的基本操作在例题中逐个分析, 这里先记录string的定义方法:
string s;
即将s定义为string类, 之后使用cin 或 getline 对其赋值即可 - … …
引例一
代码实现
#include<iostream>
#include<string>
using namespace std;
string a;
int main(void)
{
cin >> a;
for(int i=0; i <a.length(); i++)
{
a[i] = toupper(a[i]);
}
cout << a << endl;
return 0;
}
分析
- 很简单的一道题目, 用来成为使用string解题的第一题刚刚好, 注意所包含的头文件, 命名空间和定义方式即可
cin >> a;
这条语句替换为getline(cin,a);
也可以, getline函数返回的是流参数, 因此可以作为while()循环的条件用来判断输入是否结束, 同时getline可以得到 一行 , 而cin遇到 空格 和 换行符 都会结束- 代码中for循环的部分可以发现, 如果要取string类中单个元素(字符), 可以将其视为字符数组, 用下标的方式即可对其内单个元素(字符)进行访问
- 学习到的一个成员函数, a.length(); 返回string中元素个数
- 另外学习到两个库函数(?) totower(x)和tolower(x), 包含在头文件
#include<iostream>
中, 作用分别是将x转变为大写字母或小写字母(不是字母则不变), 但其只能对单个字符产生效果, 而不能传递给其一整个字符串.
引例二
代码实现
#include<cstdio>
#include<iostream>
#include<string>
#include<cctype>
using namespace std;
int len(int x)
{
int lenth = 0;
if(x==0) return 1;
else if(x<0) lenth ++;
while(x!=0)
{
x /= 10;
lenth ++;
}
return lenth;
}
int change(string s)
{
int ans=0;
for(int i=0; i<s.size(); i++)
ans = ans*10+s[i]-48;
return ans;
}
int main(void)
{
int t;
scanf("%d", &t);
getchar();
getchar();
string s;
char c;
while(t--)
{
getline(cin, s);
if(s[0]>='a' && s[0]<='c')
{
c = s[0];
}
int flag = 0;
string a, b;
for(int i=0; i<s.length(); i++)
{
if(s[i]>='0'&&s[i]<='9'&&!flag)
{
flag = 1;
while(s[i]>='0'&&s[i]<='9')
{
a.append(1,s[i++]);
}
}
if(s[i]>='0'&&s[i]<='9'&&flag)
{
while(s[i]>='0'&&s[i]<='9')
{
b.append(1,s[i++]);
}
break;
}
}
int x, y;
x = change(a);
y = change(b);
if(c=='a')
{
int sum = x + y;
printf("%d+%d=%d\n", x, y, sum);
printf("%d\n", len(x)+len(y)+len(sum)+2);
}
else if(c=='b')
{
int sum = x - y;
printf("%d-%d=%d\n", x, y, sum);
printf("%d\n", len(x)+len(y)+len(sum)+2);
}
else
{
int sum = x * y;
printf("%d*%d=%d\n", x, y, sum);
printf("%d\n", len(x)+len(y)+len(sum)+2);
}
}
return 0;
}
分析
收获很多的一道题, 一个一个说
- 首先, 对题目本身而言难度并不大, 学习到的第一个点是: 如果要向string类字符串后添加字符(串), 最好不要用下标法. 本题一开始即使用这种方法, 虽然在调试中发现a, b中的某些值确实发生了改变, 但当我想打印a, b时却只有空行.
- 推荐的做法是使用+=拼接字符串的方法, 或使用成员函数.append()向队尾添加字符(串).
其中.append()函数共有三种常见使用方法:
①. .append(str)
②. .append(str, 起始位置, 截止位置)
③. .append(次数, c) — c为单个字符
本题中使用的就是第三种方法在队尾添加所需字符 - C11标准新添加了stoi, atoi这类将字符串转化为数字的函数, 但考虑到之后的比赛可能大部分不会支持C11标准, 因此
自己写照搬了洛谷一个题解中的字符串转变为int型的方法
原题解地址
也就是如下这段代码(可以当板子背记下来 )
int change(string s)
{
int ans=0;
for(int i=0; i<s.size(); i++)
ans = ans*10+s[i]-48;
return ans;
}
- 该函数中使用了成员函数.size(), 目前学习, 查阅到的资料说.size()和.length()没有任何区别
- 接下来要说的就与题目本身无关, 而是与评测相关的了.
首先, 本题最早提交时, 在执行了scanf()后直接执行getline(), 结果显而易见就是将换行符错误的接收了, 因此在scanf()后添加了getchar(). - 其次, 即使添加了getchar(), 洛谷的评测机也无法正确接收到输入的内容, 经过查找后了解到洛谷的评测是建立在Linux系统上的, 该系统上会将\r\n视为两个字符, 而Windows系统上会将\r自动忽视. 因此Linux上需要两个getchar(), 才能令所需的数值正确输入.
- 最后是代码健壮性(最近数据结构老师总在说这个词)的问题了. 最初主函数中接收a, b两个数字字符串时默认二中中间是以空格分隔的, 但显然洛谷评测(或者其他可能的OJ评测)的时候, 中间的字符不一定是空格.(具体是啥呢我也不知道) 所以不应该刻意判断二者中间的字符是什么来作为分隔的标志, 而是要以二者(两个数字字符串)本身作为判断的依据, 这样便不会出错了
引例三
代码实现
#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
string nums[30] ={"zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen",
"eighteen", "nineteen", "twenty", "a", "another", "first", "both", "second", "third"};
int main(void)
{
int t=6;
int a[6] = {0};
int k = 0;
while(t--)
{
string s;
cin >> s;
if(s[s.length()-1]=='.')
{
s = s.substr(0, s.length()-1);
}
for(int i=0; i<=26; i++)
{
if(s==nums[i])
{
if(i<=20)
{
a[k++] = i;
}
else if(i<=23)
{
a[k++] = 1;
}
else if(i<=25)
{
a[k++] = 2;
}
else
{
a[k++] = 3;
}
}
}
}
if(k==0)
{
printf("0");
}
else
{
for(int i=0; i<k; i++)
{
a[i] = a[i]*a[i]%100;
}
sort(a,a+k);
int flag = 0;
for(int i=0; i<k; i++)
{
if(a[i]>0 && !flag)
{
flag = 1;
printf("%d", a[i]);
}
else if(a[i]>0 && a[i]<10 && flag)
{
printf("0%d", a[i]);
}
else if(a[i]>=10 && flag)
{
printf("%d", a[i]);
}
}
}
return 0;
}
分析
重要的有两点
- 我们可以创建string类型的数组, 其初始化方式如代码所示
- 如果我们要删除string中最末尾的字符, 不应该像C一样, 将其改为’\0’ (C++中的字符串并不以\0结尾, 或者说并不全是以\0结尾). 而是应该使用如下三种方法:
①. s.substr() 该成员函数是截取s的子字符串, 若括号中有一个数字, 则是截取从该角标到结尾作为原字符串的新值; 若有两个数字a,b, 则是从a开始截取长度为b的子串.
例:
string s = "12345";
string sub1 = s.substr(3); //sub1 == "45"
string sub2 = s.substr(1,3); //sub2 == "234"
②. s.erase() 该成员函数的一种使用方法是括号中一个变量, 该变量不能只是一个数字, 而是要用s.begin加一个数字, 表示删除这个位置上的字符. 另一种使用方法是括号中两个变量a,b, 表示删除从a开始的b个长度的字符串
例:
string s = "12345";
s.erase(s.begin()+2); //s == "1245"
s.erase(1,2); //s == "15"
- 是s.pop_back(), 直接删除字符串最后一个字符, 简单粗暴, 但是突然发现是C11标准下的东西, 所以不做太多解释
引例四
详见 洛谷 P1601 A+B Problem(高精)大数加法_2
引例五
代码实现
#include<cstdio>
#include<string>
#include<iostream>
using namespace std;
string add(string str1, string str2)
{
string str;
int len1 = str1.length();
int len2 = str2.length();
if(len1<len2)
{
for(int i=1; i<=len2-len1; i++)
str1 = "0" + str1;
}
else
{
for(int i=1; i<=len1-len2; i++)
str2 = "0" + str2;
}
len1 = str1.length();
int cf=0;
int temp;
for(int i=len1-1; i>=0; i--)
{
temp = str1[i]-'0'+str2[i]-'0'+cf;
cf = temp/10;
temp %= 10;
str = char(temp+'0') + str;
}
if(cf!=0) str = char(cf+'0') + str;
return str;
}
string mul(string str1, string str2)
{
string str;
if(str1=="0"||str2=="0")
return "0";
int len1 = str1.length();
int len2 = str2.length();
string tempstr;
for(int i=len2-1; i>=0; i--)
{
tempstr = "";
int temp = str2[i]-'0';
int t = 0;
int cf = 0;
if(temp!=0)
{
for(int j=1; j<=len2-1-i; j++)
{
tempstr += "0";
}
for(int j=len1-1; j>=0; j--)
{
t = (temp*(str1[j]-'0')+cf)%10;
cf = (temp*(str1[j]-'0')+cf)/10;
tempstr = char(t+'0') + tempstr;
}
if(cf!=0) tempstr = char(cf+'0')+tempstr;
}
str = add(str, tempstr);
}
str.erase(0,str.find_first_not_of('0'));
return str;
}
int main(void)
{
string str1, str2;
cin >> str1 >> str2;
cout << mul(str1,str2) << endl;
return 0;
}
分析
与上面的A+B问题相似, 仍是使用string类代替以往的字符数组实现高精运算, 只是乘法运算中使用到了A+B, 故在此统一列出. 本题自然也是一道"模板题", 这类高精度的算法确实不需要背下来, 但是本着提升编程思维和写代码能力的目的, 还是准备打印出来仔细学习其中的思路.
另外本题中学习到了string类中的find()成员函数, 用法如下:
#include<iostream>
#include<string>
using namespace std;
int main(void)
{
string s = "abcabcabc";
cout << s.find('a') << endl; //0
cout << s.find('a', 1) << endl; //3
cout << s.rfind('a') << endl; //6
cout << s.find('d') << endl; //18446744073709551615
cout << (s.find('d') == -1) << endl; //1
cout << endl;
cout << s.find("abc") << endl; //0
cout << s.find("abc", 1) << endl; //3
cout << endl;
cout << s.find_first_of('c') << endl; //2
cout << s.find_first_of('c', 3) << endl; //5
cout << s.find_last_of('c', 3) << endl; //2
cout << s.find_first_of("jiefuc", 3) << endl; //5
cout << endl;
cout << s.find_first_not_of('c') << endl; //0
cout << s.find_first_not_of('c', 2) << endl; //3
cout << s.find_last_not_of('c', 2) << endl; //1
cout << s.find_first_not_of("isfiab") << endl; //2
return 0;
}
引例六
代码实现
#include<cstdio>
#include<string>
#include<iostream>
#include<cmath>
using namespace std;
int main(void)
{
int p1, p2, p3;
string s;
string::iterator it;
cin >> p1 >> p2 >> p3 >> s;
for (it = s.begin(); it < s.end(); it++)
{
if (*it == '-')
{
if (it - s.begin() == 0 || it - s.end() + 1 == 0)
{
continue;
}
string t;
string::iterator itt;
char begin = *(it - 1);
char end = *(it + 1);
if (begin == '-' || end == '-')
{
continue;
}
else
{
begin++;
end--;
}
if (begin - end == 1)
{
s.erase(it);
it--;
continue;
}
else if (begin > end || fabs(begin - end) > 26)
{
continue;
}
while (begin <= end)
{
t.append(p2, begin);
begin++;
}
if (p1 == 3)
{
for (itt = t.begin(); itt < t.end(); itt++)
{
*itt = '*';
}
}
else
{
if (p1 == 2 && begin >= 'a')
{
for (itt = t.begin(); itt < t.end(); itt++)
{
*itt += 'A' - 'a';
}
}
}
s.erase(it);
if (p3 == 1)
{
string::iterator temp = s.insert(string::const_iterator(it), t.begin(), t.end());
it = temp;
}
else
{
string tt(t.rbegin(), t.rend());
auto temp = s.insert(string::const_iterator(it), tt.begin(), tt.end());
it = temp;
}
it += t.size();
}
}
cout << s << endl;
return 0;
}
分析
又是一道题内题外收获都很多的题目, 虽说这道题显然有一些更简单的做法, 但毕竟是为了学新知识嘛, 在出现无数让人脱发的bug后, 请教了lxd大佬, 也通过自己的一系列查找资料, 终于是理解了各种bug的原因.
- 首先让我们隆重欢迎迭代器小哥哥! 迭代器似乎是一个类似指针的东西, 但又和指针不尽相同, 之后在学习其他容器时可以加深学习理解. 定义方法为
string::iterator (变量名);
这个东西可以通过解引用符* 来获取其当前指向位置的字符. 同时可以与s.begin(), s.end(), 之类的函数(迭代器)进行运算 - 当迭代器(下面我们用it来代表迭代器)出现在需要使用增删函数的字符串中, (可能)会发生类似于指针错位的bug. 例如当插入多个元素时, 当前所剩内存空间不足以插入这么多个元素, 就会导致系统自动将这部分数据转移到另一个可以容纳得下的位置, 而it并不会随之移动, 这就导致了迭代器失效. 虽说是可能发生, 但我们需要假设这个bug每次运行都会发生. 解决方法的话, 我们需要知道C11之前的insert()函数返回值并非迭代器类型(这就导致我的devcpp遇到这个问题后很难解决), 于是首先我们转战vscode, 在每条insert()语句前, 使用auto类型定义一个迭代器变量, 让他接受insert()的返回值, 再把这个值返还给it. 这就实现了使得迭代器不会失效.
- 如第二点中所说的auto类型, 这是C++11标准以上(大概)特有的自动类型变量, 必须进行初始化, 并会自动接受赋值号右侧的内容, 转化成同一类型. 很是方便.
- 语句
string tt(t.rbegin(), t.rend());
实现了将字符串t逆序交给字符串tt的功能. 其中的rbegin()和rend()为反向迭代器. - ps. 以后遇到迭代器相关问题就用vscode吧