zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Learning C++ No.10【STL No.2】

2023-04-22 10:58:17 时间

引言:

北京时间:2023/2/14/23:18,放假两个月,没有锻炼,今天去跑了几圈,一个字,累,感觉人都要原地升天了,所以各位小伙伴,准确的说是各位卷王,一定要坚持锻炼,不敢像我一样哦!身体才是本钱,锻炼才是王道,并且今天看了一下小黑书和计算机导论中的一些有关计算机系统的知识,发现以前通过别人把饭嚼碎给我们吃获得知识的方式,在碰到一个很陌生的知识点的时候,只能通过强硬的记忆理解去理解这个知识点,导致我们不能很好的理解这个知识点,所以虽然通过人家把饭嚼碎给我们吃,这样能够更容易上道,但是会导致不易学透;所以当自己去看书的时候,去理解书中讲解的知识点的时候,我充分发现,还是一定要自己学会吃饭,不能一直吃别人嚼碎的饭,这样才可以更好的理解某些知识点;本今天是打算自己开一个专栏,记录一下今天看到的几个有关计算机系统的,比较容易理解不全面的,当然相对于我自己来说,是和自己以前的理解有所偏差的知识点,但是由于时间问题,所以今天并没有记录,明天咱们再搞这一块。现在让我们再次进入STL的世界,开始string类的二次深入学习吧!
在这里插入图片描述

再谈string类

string类其实就是一个管理字符串的类,里面的函数就是用来对字符进行增删查改用的,用来像顺序表一样,管理一个字符数组;在上篇博客中,我们已经把string类中的一些接口给学习过了,现在我们就继续学习一下别的string类中的接口,首先是接着上篇博客,学习一下有关容量的接口。
首先我们就先来看一下容量函数中的capacity接口,观察一下它是怎么进行扩容的,如图:

我们可以发现,在vs中capacity函数大致是以1.5倍进行扩容的,而在Linux中,capacity函数大致是以2倍进行扩容的,所以我们可以得出结论,在不同的编译器上,STL库是有一定的区别的,所以导致string类中的capacity函数是有一定的区别的,所以导致扩容的倍数是有一定区别的。

讲述上述capacity函数的扩容原理,本质上我们是想要为了解string类中的reverse函数做一些铺垫的,reserve函数的作用就是可以直接进行开空间,当知道需要多少空间时,可以直接提前开空间,把空间开好之后,就是可以减少扩容,因为扩容是有消耗的,所以可以提高程序的效率。前提是知道需要多少空间哦!如下图就是reverse函数开空间的使用方法:

可以看出在编译器不同的情况下,使用reserve函数直接开空间,也是存在着一定的差异的,但是一定要区分reverse函数(逆置函数)。搞定了reserve函数,此时我们再来看一看什么是resize函数,

在这里插入图片描述

如图我们可以发现resize函数有两种用法,一种是开辟空间,和更改size的大小,一种是可以给一个字符,把剩下的size空间全部填充该字符,如上图中所示,并且此时从另一个层面去看,resize函数还具有删除数据的功能(原理:如果此时的容量比resize开的小,那么就增大空间,如果比resize开的大,那么就减小空间),此时的减小空间就可以理解为此时的删除数据,例:如图

如图可以发现,此时的size确实是变成了5,然后只剩下了hello,把后面的内容都给删除了,也可以发现capacity容量是没有改变的。所以本质上resize就是在对string类中的成员变量中指针指向的那块空间的数组进行改变而已。

浅浅摸一下迭代器

我们可以知道迭代器是STL中的一个重点知识,所以此时我们就先来浅浅的摸一下它,为以后迭代器的学习打下一定的基础。
如图就是迭代去在string中使用的一个经典代码:

正序遍历

这个就是 string类 中迭代器的基本函数:

综合上述的两幅图,可以发现,begin就是迭代器的开始位置,end就是迭代器的结束位置,上述代码中,我们使用这两个迭代器函数就可以很好的遍历一个字符串,但是要注意,在使用begin和end的时候,我们最终得到的还是该位置的地址而已,所以想要获取其中的数据,就一定需要进行解引用操作,并且目前我们可以把迭代器想象成C语言中的指针,但是以后就不行了,因为后面的迭代器会更加的复杂,我们这里只是简单的摸一下它而已, 但是在底层的代码实现,本质上迭代器肯定还是使用指针实现。并且当我们学了迭代器,我们就可以使用三种不同的方式去访问string范围for、迭代器、下标+[] ,但是 下标+[]的本质上是使用了[]运算符重载范围for本质上是使用了迭代器,足以看出迭代的重要性,STL六巨头之一不是徒有虚名的。

逆序遍历

并且可以看出,上述我们对数组的遍历是从前向后遍历的,所以该迭代器称之为正向迭代器,接下来我们就介绍一下什么是反向迭代器

注意: 因为此时是反向迭代器,所以rbegin是在rend的后面的,所以此时rbegin想要靠近rend也是要使用加加,不可以使用减减

总:正向迭代器,加加表示向前走,反向迭代器,加加表示向后走。

了解了什么是正向迭代器,什么是反向迭代器,此时我们就根据迭代器的类型再来聊一聊,普通迭代器和const修饰的迭代器, 如下图:

此时我们可以发现,无论是正向迭代器,还是反向迭代器,它们都具有两种类型,一种是普通的iterator、reverse_iterator类型,另一种是const_iterator、const_reverse_iterator类型,此时的普通类型迭代器是允许遍历和读写容器的数据的而const修饰的迭代器只允许遍历和读,并不允许写

如下代码就是这4中类型的迭代器:

#include<iostream>
#include<string>
using namespace std;

int main()
{
	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 << endl;
		++rit;
	}
	cout << endl;

	string::const_iterator it = s1.begin();//const正向迭代器
	while (rit != s1.rend())
	{
		cout << *it << endl;
		++rit;
	}
	cout << endl;

	string::const_reverse_iterator rit = s1.rbegin();//const反向迭代器
	while (rit != s1.rend())
	{
		cout << *rit << endl;
		++rit;
	}
	cout << endl;

	return 0;
}

并且此时可以发现,迭代器的类型的返回值类型是比较长的,所以此时我们就可以使用以前学的一个自动匹配类型的关键字auto ,使用auto的好处就凸显出来了,减少代码量,所以auto在有些场景中是非常的实用的,不过前提是我们自己知道这个函数的返回值类型具体是什么,这样使用auto才可以让我们更加遍历,但是auto也是有一定的缺点的,就是导致代码的可读性很低,只有了解这部分知识的人,才可以看懂代码,如下图:就是auto和迭代器的结合使用。

注意: 此时const类型的迭代器中的const修饰的是数据不能被修改,并不是位置不能被修改,所以此时rit是可以修改的,*rit是不可以修改的。

为什么要学习迭代器

很多同学可能会有疑问,我们可以使用下标和重载运算符[]来实现字符串的遍历,那么我们为什么还要学习迭代器呢?不都是为了遍历字符串吗?原因:链表和树状结构,如果使用**重载运算符operator[]**的效率是非常的低下,所以就不可以使用[]运算符和下标进行重载,此时就只可以只能使用迭代器,只有使用迭代器才可以让遍历其它非数组结构的数据结构的效率更高 ,所以C++中迭代器的概念是为了给除了数组以外的数据结构使用的。

总:迭代器作为STL中的六巨头之一,不是浪得虚名的,非常重要!

string类中的各种接口

string类中的insert函数

如图insert函数就是对字符串进行任意位置的插入字符或者字符串而已,并且也可以使用迭代器的形式就行任意位置的插入,并且insert函数的重载函数是非常多种的,可以说是满足任何的插入需求,但是注意: 因为insert每次插入字符都是以移动字符为前提的,所以在string类中,我们应该少使用insert函数,这样可以提高我们代码的效率。

string类中的erase函数

上述最后一个使用样例是属于迭代区间,目前我们就先不做了解,以后再了解,并且此时的erase函数也是和insert一样,我们并不支持经常使用,原理:经常挪动数据效率低下。

string类中的replace函数

该函数的使用是非常的不友好的,因为它不仅需要进行扩容,还需要进行字符的挪动,只有这样才可以实现该函数,所以效率是非常的低下的。

string类中的find函数

学习find函数,此时我们通过一个题目来搞定。

题目:把一句英文中的空格全部给替换成目标字符串。

代码如下:

我们使用了find函数的第一个样例,在特定的位置开始找目标字符,并且此时有一个缺省值(0),如果不是特定位置,它就会默认从0开始找目标字符,但是我们为了提高程序的效率,我们可以把这个值给成pos位置的后一个位置或者后几个位置,目的就是为了让find函数,不用每次都从头开始找,而是直接跳过我插入的特定字符,从特定字符的后一个位置去找,这样每次都是在向后遍历,极大的提高了程序的时间复杂度,并且如代码中,我们还使用了reserve函数进行开空间,前提是我们知道此时需要多少的空间,这样就可以避免扩容带来的消耗,极大的提高程序的效率。

并且此时该题还有第二种写法,一种用空间换时间的写法,但是代码的缺陷和上述代码还是差不多的,都是可以使用提前开空间(reserve)函数来进行优化的,如下代码:

#include<iostream>
#include<string>
using namespace std;

int main()
{
	string str("hello world i love you");
	string newStr;
	size_t num = 0;
	for (auto ch : str)
	{
		if (ch == ' ')
		{
			++num;;
		}
	}
	newStr.reserve(str.size() + 2 * num);//还是提前把空间开好
	for (auto ch : str)
	{
		if (ch != ' ')
		{
			newStr += ch;
		}
		else
		{
			newStr += "20%";
		}
	}

	str = newStr;
	cout << str << endl;

	return 0;
}

从这个题目,我们可以发现,在string类中是有非常多好用的功能(通过各种函数接口),使我们做题变得更加的简洁和灵活,只要你把string类中的函数给熟练的使用,是真的可以很好的利用它们去解决各种有关字符的题目。

利用string类中的函数解决问题

搞定了find函数,我们把string类中的常见的函数就给搞定的差不多了,此时我们就可以使用这些函数来做一些题目了,如下题:

给你一段有运算符和英文字母的语句,然后仅反转其中的英文字母,非英文字母保留在原有位置,所有英文字母(小写或大写)位置反转,然后返回反转后的 s
例:
输入:s = “ab - cd”
输出:“dc - ba”

并且此时通过这个题目,有一个注意点,就是if和if的使用和if和else if的使用

1.从字面上理解if为如果,就是如果这种情况,如果那种情况。
2.else if 不是上一个条件的前提下,如果是这个条件。(总:else if是在上一个条件不成立的情况下,进行判断)
3.区别1:if无论是否满足条件都会向下执行,直到程序结束,else if 满足一个条件就会停止执行。(总:else if 一但满足之后,就会停止程序)
4.区别2:由于if都会执行一遍,则可能会同一个需要判断的事件,会进入2个if语句中,出现错误,而else if就不会发生这样的事情。

弄清楚了if、if使用和if、else if的使用,此时我们就正式进入题目:
思路:

  1. 当我们想要去遍历该语句的时候,思考使用那种遍历的方式最合适,发现范围for和迭代器不怎么合适,所以使用下标+[]
  2. 实现判断是否是英文字母的函数,并且注意:前后的开始位置
  3. 前后同时遍历,找字母,是字母就停下来,然后交换,不是字母就加加到后面一个,或者减减到前面一个


#include<iostream>
#include<string>
using namespace std;

class Solution
{
public:
	bool isLetter(char ch)
	{
		if (ch >= 'a' && ch <= 'z')
		{
			return true;
		}
		if (ch >= 'A' && ch <= 'Z')
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	string reverseOnlyLetters(string s)
	{
		size_t begin = 0;
		size_t end = s.size() - 1;
		while (begin < end)
		{
			while (begin < end && !isLetter(s[begin]))
			{
				++begin;
			}
			while (begin < end && !isLetter(s[end]))
			{
				++end;
			}
			swap(s[begin], s[end]);
			begin++;
			end--;
		}

		return s;
	}
};

有了上述的思路和string类中的各种函数接口的使用,我们很愉快的就搞定了该题,所以下一题吧!!!
如题:

字符串中的第一个唯一字符,给定一个字符串s,找到它的第一个不重复字符,并返回它的索引,如果不存在,则返回-1(false)
例:
输入:s = “leetcode”
输出:1

原理:

  1. 该题第一时间,就可以想到使用计数排序的思想,使用映射的方式进行是最好的方法
  2. 按照计数排序的思想,此时第一个步骤就是统计每个字母出现的次数
  3. 判断谁是只出现一次,并且是第一个出现的(通过字符串和映射的数组中统计的次数,直接和1比较就行了)
#include<iostream>
#include<string>
using namespace std;

class Solution
{
public:
	int firstUniqChar(string s)
	{
		int countA[26] = { 0 };//将该数组初始化为0,方便映射,不需要我们自己去初始化了,以前计数排序的时候使用的是memset初始化
		for (auto ch : s)
		{
			countA[ch - 'a']++;//按照原理:下标0放a,1放b,2放c,3放d,所以此时按照这个下标理论,就可以让ch中的某个字母-掉a的ASCII码值,这样就可以在相应的下标位置进行++了
		}

		for (int i = 0; i < s.size(); ++i)//此时这个遍历(因为要知道下标,所以不适合使用范围for和迭代器,使用下标是最合适的)
		{
			//总:遍历,一定要灵活选择
			if (countA[s[i] - 'a'] == 1)
			{
				return i;
			}
		}
	}
};

通过上题,我们可以发现计数排序的好处,但是也可以发现,我们有时候,很难去使用,特别是countA[ch - 'a']++; 统计每一个字符出现的次数的这步,有时候需要我们仔细斟酌,还有就是这步 if (countA[s[i] - 'a'] == 1),让字符串中的字符从前向后去和统计出的出现次数进行比较,看谁是第一个出现一次的,这步也是需要我们细细斟酌,所以这两步一定要熟练的使用,这样才可以让我们的代码更加的优,并且思路更加的清晰,做题更加的轻松。

总:计数排序的思想是非常的好用的,并且string的使用场景(遍历)大部分都是:下标+[]配合使用的。

在这里插入图片描述

总:搞定了string类中常用函数和如何使用,不仅可以为以后STL的学习提供便利,而且可以让我们做题变得更加的轻松。