《算法笔记》——第六章 STL 学习记录

C++为使用者提供了标准模板库( Standard Template Library,STL),其中封装了很多相当实用的容器(读者可以先把容器理解成能够实现很多功能的东西),不需要费力去实现它们的细节而直接调用函数来实现很多功能,十分方便。

vector

vector翻译为向量,但是这里使用“变长数组”的叫法更容易理解,也即“长度根据需要而自动改变的数组”。在考试题中,有时会碰到只用普通数组会超内存的情况,这种情况使用vector 会让问题的解决便捷许多。另外,vector还可以用来以邻接表的方式储存图,这对无法使用邻接矩阵的题目(结点数太多)、又害怕使用指针实现邻接表的读者是非常友好的,写法也非常简洁。

vector一般有两种访问方式:通过下标访问或通过迭代器访问。

迭代器(iterator)可以理解为一种类似指针的东西。

vector<typename>::iterator it;

这样it就是一个vector<typename::iterator型的变量(虽然这个类型看起来很长),其中typename就是定义vector时填写的类型。这样就得到了迭代器it,并且可以通过*it来访问vector里的元素。

vi[i]和*(vi.begin()+ i)是等价的。
既然上面说到了begin()函数的作用为取vi的首元素地址,那么这里还要提到end()函数。和begin()不同的是,end()并不是取vi的尾元素地址,而是取尾元素地址的下一个地址。end()作为迭代器末尾标志,不储存任何元素。

除此之外,迭代器还实现了两种自加操作: ++it 和it++ (自减操作同理)。

最后需要指出,在常用STL容器中,只有在vector和string中,才允许使用vi.begin()+3这种迭代器加上整数的写法。

vector常用函数解析

(1) push_back()
顾名思义,push_back(x)就是在vector后面添加一个元素x,时间复杂度为\(O(1)\)
(2) pop_back()
有添加就会有删除,pop_back()用以删除vector的尾元素,时间复杂度为\(O(1)\)
(3) size0
size()用来获得vector中元素的个数,时间复杂度为\(O(1)\)。size()返回的是unsigned类型,不过一般来说用%d不会出很大问题,这一点对所有STL容器都是一样的。
(4) clear()
clear()用来清空vector中的所有元素,时间复杂度为\(O(N)\), 其中N为vector中元素的个数。
(5) insert0
insert(it, x)用来向vector的任意迭代器it处插入一个元素x,时间复杂度\(O(N)\)
(6) erase()
erase()有两种用法:删除单个元素、删除一个区间内的所有元素。时间复杂度均为\(O(N)\)

  1. 删除单个元素
    erase(it)即删除迭代器为it 处的元素。
  2. 删除一个区间内的所有元素。
    erase(frst, last)即删除[first, last)内的所有元素。

由上面的说法可以知道,如果要删除这个vector 内的所有元素,正确的写法应该是vi.erase(vi.begin(), vi.end()),这正如前面说过,vi.end()就是尾元素地址的下一个地址。(当然,更方便的清空vector的方法是使用vi.clear())。

vector的常见用途

(1)储存数据

  1. vector本身可以作为数组使用,而且在一些元素个数不确定的场合可以很好地节省空间。
  2. 有些场合需要根据一些条件把部分数据输出在同一行,数据中间用空格隔开。由于输出数据的个数是不确定的,为了更方便地处理最后一个满足条件的数据后面不输出额外的空格,可以先用vector记录所有需要输出的数据,然后一次性输出。
    (2)用邻接表存储图
    使用vector实现邻接表可以让一些对指针不太熟悉的读者有一个比较方便的写法。

set

set翻译为集合,是一个内部自动有序且不含重复元素的容器。在考试中,有可能出现需要去掉重复元素的情况,而且有可能因这些元素比较大或者类型不是int型而不能直接开散列表,在这种情况下就可以用set来保留元素本身而不考虑它的个数。当然,上面说的情况也可以通过再开一个数组进行下标和元素的对应来解决,但是set提供了更为直观的接口,并且加入set之后可以实现自动排序,因此熟练使用set可以在做某些题时减少思维量。

set只能通过迭代器(iterator)访问。除开vector和string 之外的STL容器都不支持*(it + i)的访问方式。

set内的元素自动递增排序,且自动去除了重复元素。

set常用函数解析

(1) insert()
inser(x)可将x插入set容器中,并自动递增排序和去重,时间复杂度\(O(logN)\),其中N为set内的元素个数。
(2) find()
find(value)返回set中对应值为value的迭代器,时间复杂度为\(O(logN)\),N为set内的元素个数。
(3) erase()
erase()有两种用法:删除单个元素、删除一个区间内的所有元素。

  1. 删除单个元素。
    删除单个元素有两种方法:

    • st.erase(it),it为所需要删除元素的迭代器。可以结合find()函数来使用。
    • st.erase(value),value为所需要删除元素的值。时间复杂度为\(O(logN)\)
    • 删除一个区间内的所有元素。st.erase(first, last)可以删除一个区间内的所有元素,其中first为所需要删除区间的起始迭
      代器,而last则为所需要删除区间的末尾迭代器的下一个地址,也即为删除[first, last)。
      (4) size()
      size()用来获得set内元素的个数,时间复杂度为\(O(1)\)
      (5) clear()
      clear()用来清空set中的所有元素,复杂度为O(N),其中N为set内元素的个数。
  2. set 的常见用途
    set最主要的作用是自动去重并按升序排序,因此碰到需要去重但是却不方便直接开数组的情况,可以尝试用set解决。

延伸:set 中元素是唯一的,如果需要处理不唯一的情况,则需要使用multisetet。

另外,C++ 11标准中还增加了unordered_set,以散列代替set内部的红黑树(Red Black Tree),一种自平衡二叉查找树)实现,使其可以用来处理只去重但不排序的需求,速度比set要快得多,有兴趣的读者可以自行了解,此处不多作说明。

string 的常见用法

在C语言中,一般使用字符数组char str[]来存放字符串,但是使用字符数组有时会显得操作麻烦,而且容易因经验不足而产生一些错误。为了使编程者可以更方便地对字符串进行操作,C++在STL中加入了string 类型,对字符串常用的需求功能进行了封装,使得操作起
来更方便,且不易出错。

用c_str()将 string类型转换为字符数组进行输出。

有些函数比如insert()与erase()则要求以迭代器为参数,因此还是需要学习一下string迭代器的用法。

由于string不像其他STL容器那样需要参数,因此可以直接如下定义:

string::iterator it;

这样就得到了迭代器it,并且可以通过*it来访问string里的每一位。

最后指出,string 和vector一样,支持直接对迭代器进行加减某个数字,如str.begin()+3的写法是可行的。

string常用函数解析

(1) operator+=
这是string的加法,可以将两个string直接拼接起来。
(2) compare operator
两个string类型可以直接使用=、!=、<、 <=、 >、>=比较大小,比较规则是字典序。
(3) length()/size()
length()返回string 的长度,即存放的字符数,时间复杂度为\(O(1)\)。size()与 length()基本相同。
(4) insert()
string的insert()函数有很多种写法,这里给出几个常用的写法,时间复杂度为\(O(N)\)

  1. insert(pos, string),在pos号位置插入字符串string。
  2. insert(it, it2, it3); it 为原字符串的欲插入位置,it2 和it3为待插字符串的首尾迭代器,用来表示串[it2,it3)将被插在it的位置上。

(5) erase()
erase()有两种用法:删除单个元素、删除一个区间内的所有元素。时间复杂度均为\(O(N)\)

  1. 删除单个元素。
    str.erase(it)用于删除单个元素,it为需要删除的元素的迭代器。
  2. 删除一个区间内的所有元素。
    删除一个区间内的所有元素有两种方法:
    • str.erase(first, last),其中first为需要删除的区间的起始迭代器,而last则为需要删除的区间的末尾迭代器的下一个地址,也即为删除[first, last)。
    • str.erase(pos, length),其中pos为需要开始删除的起始位置,length为删除的字符个数。

(6) clear()
clear()用以清空string中的数据,时间复杂度一般为\(O(1)\)
(7) substr()
substr(pos,len)返回从pos号位开始、长度为len的子串,时间复杂度为\(O(len)\)
(8) string::npos
string::npos是一个常数,其本身的值为-1,但由于是unsigned_int类型,因此实际上也可以认为是unsigned_int类型的最大值。string::npos用以作为find函数失配时的返回值。例如在下面的实例中可以认为string::npos 等于-1或者4294967295。
(9) find()
str.find(str2),当str2 是str的子串时,返回其在str 中第一次出现的位置;如果str2不是str的子串,那么返回string::npos。

str.find(str2, pos),从str的pos号位开始匹配str2,返回值与上相同。

时间复杂度为\(O(nm)\),其中n和m分别为str和str2的长度。
(10) replace()
str.replace(pos, len, str2)把str从pos号位开始、长度为len的子串替换为str2。
str.replace(itl, it2, str2)把str的迭代器[it1, it2)范围的子串替换为str2。
时间复杂度为\(O(str.length())\)

map

map翻译为映射,也是常用的STL容器。众所周知,在定义数组时(如int array[100]),其实是定义了一个从int型到int型的映射,比如array[0]= 25、array[4]= 36就分别是将0映射到25、将4映射到36。一个double型数组则是将int型映射到double型,例如db[0]=3.14,db[1]=0.01。但是,无论是什么类型,它总是将int型映射到其他类型。这似乎表现出一一个弊端:当需要以其他类型作为关键字来做映射时,会显得不太方便。

例如有一本字典, 上面提供了很多的字符串和对应的页码,如果要用数组来表示“字符串-->页码”这样的对应关系,就会感觉不太好操作。这时,就可以用到map,因为map可以将任何基本类型(包括STL容器)映射到任何基本类型(包括STL容器),也就可以建立string型到int 型的映射。

还可以来看一个情况:这次需要判断给定的一些数字在某个文件中是否出现过。按照正常的思路,可以开一一个bool型hashTable[max_size], 通过判断hashTable[x]为true还是false来确定x是否在文件中出现。但是这会碰到一个问题,如果这些数字很大(例如有几千位),那么这个数组就会开不了。而这时map就可以派上用场,因为可以把这些数字当成一些字符串,然后建立string至int的映射(或是直接建立int至int的映射)。

如果是字符串到整型的映射,必须使用string而不能用char数组,这是因为char 数组作为数组,是不能被作为键值的。如果想用字符串做映射,必须用string。

前面也说到,map的键和值也可以是STL容器,例如可以将一个set容器映射到一个字符串:

map<set<int>, string> mp;

要注意的是,map中的键是唯一的。

map迭代器的使用方式和其他STL容器的迭代器不同,因为map的每一对映射 都有两个typename,这决定了必须能通过一一个 it 来同时访问键和值。事实上,map可以使用it->first来访问键,使用it->second来访问值。

#include<cstdio>
#include<iostream>
#include<map>
using namespace std;
int main()
{
	map<char, int> mp;
	mp['m'] = 20;
	mp['r'] = 30;
	mp['a'] = 40;
	for(map<char, int>::iterator it=mp.begin();it != mp.end();it++)
		cout<<it->first<<' '<<it->second<<endl;
		
	return 0;
}

输出如下:

a 40
m 20
r 30

map会以键从小到大的顺序自动排序。这是由于map内部是使用红黑树实现的(set也是),在建立映射的过程中会自动实现从小到大的排序功能。

map常用函数

  1. find()
    find(key)返回键为key的映射的迭代器,时间复杂度为\(O(logN)\), N为map中映射的个数。

  2. erase()
    erase()有两种用法:删除单个元素、删除一个区间内的所有元素。

    1. 删除单个元素。
      删除单个元素有两种方法:

      1. mp.erase(it), it为需要删除的元素的迭代器。时间复杂度为\(O(1)\)
      2. mp.erase(key), key为欲删除的映射的键。时间复杂度为\(O(logN)\),N为map内元素的个数。
    2. 删除一个区间内的所有元素。
      mp.erase(first, last), 其中first为需要删除的区间的起始迭代器,而last则为需要删除的区间的末尾迭代器的下一个地址,也即为删除左闭右开的区间[first, last)。时间复杂度为O(last-first)。

  3. size()
    size()用来获得map中映射的对数,时间复杂度为\(O(1)\)

  4. clear()
    clear()用来清空map中的所有元素,复杂度为\(O(N)\),其中N为map中元素的个数。

map的常见用途

  1. 需要建立字符(或字符串)与整数之间映射的题目,使用map可以减少代码量。
  2. 判断大整数或者其他类型数据是否存在的题目,可以把map当bool数组用。
  3. 字符串和字符串的映射也有可能会遇到。
    延伸: map的键和值是唯一的,而如果一个键需要对应多个值,就只能multimap。 另外,C++ 11标准中还增加了unordered_map,以散列代替map内部的红黑树实现,使其可以用来处理只映射而不按key排序的需求,速度比map要快得多。

queue

由于队列(queue)本身就是一种先进先出的限制性数据结构,因此在STL中只能通过front()来访问队首元素,或是通过back()来访问队尾元素。

queue常用函数解析

(1)push()
push(x)将x进行入队,时间复杂度为\(O(1)\)
(2) front()、back()
front()和back()可以分别获得队首元素和队尾元素,时间复杂度为\(O(1)\)
(3) pop()
pop()令队首元素出队,时间复杂度为\(O(1)\)
(4) empty()
empty()检测queue是否为空,返回true则空,返回false则非空。时间复杂度为\(O(1)\)
(5) size()
size()返回queue内元素的个数,时间复杂度为\(O(1)\)

4.queue的常见用途
当需要实现广度优先搜索时,可以不用自己手动实现一个队列,而是用queue作为代替,以提高程序的准确性。

另外有一点注意的是,使用front()和pop()函数前,必须用empty()判断队列是否为空,否则可能因为队空而出现错误。

延伸:STL的容器中还有两种容器跟队列有关,分别是双端队列(deque)和优先队列(priority_queue),前者是首尾皆可插入和删除的队列,后者是使用堆实现的默认将当前队列最大元素置于队首的容器。其中优先队列将在6.6节进行介绍,而双端队列则留给有兴趣的读者去了解,此处不再多作介绍。

priority_queue

priority_queue又称为优先队列,其底层是用堆来进行实现的。

当然,可以在任何时候往优先队列里面加入(push)元素,而优先队列底层的数据结构堆(heap)会随时调整结构。

和队列不一样的是,优先队列没有front()函数与back()函数,而只能通过top()函数来访问队首元素(也可以称为堆顶元素)。

priority_queue常用函数解析

(1) push()
push(x)将令x入队,时间复杂度为\(O(logN)\),其中N为当前优先队列中的元素个数。
(2) top()
top()可以获得队首元素(即堆顶元素),时间复杂度为\(O(1)\)
(3) pop()
pop()令队首元素(即堆顶元素)出队,时间复杂度为\(O(logN)\),其中N为当前优先队列中的元素个数。
(4) empty()
empty()检测优先队列是否为空,返回true则空,返回false则非空。时间复杂度为\(O(1)\)
(5) size()
size()返回优先队列内元素的个数,时间复杂度为\(O(1)\)

priority_queue 内元素优先级的设置

如何定义优先队列内元素的优先级是运用好优先队列的关键,下面分别介绍基本数据类型(例如int、double、char) 与结构体类型的优先级设置方法。
(1)基本数据类型的优先级设置
此处指的基本数据类型就是int型、double型、char型等可以直接使用的数据类型,优先队列对它们的优先级设置一般是数字大的优先级越高,因此队首元素就是优先队列内元素最大的那个(如果char型,则是字典序最大的)。对基本数据类型来说,下面两种优先队列的定义是等价的(以int型为例,注意最后两个>之间有一个空格 ):

priority_queue<int> q;
priority_queue<int,vector<int>,less<int> > q;

可以发现,第二种定义方式的尖括号内多出了两个参数:一个是vector,另一个是less。其中vector(也就是第二个参数)填写的是来承载底层数据结构堆(heap)的容器,如果第一个参数是double型或char型,则此处只需要填写vector或vector;而第三个参数less 则是对第一个参数的比较类,less表示数字大的优先级越大,而greater表示数字小的优先级越大。

因此,如果想让优先队列总是把最小的元素放在队首,只需进行如下定义:

priority_queue<int,vector<int>,greater<int> > q;

(2)结构体的优先级设置
对小于号的重载与排序函数sort中的cmp函数有些相似,它们的参数都是两个变量,函数内部都是return了true或者false。事实上,这两者的作用确实是类似的,只不过效果看_上去似乎是“相反”的。在排序中,如果是“return fl.price> f2.price",那么则是按价格从高到低排序,但是在优先队列中却是把价格低的放到队首。原因在于,优先队列本身默认的规则就是优先级高的放在队首,因此把小于号重载为大于号的功能时只是把这个规则反向了一下。如果读者无法理解,那么不妨先记住,优先队列的这个函数与sort中的cmp函数的效果是相反的。

那么,有没有办法跟sort中的cmp函数那样写在结构体外面呢?自然是有办法的。只需把小于号改成一对小括号, 然后把重载的函数写在结构体外面,同时将其用struct包装起来,如下所示(请读者注意对比):

struct cmp
{
	bool operator() (fruit f1,fruit f2)
	{
		return f1.price > f2.price;
	}
}

在这种情况下,需要用之前讲解的第二种定义方式来定义优先队列:

priority_queue<fruit,vector<fruit>,cmp> q;

可以看到,此处只是把greater<部分换成了cmp。

读者应当能够想到,即便是基本数据类型或者其他STL容器(例如set),也可以通过同样的方式来定义优先级。

最后指出,如果结构体内的数据较为庞大(例如出现了字符串或者数组),建议使用引用来提高效率,此时比较类的参数中需要加上“const"和“&”。

priority_queue 的常见用途

priority_queue可以解决一些贪心问题(例如9.8节),也可以对Dijkstra算法进行优化(因为优先队列的本质是堆)。

有一点需要注意,使用top()函数前,必须用empty()判断优先队列是否为空,否则可能因为队空而出现错误。

stack

由于栈(stack)本身就是一种后进先出的数据结构,在STL的stack中只能通过top()来访问栈顶元素。

stack常用函数解析

(1) push()
push(x)将x入栈,时间复杂度为\(O(1)\)
(2) top()
top()获得栈顶元素,时间复杂度为\(O(1)\)
(3) pop()
pop()用以弹出栈顶元素,时间复杂度为\(O(1)\)
(4) empty()
empty()可以检测stack内是否为空,返回true 为空,返回fals为非空,时间复杂度为\(O(1)\)
(5) size()
size()返回stack内元素的个数,时间复杂度为\(O(1)\)

stack的常见用途

stack用来模拟实现一些递归,防止程序对栈内存的限制而导致程序运行出错。一般来说,程序的栈内存空间很小,对有些题目来说,如果用普通的函数来进行递归,一旦递归层数过深(不同机器不同,约几千至几万层),则会导致程序运行崩溃。

如果用栈来模拟递归算法的实现,则可以避免这一方面的问题(不过这种应用出现较少)。

pair

pair是一个很实用的“小玩意”,当想要将两个元素绑在一起作为一个合成元素、又不想要因此定义结构体时,使用pair可以很方便地作为一个代替品。也就是说,pair实际上可以看作一个内部有两个元素的结构体,且这两个元素的类型是可以指定的。

pair中只有两个元素,分别是first和second,只需要按正常结构体的方式去访问即可。

pair常用函数解析

两个pair类型数据可以直接使用=、!=、<、<=、>、>=比较大小,比较规则是先以first的大小作为标准,只有当first相等时才去判别second的大小。

pair的常见用途

关于pair有两个比较常见的例子:

  1. 用来代替二元结构体及其构造函数,可以节省编码时间。
  2. 作为map的键值对来进行插入,例如下面的例子。

algorithm头文件下的常用函数

  1. max()、min()和abs()
    max(x, y)和min(x, y)分别返回x和y中的最大值和最小值,且参数必须是两个(可以是浮点数)。如果想要返回三个数x、y、z的最大值,可以使用max(x,max(y, z))的写法。

abs(x)返回x的绝对值。注意: x必须是整数,浮点型的绝对值请用math 头文件下的fabs。

  1. swap()
    swap(x, y)用来交换x和y的值。

  2. reverse()
    reverse(it, it2)可以将数组指针在[it, it2)之间的元素或容器的迭代器在[it, it2)范围内的元素进行反转。

  3. next_permutation()
    next_permutation()给出一个序列在全排列中的下一个序列。

使用循环是因为next_permutation()在已经到达全排列的最后-一个时 会返回false,这样会方便退出循环。而使用do... while语句而不使用while语句是因为序列123本身也需要输出,如果使用while会直接跳到下一个序列再输出,这样结果会少一个123。

  1. fill()
    fill()可以把数组或容器中的某一段区间赋为某个相同的值。和memset不同,这里的赋值可以是数组类型对应范围中的任意值。

sort()

顾名思义,sort就是用来排序的函数,它根据具体情形使用不同的排序方法,效率较高。

一般来说,不推荐使用C语言中的qsort函数,原因是qsort用起来比较烦琐,涉及很多指针的操作。而且sort在实现中规避了经典快速排序中可能出现的会导致实际复杂度退化到\(O(n^2)\)的极端情况。希望读者能通过这篇介绍来轻松愉快地使用sort函数。

对char型数组排序默认为字典序。

如果需要对序列进行排序,那么序列中的元素一定要有可比性,因此需要制订排序规则来建立这种可比性。特别是像结构体,本身并没有大小关系,需要人为制订比较的规则。sort 的第三个可选参数就是compare函数(一般写作cmp函数),用来实现这个规则。

在STL标准容器中,只有vector、string、 deque 是可以使用sort 的。这是因为像set、map这种容器是用红黑树实现的(了解即可),元素本身有序,故不允许使用sort排序。

lower_bound()和upper_bound()

lower_bound()和upper_bound()需要用在一个有序数组或容器中,在4.5.1 中已经讨论过它们的实现。

lower_bound(first, last, val)用来寻找在数组或容器的[first, last)范围内第一个值大于等于val的元素的位置,如果是数组,则返回该位置的指针;如果是容器,则返回该位置的迭代器。

upper_bound(first, last, val)用来寻找在数组或容器的[first, last)范围内第一个值大于val的元素的位置,如果是数组,则返回该位置的指针;如果是容器,则返回该位置的迭代器。

显然,如果数组或容器中没有需要寻找的元素,则lower_bound()和upper_bound()均返回可以插入该元素的位置的指针或迭代器(即假设存在该元素时,该元素应当在的位置)。

lower_bound()和upper_bound()的复杂度均为\(O(log(last - first))\)

posted @ 2021-02-17 10:53  Dazzling!  阅读(19)  评论(0编辑  收藏  举报