前言
上节课我们简单的了解了一下模板,本节我们来学习一下C++标准库的string类并体会string类的作用效果,那么废话不多说,我们正式进入今天的学习。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. 学习string类的意义
我们在日常生活中有各种各样的信息,这些信息有些需要用整型存储,有些需要用浮点型存储……但是还有一些信息必须要使用字符串进行存储,例如:身份证号码、家庭住址、电话号码……所以单独创建一个管理字符串的数据结构类型就会比较方便
(string类的产生早于STL(数据结构与算法),底层是模板|,但根据功能来看,string类与STL有着极大的共同点。不较真的话,将string类归类于STL也是没有问题的)
由于编码的原因(先不做解释,只需要了解即可),string其实是被typedef出来的,它原生的类型应该是basic_string<char>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
除了char还有其他的类型,但是不是很常用:
它们的使用方法都是类似的,它们有这种明确的分类都是与编码有关系
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2. string类
使用string类需要包含头文件<string>
2.2 string类的常用接口
**************************************************************************************************************
1.string类对象的常见构造
(第七个函数涉及迭代器,暂时不作讲解,以后会讲解)
**************************************************************************************************************
我们来看一下主要的成员函数以及它的功能:
函数名称 | 函数功能 |
string( ) | 构造空的string类对象,即空字符串 |
string(const char* s) | 用C-string来构造string类对象 |
string(const string&s) | 拷贝构造函数 |
int main(void)
{
string s1;//构造空的string类对象s1
string s2("abc123");//用C格式字符串构造string类对象s2
string s3(s2);//拷贝构造
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
return 0;
}
(注意:string重载了流插入和流提取,所以我们可以直接使用cout来打印相关字符串)
我们顺便再来看看其他几个成员函数:
函数名称 | 函数功能 |
string (const string& str, size_t pos, size_t len = npos) | 从pos位置开始,拷贝len个字符 |
string(const char* s, size_t n) | 取某个字符串的前n个数据 |
string(size_t n, char c) | 用n个c字符来初始化 |
**************************************************************************************************************
1.string (const string& str, size_t pos, size_t len = npos)
int main(void)
{
string s1("hello world");
string s2(s1, 6, 5);
cout << s2 << endl;
return 0;
}
假设我们这里的“5”个字符写成了“15”个字符呢?此时会不会报错呢?我们来验证一下:
我们通过看文档知道:如果len的长度小于pos位置到字符串末尾的位置时,就会拷贝len个字符;如果len的长度大于pos位置到字符串末尾的位置时,或者传npos和不传len的时候就会有多少字符拷贝多少字符。
不传len的情况:
int main(void)
{
string s1("hello world");
string s2(s1, 6);
cout << s2 << endl;
return 0;
}
npos的情况:
如图所示,npos的值是-1。但其实它的真实值不是-1。因为-1在这里存入的是补码,-1的补码全是1(二进制),可以看到,它的类型是size_t,size_t是unsigned int类型的,所以此时它表示的是整型的最大值。所以这里它想表达的意思是有多长取多长。
**************************************************************************************************************
2.string (const char* s, size_t n)
这个函数的功能是取字符串中的前n个数据
int main(void)
{
string s2("hello world", 5);
cout << s2 << endl;
return 0;
}
**************************************************************************************************************
3.string(size_t n, char c)
int main(void)
{
string s1(10, 'A');
cout << s1 << endl;
return 0;
}
string底层是一个动态开辟的数组,它是一个类,所以它还有析构函数,程序结束时会自动调用:
**************************************************************************************************************
2. string类对象的容量操作
函数名称 | 函数功能 |
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty | 检测字符串释放为空串,是返回true,否则返回false |
clear | 清空有效字符 |
reserve | 为字符串预留空间 |
resize | 将有效字符的个数改成n个,多出的空间用字符c填充 |
max_size | 说明string最大能开多少个字节 |
shrink_to_fit | 用于缩小容量 |
**************************************************************************************************************
1. size
size函数可以用来返回字符串中的有效字符的长度:
int main(void)
{
string s1("hello world");
cout << s1.size() << endl;
return 0;
}
**************************************************************************************************************
2.length
length与size的功能一模一样,而且它们在底层的实现也几乎相同,二者没有本质的区别,那么为什么还有设置两个同样功能的函数呢?
根据在互联网上查询相关资料可以知道:
length是沿用C语言的习惯而保留下来的,string类最初只有length。在引入STL之后,为了兼容才有了size,它是作为STL容器的属性存在的,以便用于STL的算法
因为需要向前兼容的原因,C++中length的语法就没有删除
**************************************************************************************************************
3.max_size
这个函数的实际使用意义不大,它可以说明string最大可以开辟的字符串有多少个字节(下图为32位下的,64位下的更大):
int main(void)
{
string s1("hello world");
cout << s1.max_size() << endl;
return 0;
}
**************************************************************************************************************
4.capacity
capacity表示的是容量,与数据结构中的capacity一样
int main(void)
{
string s1("hello world");
cout << s1.capacity() << endl;
return 0;
}
**************************************************************************************************************
我们下面来看一组代码:
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << s.capacity() << endl;
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
int main(void)
{
TestPushBack();
return 0;
}
这段代码向我们展示扩容的过程是如何进行的。我们先用sz变量来保存之前的容量,如果我们pushback了以后capacity变化了跟之前的容量不一致那我们此时就打印之前的容量,这样我们就能够观察到capacity在扩容的时候的变化过程:
我们发现这里capacity的变化规律并不是很明显。在第一次扩容的时候,capacity直接扩容了两倍,因为它本身的空间应该是16和32,二者都给了1个空间给“\0”。再往后扩容就是1.5倍扩容
为什么会采取这样的扩容方式呢?我们首先要来了解以下string内部的结构,它的底层代码大致可以用下面的代码来理解:
private:
char _buff[16];
char* _str;
size_t _size;
size_t _capacity;
};
string刚开始存入数据的时候并不是直接把数据存入到堆上,当数据量较小的时候它会把数据存入到_buff[16]变量中。这就意味着string类中使用char _buff[16] 和 char* _str 这两个变量来存储数据的。当小于16的时候数据存入_bbuff[16],当数据大于16的时候数据存入_str指向的空间中此时_buff[16]就会被废弃,就不会存数据了。当数据大于16capacity就会扩容2倍,然后开辟一个新的空间在堆上来存储数据,我们可以来验证一下:
int main(void)
{
string s1("hello world");
string s2("abcdefghijkmlnopqrstuvwxyz");
return 0;
}
我们在VS调试中打开监视窗口,监视窗口中有一个原始视图的选项,我们把它打开:
s1中的_Buf就是_buff[16]中存储的数据,因为字符串"hello world"只有11个字节,所以_Ptr中没有任何数据
而我们再来看一下s2中的原始视图:
因为此时的字符串长度大于16,所以数据内容都存到了堆上,此时_Buf中没有任何的数据
不同环境下的扩容方式也是不同的,下图是G++的扩容:
**************************************************************************************************************
**************************************************************************************************************
5.reserve
reserve函数需要传入参数n,此时就会提前开辟好容量为n的空间
int main(void)
{
string s;
s.reserve(100);
size_t sz = s.capacity();
cout << s.capacity() << endl;
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
return 0;
}
在上述代码中,我们选择开辟大小为100的空间,但实际上空间的大小一般会要大于100.
因为这里开辟的空间足够,所以就不需要扩容了
我们现在看到上面的函数说明的最后一句英文,上面英文的意思是reverse函数并不会影响数据的内容,也就是size的大小。那么遇到下面这几种情况编译器会如何处理呢?
我们先来看一下capacity的大小:
int main(void)
{
string s1("0123456789");
cout << s1.capacity() << endl;
return 0;
}
1.假设size = 10 ,capacity = 15 ,n < 10
int main(void)
{
string s1("0123456789");
s1.reserve(5);
cout << s1.capacity() << endl;
cout << s1 << endl;
return 0;
}
如果n小于size,此时可以看到reserve函数并没有改变空间的大小,所以我们可以知道当n小于size的时候不会采取任何操作
2.假设size = 10 ,capacity = 15 ,10 < n < 15
int main(void)
{
string s1("0123456789");
s1.reserve(13);
cout << s1.capacity() << endl;
cout << s1 << endl;
return 0;
}
此时n大于size,但是n小于capacity,所以capacity保持不变
3.假设size = 10 ,capacity = 15 ,n > 15
int main(void)
{
string s1("0123456789");
s1.reserve(25);
cout << s1.capacity() << endl;
cout << s1 << endl;
return 0;
}
此时情况和2中的一样,capacity也被扩容了
**************************************************************************************************************
6.clear函数
clear函数是用来清除数据的,具体使用方法如下:
int main(void)
{
string s1("0123456789");
s1.clear();
cout << s1 << endl;
return 0;
}
clear函数仅清除数据,不会清除容量:
int main(void)
{
string s1("0123456789");
cout << s1.capacity() << endl;
s1.clear();
cout << s1 << endl;
cout << s1.capacity() << endl;
return 0;
}
clear函数使用的非常少,仅作了解即可
**************************************************************************************************************
7.shrink_to_fit函数
该函数可以请求将字符串的容量减小,且此操作对字符串中的数据没有影响:
int main(void)
{
string s1("0123456789");
s1.reserve(100);
cout << s1.capacity() << endl;
s1.shrink_to_fit();
cout << s1.capacity() << endl;
return 0;
}
**************************************************************************************************************
3.string类对象的访问及遍历操作
函数名称 | 函数功能 |
operator[] | 返回pos位置的字符,const string类对象调用 |
begin和end | begin获取第一个字符的迭代器 + end获取最后一个字符下一 个位置的迭代器 |
rbegin和rend | begin获取第一个字符的迭代器 + end获取最后一个字符下一 个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
**************************************************************************************************************
string类中重载了一个非常有用的运算符,叫做operator[],我们可以这样来理解它的底层代码:
class string
{
public:
char& operator[](size_t i)
{
return _str[i];
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
operator[]可以返回某个字符的引用。返回这个字符的引用目的不是为了减少拷贝,而是为了可以修改这个字符:
int main(void)
{
string s1("hello world");
s1[5] = '*';
cout << s1 << endl;
return 0;
}
这样做就可以像修改数组一样修改某个字符串,使用起来就会非常的方便
而且如果我们越界访问了,它还能够帮助我们检查出来:
int main(void)
{
string s1("hello world");
s1[15] = '*';
cout << s1 << endl;
return 0;
}
因为这里的代码会转变为operator[]的调用,在调用的过程中直接就加入了断言:
assert(i < _size);
如果i大于_size编译器就会直接报错0
**************************************************************************************************************
假设我们需要遍历一个字符串的话,应该要怎么处理呢?
我们通常有三种方式来遍历字符串:
1. 使用size函数 + 下标[ ]
我们刚才学习了string类中有一个接口叫做size,size函数可以获取并且返回当前这个字符串中字符的数量
int main(void)
{
string s1("hello world");
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
return 0;
}
**************************************************************************************************************
2.使用迭代器
迭代器是STL六大组件之中的一大组件,迭代器的作用是用来遍历和访问容器的
int main(void)
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
我们可以发现,这个东西有点像指针,string的底层有下面三个成员:
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
};
_str是一个字符指针,指向的是字符串——“hello world”
任何容器的迭代器都规定:不管它底层是怎么实现的,它都是属于对应的容器的内域的
string::iterator
vector<int>::iterator
我们使用迭代器定义了一个对象it,这个对象就像一个指针(可能是一个指针,但是不一定是指针),因为它的使用方法与指针几乎一样。
迭代器还可以进行加减的操作:
int main(void)
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
*it += 2;
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
当我们进行加减操作以后,s1也会改变:
int main(void)
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
*it += 2;
cout << *it << " ";
++it;
}
cout << endl;
cout << s1 << endl;
return 0;
}
begin()是用来返回开始位置的迭代器;
end()是用来返回最后一个有效数据的下一个位置的数据(一般是\0);
*it有可能是解引用(it为指针),也有可能是运算符重载(it不为指针);
迭代器的优势之处在于:迭代器提供了一种通用的访问容器的方式,所有的容器都可以使用迭代器来访问
链表也可以通过迭代器来访问,因为迭代器是一种通用的访问形式:(需要包含头文件<list>)
int main(void)
{
list<int> it = { 1,2,3,4,5,6,7 };//初始化链表
list<int>::iterator list = it.begin();
while (list != it.end())
{
cout << *list << " ";
++list;
}
cout << endl;
return 0;
}
因为所有的容器都可以使用迭代器来访问,我们需要重点掌握迭代器的使用方法
假设是const修饰的字符串,我们就不能直接使用迭代器了,因为迭代器是可读可写的。此时我们就要介绍到const迭代器了,const迭代器是只能读不能修改的
我们在使用string类的成员函数时,如果是普通的对象返回的是普通迭代器,如果是const对象返回的是const迭代器
我们需要注意:const修饰的迭代器表述形式应该是:
string::const_iterator it = s1.begin();
这样表示的意思是,它本身可以被修改,但是它指向的内容不能被修改,而不是下面的这种表述形式:
const string::iterator it = s1.begin();
这样写的话,它本身也不能被修改了,就不能进行++的操作来遍历字符串了
因为有时候begin和end返回的是普通的迭代器,后时候返回的又是const迭代器,这样就有点不清晰,所以C++中又创造出了cbegin和crbegin的语法:
这里使用begin和cbegin的区别不大,可以根据自己的偏好来选择
**************************************************************************************************************
刚才我们说的迭代器是正向迭代器,这里我再来了解一下反向迭代器,反向迭代器是倒着遍历的:
int main(void)
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
return 0;
}
注意反向迭代器这里不是--,应该还是继续写成++
**************************************************************************************************************
**************************************************************************************************************
所以综上所述,迭代器一共有四种类型:
1.iterator
2.const_iterator
3.reverse_iterator
4.const_reverse_iterator
**************************************************************************************************************
3.使用auto和范围for
我们先来讲解一下auto关键字:
auto关键字是C++11中的小语法,可以自动推导变量的类型。且auto声明的变量会在编译器的编译阶段实现推导
我们在使用auto时应该注意以下几点注意事项:
1.使用auto来声明指针的类型时,使用auto和auto*没有任何的区别,但是用auto声明引用类型的时候必须加上&
2.当同一行声明多个变量的时候,声明的变量的类型必须都相同,否则编译器就会报错,因为在这种情况下,编译器只会对第一个参数的类型进行推导,然后用推导出来的类型去定义其他的变量
3.auto不能用来作为函数的参数,但是可以用来做返回值,需要谨慎使用(如果是多个函数内部连续调用,那么推返回值的类型会很麻烦,但是不存在语法方面的问题)
4.auto不能直接用来声明数组
auto在一般情况下是用来简化代码的,我们来看看auto的优越之处:
map<string, string> dict;
map<string, string>::iterator mit = dict.begin();
auto mit = dict.begin();
我们先来看一下这段代码,不清楚这段代码具体的意义也没有关系。mit的类型是:
map<string, string>::iterator
如果我们每次使用到mit时,如果都要手动的去写mit的类型就会很麻烦,但是我们有了auto关键字就会很轻松
auto也有一点缺陷:auto关键字在某种意义上来说牺牲了代码的可读性
再来介绍一个小语法:typeid( ).name( ) 。这个关键字是用来打印变量的类型的:
int main(void)
{
int a = 2;
auto b = &a;
auto* c = &b;
auto& d = a;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
范围for也适用于所有容器,我们先来看看具体的用例:
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
这串代码的意思是:编译器自动从容器中(也就是s1中)取出它的每一个字符,并且把这个字符给ch变量。变量的类型是auto,auto类型可以自动推导出ch的类型,如果明确了ch变量的类型,也可以不写auto。
范围for可以实现:自动赋值、自动迭代、自动判断结束。
范围for在底层的实现原理和刚才所说的迭代器的原理基本相同,在我们写完范围for的代码以后,编译器会在底层将它转换为迭代器的代码。迭代器与范围for的关系就像指针与引用的关系一样,二者在底层都是同一样东西,但是在使用上存在着一些差异
范围for也支持加减操作:
int main(void)
{
string s1("hello world");
for (auto ch : s1)
{
ch -= 2;
cout << ch << " ";
}
cout << endl;
return 0;
}
但此时我们要注意一点,使用范围for进行加减操作时,s1并不会改变;但是如果是迭代器进行加减操作的话s1就会改变:
int main(void)
{
string s1("hello world");
for (auto ch : s1)
{
ch -= 2;
cout << ch << " ";
}
cout << endl;
cout << s1;
cout << endl;
return 0;
}
如果我们想要用范围for修改的话,我们就得加上引用&:
int main(void)
{
string s1("hello world");
for (auto& ch : s1)
{
ch -= 2;
cout << ch << " ";
}
cout << endl;
cout << s1 << endl;;
return 0;
}
**************************************************************************************************************
对于string类而言,这三种遍历方式没有区别,不会有性能上的差异,我们在写代码中选择自己喜欢的形式就行
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3.练习题
我们学习了一部分string类的内容,已经可以开始尝试解题了,下面我们来看几道题:
**************************************************************************************************************
1.仅仅反转字母
这个题目肯定是需要遍历的,我们可以直接排除掉使用范围for,因为范围for只能够正向遍历而不能反向遍历
思路一
我们首先就可能会想到创建一个正向迭代器it和一个反向迭代器rit,然后it从前往后遍历,让rit从后往前遍历,然后让it的字符==rit的字符。
但是这个思路我们仔细思考一下其实是非常麻烦的,因为我们无法判断何时结束遍历(不能以两个迭代器相遇时字符相等为结束的条件,因为在遍历的过程中可能会有相同的字符),正向迭代器和反向迭代器因为类型不同的原因是无法比较的
思路二
我们可以使用"下标+[]"来解决问题,同时从前向后遍历和从后向前遍历并且赋值,当相遇时停止
首先我们需要写一个函数用来判断是否为字母:
bool isletter(char ch)
{
if (ch >= 'a' && ch <= 'z')
return true;
if (ch >= 'A' && ch <= 'Z')
return true;
return false;
}
其次我们创建变量left指向字符串的第一个字符,再创建一个变量right指向字符串的最后一个字符:
int left = 0;
int right = s.size() - 1;
接下来我们来确定一下结束的条件:当 left < right 的时候,就会结束赋值:
while (left < right)
我们每次进入循环的时候都需要判断right和left变量是不是字母,如果left不是字母就++,right不是字母就--:
while (!isletter(s[left]))
{
++left;
}
while (!isletter(s[right]))
{
--right;
}
结束完判断以后我们就可以让right和left交换,并且让right--和left++“
swap(s[left++], s[right--]);
我们写到这里还需要考虑一下越界访问的问题,如果需要交换的字符串中没有字母的话,right就会一直--,left就会一直++,这样就会造成越界访问,所以我们此时就要修改一下刚才的判断函数:
while (left < right && !isletter(s[left]))
{
++left;
}
while (left < right && !isletter(s[right]))
{
--right;
}
到这里我们就可以整合代码了”
class Solution
{
public:
bool isletter(char ch)
{
if (ch >= 'a' && ch <= 'z')
return true;
if (ch >= 'A' && ch <= 'Z')
return true;
return false;
}
string reverseOnlyLetters(string s)
{
int left = 0;
int right = s.size() - 1;
while (left < right)
{
while (left < right && !isletter(s[left]))
{
++left;
}
while (left < right && !isletter(s[right]))
{
--right;
}
swap(s[left++], s[right--]);
}
return s;
}
};
我们试着在leetcode中提交:
**************************************************************************************************************
2.找出字符串中的第一个唯一的字符
题中所说的“返回索引”,也就是返回下标
思路一
拿示例1为例,我们可以直接找到它的第一个字符l,然后用l和后面字符串中的所有字符相比较。如果后面的字符中含有l就继续向后遍历,看字符e是否唯一;如果后面的字符中不含有l就返回l字符的下标
这个思路的优点是容易想到,但是缺点是它的时间复杂度是O(N^2)
思路二
我们可以采用计数排序的思路。也就是说我们可以统计每个字符出现的次数
我们首先需要统计每个字母出现的次数:
int count[26] = { 0 };
//统计次数
for (auto ch : s)
{
count[ch - 'a']++;
}
统计到这里,可能会存在很多个只出现过一次的字母,现在我们就需要想办法找到第一个出现的唯一字母:
for (size_t i = 0; i < s.size(); i++)
{
if (count[s[i] - 'a'] == 1)
return i;
}
return -1;
现在我们来整合一下代码:
class Solution
{
public:
int firstUniqChar(string s)
{
int count[26] = { 0 };
//统计次数
for (auto ch : s)
{
count[ch - 'a']++;
}
for (size_t i = 0; i < s.size(); i++)
{
if (count[s[i] - 'a'] == 1)
return i;
}
return -1;
}
};
**************************************************************************************************************
3.字符串相加
在科学计算中,直接用整型计算可能会超过整型的最大值,所以这种情况下我们通常会使用字符串相加,这种运算模式就叫做大数运算
我们首先来了解两个函数:stoi函数和to_string函数
stoi函数可以把字符串转成整型
to_string函数可以把任意类型的数据转变成字符串
我们以示例2中的"456"和"77"为例:
首先,字符串中的字符需要从后往前取,因为如果我们是从其那往后取的话取出的是高位,我们需要从低位开始取
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
随后,我们需要把取出来的字符转变为对应的整型,这个过程只需要减去字符0就行了
int val1 = end1 >= 0 ? num1[end1] - '0' : 0;
int val2 = end1 >= 0 ? num2[end2] - '0' : 0;
我们还需要知道有一个进位的概念,我们创建一个变量next来表示进位的概念:
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1] - '0' : 0;
int val2 = end1 >= 0 ? num2[end2] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
}
此时我们需要考虑到一个情况:如果 end1 = 1 和 end2 = 9 相加,此时就已经结束了,结果此时为0,所以我们要加上进位1:
接下来就只需要把字符串存入就行了,先创建一个字符串str,每循环一次,就把计算出来的结果头插进字符串中。此时我们需要了解到一个函数叫做insert
我们此时就可以整合代码了:
class Solution
{
public:
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
int next = 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 + next;
next = ret / 10;
ret = ret % 10;
str.insert(str.begin(), '0' + ret);
}
if (next == 1)
str.insert(str.begin(), '1');
return str;
}
};
**************************************************************************************************************
**************************************************************************************************************
因为头插每次都要把之前插入的数据往后挪动一位,所以空间复杂度还是会比较高,我们可以适当的修改一下代码,让代码更加高效,也就是使用尾插(还没有学习过,先提前了解):
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 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
尾插完以后,数据是反过来的,我们需要把数据逆置一下:
reverse(str.begin(), str.end());
我们来整合一下代码:
class Solution
{
public:
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
int next = 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 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
};
采用这种写法以后,效率会更加的高,空间复杂度为O(N)(具体使用方式以后会详细讲解)
**************************************************************************************************************
结尾
string类的内容较多,在日常的编程过程中使用也很多,所以后面几节将会继续补充string类的内容,那么本节的内容就到此结束了,谢谢您的浏览!!!