相信大家都知道STL在C++中的重要性,作为其模板库中的一部分,包含了常见的数据结构和算法,是C++的标准库
而我们今天要讲的String类(String底层是一个字符顺序数组的顺序表对象,可以归类为容器),其实其诞生于STL之前,并不属于STL,但是你却可以从中看到属于STL的一角,相信学了String类之后,在后面学习STL时,vector等类对大家来说一定手到擒来
为什么学习String类
C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
在OJ中,有关字符串的题目一般也是由String类形式出现,很少有人去用C库中的字符串操作函数
标准库中的String类
String类基本了解
- 字符串是表示字符序列的类
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结: - string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
- 不能操作多字节或者变长字符的序列。在使用string类时,必须包含#include头文件以及using namespace std;
String类常用接口说明
1. String类对象常用构造
我们分别在编译器中使用这些函数试试
相信通过这个演示,大家已经知道string类的构造函数是如何使用的了,不过我们一般不使用第三种,其的重要性不如其他三个
2. string类容量操作
注意:
1.size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
2 clear()只是将string中有效字符清空,不改变底层空间大小。也想释放空间的话,要用shrink_to_size,其叫缩容
3 resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用\0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。开一段空间再初始化一下,就可以用resize
4 reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。并且你reserve多少不一定就给你开多少空间,一般是>=的关系
接下来也是用代码来举例子
3. string类对象的访问及遍历操作
代码演示:
需要注意的是,operator[]是重载运算符,后面我们在自行实现string类时,可以仔细观察
而且operator[]这个模拟的是数组的行为,只是看上去与数组一样,比如其还可以写成s1.operator,其实就是一个函数;并且它除了可以获取pos位置的值,也可以修改pos位置的值,可读可写
at跟[]很像,区别在于越界的检查,[]不会对其进行检查,但是at会,所以用at比用[]更加安全,而front指向第一个位置,back指向最后一个元素的位置
其也有两个版本,一个可读可写,就是上面的;还有一个加了const修饰符只可以读的版本,它们俩构成函数重载
在此我们也补充一下迭代器(iterator)的知识,迭代器是一个类型,定义在string的类里面。迭代器的用法像指针(但是它不能直接理解为指针,只是行为像而已,虽然很多编译器底层就是指针),有一个iterator(正向迭代器)和reverse_iterator(反向迭代器),iterator使用string中的begin()和end(),而reverse_iterator使用rbegin()和rend()来遍历字符串:begin指向字符串的第一个位置,而end指向的是最后一个字符的下一个位置,左闭右开,所以我们遍历时才写!=end();并且其也可以直接修改字符串的值,不过也有另外const只可以读的形式,就比如cbegin,于C++11加入,不过用的并不多
迭代器才是我们之后使用的主流,operator[]虽然好用,但是在别的类型中是不好使用的,比如链表,是使用不了[]的,但是迭代器都可以实现;迭代器屏蔽了底层的实现细节,是哪个都可以用它遍历,其是一种通用的遍历方法
我们上面还使用了范围for来遍历,这里我们也给大家再补充一下,其也是可以遍历容器的,而这里我们也要插一嘴,其底层其实就是迭代器;这些都是正向遍历,如果要反向迭代,就要用到我们的反向迭代器
string s1("hello");
for (auto e : s1) {
cout << e << endl;
}
4. string类对象的修改操作
代码例子:
注意:
- 在string尾部追加字符时,s.push_back( c ) / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。(所以实际意思就是+=好用)
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
- npos作为一个特殊值,其类型为unsigned int,其实就是size_t,所以给-1反而让其变成整形的最大值,这就是为什么缺省值给npos能够直接到达字符串末尾的原因
- cstring与string虽然都有\0,但是string 的size中是不包含\0的大小的,这只是为了兼容C
- 这里补充一个replace:{string& replace (size_t pos, size_t len, const string& str);}
举个例子大家看的更懂,可以替换指定位置的内容
这里的replace(found,1,“_”),意思就是找到found位置的后一个元素,把其替换为 _,如果我们改成2,就是后面两个元素,以此类推(这里应该是found+1奥)
总结一下:insert,erase,replace都少用,基本都需要挪动数据,效率不高
5.string类非成员函数
一般来说,我们知道非成员函数就是没有写在类内部的函数,所以像重载函数一些的函数都需要写在外部,不然的话会出现无法改变函数的参数顺序的情况,毕竟在我们先前写日期类时就出现过这些问题
关系操作(relational operators):有==(等于) !=(不等于) <(小于) <=(小于等于) >(大于) >=(大于等于)
上面的几个接口大家了解一下,下面的OJ题目中会有一些体现他们的使用。string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。
代码展示:
6.其他函数
不知道大家还记不记得strtok,我们使用一下让大家回忆一下:
最后输出的结果是hello world how are you
不过strtok不是重点,我们由其引出find_frist_of
我们查找子串中的任意一个字符,并把其替换成*
除了这个,还有find_last_of,就是倒着找,还有find_first_not_of和find_last_not_of
还有一些转换形式的,如stoi,stod,stol,分别可以把字符串转换为int,double和long
这些函数的作用与to_string是恰好相反的
牛刀小试
正确答案
class Solution {
public:
string reverseOnlyLetters(string s) {
int left = 0;
int right = s.size() - 1;
while (left < right) {
if (!isalpha(s[left])) {
left++;
} else if (!isalpha(s[right])) {
right--;
} else {
swap(s[left], s[right]);
left++;
right--;
}
}
return s;
}
};
容易错误答案
class Solution {
public:
string reverseOnlyLetters(string s) {
int begin = 0;
int end = s.size() - 1;
while (begin < end) {
if (!isalpha(s[begin]))
begin++;
if (!isalpha(s[end]))
end--;
swap(s[begin], s[end]);
begin++;
end--;
}
return s;
}
};
这里我们拿一个错误用例:7_28,本来由于其都是非英文字母,所以其实本来应该直接循环结束也不会更改任何一个位置,但是我们只考虑了可能会有1次非英文字母,导致了在检查了一次之后就直接交换,产生错误,最后的结果直接错误,而如果用if else一句,一次循环只有一次判断,虽然会麻烦一点,但是不会出现这样的错误。也有人会说,可以用while语句,但是用while语句的话,有时会超出时间,所以我们这里也不推荐,if else语句是我们比较建议的一种写法
正确答案
class Solution {
public:
int firstUniqChar(string s) {
int *a = (int*)calloc(sizeof(int), 256);
for (int i = 0; i < s.size(); i++) {
a[s[i]]++;
}
for (int i = 0; i < s.size(); i++) {
if (a[s[i]] == 1) {
free(a);
return i;
}
}
free(a);
return -1;
}
};
正确答案
#include <iostream>
using namespace std;
int main() {
string s;
getline(cin,s);
if(s.empty())
cout<<0<<endl;//如果是空串,直接输出0
size_t rfound=s.rfind(' ');//找到最后一个空格
if(rfound){
cout<<s.size()-1-rfound<<endl;//如果找到了,就用中间段相减得出结果
}
else{
cout<<s.size()<<endl;//没有空格,现在的就是最后一个单词,也就是现在的单词长度就是字符串最后一个单词
}
}// 64 位输出请用 printf("%lld")
正确答案
class Solution {
public:
bool isPalindrome(string s) {
string str;
str.reserve(s.size()+1);//提前reserve,只是为了后面不用边加边扩容
for(int i=0;i<s.size();i++){
if(isalpha(s[i])){
str+=tolower(s[i]);//如果是字母,直接尾插小写的字母,不管它小写的还是大写的
}
if(isdigit(s[i])){
str+=s[i];//字母就直接尾插
}
}
string str_begin=str;//保存反转前的str
reverse(str.begin(),str.end());
string rstr=str;//保存反转后的str
if(rstr==str_begin){
return true;
}
else{
return false;
}
}
};
正确答案
#include <algorithm>
#include <string>
class Solution {
public:
std::string addStrings(std::string num1, std::string num2) {
if (num1 == "0")
return num2;
if (num2 == "0")
return num1;//完美解决了0的三种情况
std::string str;//用来存储相加后的字符串
std::string::reverse_iterator rit1 = num1.rbegin();
std::string::reverse_iterator rit2 = num2.rbegin();//从最后一位开始相加,其实就是做竖式的过程
int sum = 0;//当位数
int carry = 0;//进位
while (rit1 != num1.rend() && rit2 != num2.rend()) {
sum = (*rit1 - '0') + (*rit2 - '0') + carry;
carry = sum / 10;
str += (sum % 10) + '0';
rit1++;
rit2++;
}
//下面是防止两个一个长一个短的情况
while (rit1 != num1.rend()) {
sum = (*rit1 - '0') + carry;
carry = sum / 10;
str += (sum % 10) + '0';
rit1++;
}
while (rit2 != num2.rend()) {
sum = (*rit2 - '0') + carry;
carry = sum / 10;
str += (sum % 10) + '0';
rit2++;
}
//如果最后加的那一下又有进位,不能忘掉
if (carry > 0) {
str += carry + '0';
}
std::reverse(str.begin(), str.end());//因为我们用+=,是尾插不是头插,所以最后要反转过来
return str;
}
};
这道题我们要注意一个地方,那就是我们是在字符串中加减,所以我们不可以直接用字符加减,而是要用其减去’0‘得到其原来的值
上面的是博主自己做的(迭代器),可能看上去很复杂麻烦,下面是力扣的标准答案,大家看一下应该会觉得很简洁
class Solution {
public:
string addStrings(string num1, string num2) {
int i = num1.length() - 1, j = num2.length() - 1, add = 0;
string ans = "";
while (i >= 0 || j >= 0 || add != 0) {
int x = i >= 0 ? num1[i] - '0' : 0;
int y = j >= 0 ? num2[j] - '0' : 0;
//这里避免了两个位数不同的问题
int result = x + y + add;
ans.push_back('0' + result % 10);
add = result / 10;
i--;
j--;
}
// 计算完以后的答案需要翻转过来
reverse(ans.begin(), ans.end());
return ans;
}
};