string类(1)

前言

上节课我们简单的了解了一下模板,本节我们来学习一下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和endbegin获取第一个字符的迭代器 + end获取最后一个字符下一                                   个位置的迭代器
rbegin和rendbegin获取第一个字符的迭代器 + 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类中有一个接口叫做sizesize函数可以获取并且返回当前这个字符串中字符的数量

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类的内容,那么本节的内容就到此结束了,谢谢您的浏览!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值