【C++基础】第9章:序列与关联容器

序列与关联容器

1 容器概述

1.1 容器:一种特殊的类型,使用这样的类型构造出相应对象,这个对象可以放置其它类型的对象(元素)

1.1.1 需要支持的操作(不是所有容器都支持4个操作):对象的添加、删除、索引、遍历(遍历容器中所有元素)

1.1.2 有多种算法可以实现容器,每种方法各有利弊

1.2 容器分类

1.2.1 序列容器:容器中的对象有序排列,使用整数值进行索引

给定一个容器,给定一个整数值,我们就能够获取这个整数值所对应的元素。

1.2.2 关联容器:容器中的对象顺序并不重要,使用键进行索引

如何获取关联容器里面的内容?

使用一种特殊的键索引。给定一个键,返回这个键所对应的值。序列容器的特殊的键:整数值。

但是对于关联容器,这个键就可能是一般意义上的键

1.2.3 适配器:调整原有容器的行为,使得其对外展现出新的类型、接口或返回新的元素

1.2.4 生成器:构造元素序列

1.3 迭代器:用于指定容器中的一段区间,以执行遍历、删除等操作

1.3.1 获取迭代器: ©begin/©end ; ©rbegin/©rend

如下图6行定义一个容器vector
7行:获取
在这里插入图片描述
上图使用begin和end获取容器的一组迭代器(迭代器通常是成对出现,区间前后,一共两个迭代器),begin指向容器x的第一个元素,end指向容器x中最后一个元素的下一位。即描述了一个区间——[b, e)

换成cbegin,cend也可以:
在这里插入图片描述
cbegin,cend和begin,end的区别:
c代表了const,如果使用cbegin构造迭代器,那么这个迭代器只能读不能写。而使用begin,有些情况下是可以写的,如下图代码没问题:(11行写入了3)
在这里插入图片描述
但是用cbegin,cend的话,有问题:
在这里插入图片描述

1.3.2 获取迭代器: ©rbegin/©rend

r:reverse
即反过来,从后到前遍历。下图,b指向的是3,e指向1的前面的位置(x第一个元素的前面的那个位置)
在这里插入图片描述

在这里插入图片描述

1.3.3 迭代器分类:分成 5 类( category ),不同的类别支持的操作集合不同

我们相应迭代器模拟数组当中的指针,如下图6行定义数组a[3],7行:a指向数组第一个元素;a+3指向数组最后一个元素的下一位;我们使用a和a+3相当于限定了一个区间,实际上也相当于两个迭代器。
在这里插入图片描述
这两迭代器功能非常强大,我们可以使用这个迭代器进行读,写,比较,解引用等等。

但是并非所有迭代器都能实现像一个数组的这种指针这样的全部功能。

容器有多种算法可以实现,可能有些算法在实现之后,容器是可以提供迭代器,但是这个迭代器不能支持某种具体操作。比如某个迭代器a,我们不能写a+3(这个迭代器不支持,或者说这个迭代器执行起来很困难)。我们用迭代器的目的是用来泛化模拟指针的概念,但是指针提供的所有操作并非迭代器都能支持,那么我们根据迭代器对于操作支持的类型不同,我们把迭代器分为5类(下图绿色)。
在这里插入图片描述
事实上,只要容器中支持迭代器,那么就可以进行遍历操作。

2 序列容器

2.1 C++ 标准库中提供了多种序列容器模板

数组就是一个典型的序列容器。本章主要讨论c++标准库的序列容器。

2.1.1 array :元素个数固定的序列容器

array不支持对象添加删除,和c的内建数组很相似。

其他容器的元素个数基本上是可变的。

2.1.2 vector :元素连续存储的序列容器

2.1.3 forward_list / list :基于单向链表 / 双向链表的容器

2.1.4 deque : vector 与 list 的折中

vector中的元素是完全连续存储的;forward_list / list是基于单向链表 / 双向链表的容器,一个元素和另外一个元素之间可能隔着很远,他们是通过指针关联起来的。

deque会把容器分成若干段,每一个段是连续存储的,但是段与段之间可能是通过链表的结构构造出来。

正是因为这样的设计导致vector、list、deque各有千秋。

2.1.5 basic_string :提供了对字符串专门的支持

array、vector、forward_list\list、deque、basic_string都是模板,我们需要具体的元素类型来实例化。

2.2 需要使用元素类型来实例化容器模板,从而构造可以保存具体类型的容器

下图6行的x能保存int类型的对象,7行y能保存double类型的对象。vector是类模板,我们需要使用int类型(元素的类型,即模板中包含什么类型的元素)构造具体的类型。
在这里插入图片描述

2.3 不同的容器所提供的接口大致相同,但根据容器性质的差异,其内部实现与复杂度不同

2.4 对于复杂度过高的操作,提供相对较难使用的接口或者不提供相应的接口

2.5 array 容器模板:具有固定长度的容器,其内部维护了一个内建数组,与内建数组相比提供了复制操作(内建数组不支持复制操作,如下图7行)

在这里插入图片描述
但是使用array容器是支持复制的。
在这里插入图片描述
以下代码可以编译运行:s复制给了b
在这里插入图片描述
之前说我们需要使用元素类型实例化容器模板,但是array有个典型特点:
在这里插入图片描述
上图是array的结构体声明,里面包含T(class,代表T是一个类型)和N(是一个数),换句话説要去构造array,不仅要提供一个元素类型,还需提供到底容器要包含多少个元素。如下图7行,构造了一个容器,容器包含了3个int型。为什么要这样提供?因为之前提到过,array具有固定长度,我们没办法在运行期动态添加或删除元素,因此,我们需要在构造array的对象时,在编译期就把容器长度给定。故对于array,我们需要提供两个参数——T和N。

2.6 提供的接口

2.6.1 array容器的构造

在这里插入图片描述

  1. 由上图,我们对array这样的类型的对象初始化时,并没有显式的构造函数,它是隐式声明的。它的构造方法和之前讨论数组时的aggregate initialization(累计初始化)相似。如下图7行:a当中所有元素都会初始化为0:
    在这里插入图片描述
    再如下图7行:a中第一个元素会被初始化为1,其他所有元素都会初始化为0
    在这里插入图片描述
    以上即累计初始化的概念。
  2. 下图这样是缺省初始化:(a中的每个元素是int类型,int是内建数据类型,不是抽象类类型,对于int类型,在缺省初始化时,它的值是不确定的,即使用缺省初始化时,可能会导致乱值)
    在这里插入图片描述

2.6.2 成员类型: value_type 等

通常来讲,容器会提供一些在内部自定义的类型信息,如成员类型。
在这里插入图片描述

  1. value_type
    如下图:value_type即代表容器元素类型
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    上图,1代表value_type是int,与之类似,int换为char,输出1则代表value_type是char
    在这里插入图片描述

在这里插入图片描述

  1. size_type
    array中包含多少个元素?我要调用一些接口来返回,返回的类型就是size_type,

2.6.3 元素访问(索引): [] , at , front , back , data

在这里插入图片描述

  1. []
    operator[ ]实现了一个成员函数,这个成员函数是使用[ ]来进行访问的,如:
    在这里插入图片描述
    上图,6行定义了一个array,写了2,4,6,8。接下来我们写一个numbers[1](8行),返回的是4。

以上就是典型的使用[]访问容器元素。

  1. at
    我们还可以使用at来访问容器元素
    at和[]有何区别?
    如果我访问的这个元素超过了容器的范围,程序直接崩溃或乱值。如下:
    在这里插入图片描述
    在这里插入图片描述
    如果下图9行加了at:(如果100超过了容器范围,系统直接崩溃)
    在这里插入图片描述
    在这里插入图片描述
  2. front
    下图,front返回的是8行的a中的第一个元素:

在这里插入图片描述
在这里插入图片描述

  1. back
    back返回的是上图a中的最后一个元素。
  2. data
    如下图,data返回的是T*:
    在这里插入图片描述
    如下图,如果我们9行调用data,那么返回的是int*:(int*是一个指针,指向数组a中第一个元素)
    在这里插入图片描述
    为什么要提供这样一个功能?

我们可能会在程序里使用array(下图9行),但是隔壁张三可能写了一个函数,这个函数需要接收int* ptr。此时我们调用fun函数时,只能写fun(a.data())
在这里插入图片描述
换句话说,a.data()干了什么事:有些函数的接口是偏向与c语言这种传统类型的接口,那么我们可以使用.data来获取底层的内容传给上图6行的接口。

vector也包含data这样的元素访问接口,但是list不包含。如果容器中的元素是在内存中连续保存的,那么我们通常会提供data接口,使得我们能访问容器中的内容。如果容器中的元素不是连续保存,那么获取data,返回什么呢?(data只能返回第一个元素,但是我们也有其他方式访问第一个元素)。因此,只有这个序列连续存储时,我们才会提供data接口,返回一个指针指向连续的这块内存的第一个元素。

2.6.4 容量相关(平凡实现): empty , size , max_size

  1. empty
    代表array是空的。array是否为空这件事情是在编译期就能确定。如下图:
    在这里插入图片描述
    在这里插入图片描述
    上图,容器a有3个int型数据,并不为空,返回0,即false。

再如:下图这样返回1
在这里插入图片描述

  1. size
    size代表了容器a中包含多少个元素。
    在这里插入图片描述
    上图代码返回3。
  2. max_size
    max_size代表容器a中最多包含多少个元素。
    在这里插入图片描述
    在这里插入图片描述
    返回的还是3,因为array容器长度固定。

2.6.5 填充与交换: fill , swap

  1. fill
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  2. swap(把两个array进行交换)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    注意:array提供了赋值操作,也提供了swap操作,但是与其他容器相比,array的复制操作和swap操作的成本非常高,因为它的内部维护一个数组。
    vertor的复制操作虽然慢但是swap操作很快,因为vertor的内部不是维护一个数组,而是维护一个指针,指针指向一个数组。此时swap只是交换两个指针的值,故swap时非常快。

2.6.6 比较操作: <=>(比较两个元素哪个大哪个小)

在这里插入图片描述
基本上是按照字典
上图全部都是<T, N>。比较的是容器中两个元素哪个大哪个小,这两个元素的类型必须相同(即上图点的lhs和rhs的类型要一样)。如下图编译error:因为a和b的类型不同,13、14行的3和4属于类型信息的一部分。
在这里插入图片描述
那么比较的行为是?a和b要进行比较,则a和b里面存储的元素要支持比较。
在这里插入图片描述
在这里插入图片描述

2.6.7 迭代器

在这里插入图片描述

2.7 vector 容器模板:元素可变

下图为vector的内部结构:
在这里插入图片描述
其中最重要的是buffer,buffer实际上是一个T*型指针(T代表元素的类型),这个指针指向了一块内存(buffer on heap),前4个T代表有效的元素,后两个深灰色格子指什么都没有,代表了分配这块内存,但是这块内存还没使用。

上一章讨论动态内存管理时说过,vector元素可变,我们可以往里面插入元素,但是如果现在指向的这块内存中没有上图深灰色两个空格,那么我需要先分配一块内存,这块内存大小大于等于5个元素,接下来把上图浅灰格的4个元素拷贝到新分配的内存中的前4个位置,最后在新内存后面再插一个元素。但是这样每插一个元素,都涉及到内存分配和移动或拷贝,代价昂贵。故我们会像上图那块内存一样,在分配内存时多分配一些(深灰格子就是多分配的内存)。

因为涉及到多分配一些内存,故vertor容器模板内部对于buffer的大小,实际上包含两个信息:

  1. size_t size:buffer里面现在包含了可用元素的个数;如上图是4个。
  2. size_t cap:buffer到底能装多少个元素;上图是6个

2.8 提供的接口

2.8.1 与 array 很类似,但有其特殊性

  1. max_size
    在这里插入图片描述
    在这里插入图片描述
    上图黑色为vector能返回的最多的元素个数
  2. vertor之间的比较
    如果vector之间的元素个数一样,那么vector的比较和array的比较一样;如果两个vector的元素个数不同,如下图:
    在这里插入图片描述
    在这里插入图片描述
    即a>b,因为是先比较第一个元素。

再如:
在这里插入图片描述
在这里插入图片描述
再如:
在这里插入图片描述
在这里插入图片描述

  1. swap
    array中的swap是数组中每一个元素都要进行交换;但是vector中有buffer这样的指针,那么vector的swap速度很快(指针进行交换、cap交换、size交换),它的时间复杂度不会根据vector的长度的变化而变化。

2.8.2 vector提供了容量相关接口: capacity / reserve / shrink_to_fit

  1. capacity:capacity返回的是size_t cap(vector最多能存储多少个元素)
  2. reserve:通常在构造vector之后马上会调用reserve,如下图6行构造了一个a,我们要往a里面填充1024个元素,那么我们可以用push不断地填充,然后让vector自动触发——当前buffer用完了,自动扩充buffer,继续往里面填充元素(9行)。
    在这里插入图片描述
    但是这样性能不好。在for循环过程中,可能会涉及到若干次buffer填满,然后开辟新bufer,数据拷贝移动填充的过程。那么显然会影响速度,故我们可以使用reserve:
    在这里插入图片描述
    上图7行,,给vector的buffer设置为1024,即一开始buffer里面就能存储1024个元素,在此基础上push_back时,就不会涉及到buffer满了,然后再重新开更大的buffer的情况。这样能提高系统性能。

上图7行几时才有用?我们知道vector里面要放多少个元素时7行才有效。

  1. shrink_to_fit:我们通常在构造一个vector之后,vector里面的buffer包含着有元素的格子和没元素的格子,有些情况下我们构造vector之后,不会再往vector里填充任何元素,此时可以使用shrink_to_fit,即开辟一块新内存,这块新内存的大小就是size_t size,接下来把buffer里面有元素的格子往这块新内存里面拷贝,再把原有的buffer释放掉,这样做的好处是我们的buffer不会占用过多的内存。我们调用shrink_to_fit就是要把过多的内存释放掉,确保buffer所占用的内存不会很多。

2.8.3 附加元素接口: push_back / emplace_back

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
二者区别:
如下图,
在这里插入图片描述
在这里插入图片描述

  1. push_back:使用字符串"hello"(c语言的字符串)构造string(c++的字符串),然后把c++的这个string调用push_back,将该字符串传入vector中。
  2. emplace_back:但是如果调用emplace_back,会直接使用"hello",在vector的这块内存中(buffer)构造一个string。即使用emplace_back的好处是少了一次对象的拷贝或者移动的过程。在某些情况下性能会得到提升。

不管push_back还是emplace_back,都是在buffer结尾处插入元素。

2.8.4 元素插入接口: insert / emplace(在vector中的buffer的中间插入元素


在vector中的buffer的中间某个地方插入元素:在插入新元素的地方,后面的元素需要往后移动,故这个过程涉及元素的两次拷贝或移动。故相比于push_back、emplace_back,性能更差。

如何使用?

使用insert需要提供迭代器(iterator pos),这个迭代器代表我要插入的位置,将value插入vector中。
在这里插入图片描述
在这里插入图片描述
如上图蓝色,构造了一个vector,里面包含了3个100。
在这里插入图片描述

2.8.5 元素删除接口: pop_back / erase / clear

  1. pop_back :把vector中的buffer的最后一个元素删除
  2. erase :删除指定位置的元素
    删除最后一个元素,并不会影响前面的元素;但是erase,删除指定位置元素,该元素后面的元素要往前挪(要保证整个buffer的连续性)。故erase的性能要比pop_back差一些。
  3. clear:删除所有元素。

2.9 注意

vector提供的是push_back和emplace_back,我们可以以很好的性能的方式往数组中最后插入元素。

2.9.1 vector 不提供 push_front / pop_front ,可以使用 insert / erase 模拟,但效率不高

push_front / pop_front即往buffer中第一个元素的位置插入/删除元素,但这样后面所有的元素都要往后挪/往前挪,这样的话会影响性能。

2.9.2 swap 效率较高

2.9.3 写操作可能会导致迭代器失效

写操作:往buffer中插入元素

以下操作可能会造成迭代器失效:
在这里插入图片描述

  1. swap造成迭代器失效
    迭代器本质还是一个指针,指向buffer中的某个元素。调用swap之后,某个vector和另一个vector进行swap,指针还是指向buffer中原来的元素,但是swap之后的这个buffer实际上是不和当前的vector相关联了。
    如下图:
    在这里插入图片描述
    上图9行执行之后,a中所关联的buffer是b原有的buffer,b中关联的buffer是a原有的buffer。
    下图:
    ptr原先指向a的buffer中的第一个元素,swap之后ptr不指向a中的buffer的第一个元素,它指向的是b中的第一个元素。那么这个ptr对于a来讲,ptr迭代器就已经失效。
    在这里插入图片描述

  2. push_back造成迭代器失效
    在某些情况下,push_back会造成迭代器失效。如下图buffer满了,push_back时,会新开辟一个buffer,再把原buffer内的元素拷贝过去,把原始buffer擦掉,然后T* buffer指向新的数组(buffer)。那么这个过程中,我们把原始的buffer擦掉了,则指向原始的buffer的迭代器会失效。
    在这里插入图片描述

2.10 list 容器模板:双向链表

下图6行:引入一个vector,里面包含3个元素:1、2、3。
7行:通过begin、end遍历vector中的元素,
在这里插入图片描述
在这里插入图片描述

实际上vector是典型的序列容器,而出了vector,还有一个常用的容器——List。

如下图,我们将上图的vector改成list,行为和vector的一致。
在这里插入图片描述
在这里插入图片描述
但list和vector的内部实现天差地别。

vector:我开辟了一段连续内存,我们在这段连续的内存中放入数据:
在这里插入图片描述
list在c++中是使用双向链表来实现的。

双向链表:

如下图,双向链表里面是由一个一个结点()构成的,每个结点里面包含3个元素,其中结点里最主要的元素是中间那个,这一块表示我存储的数据。除了这一块之外,还有两个元素,这两个元素是两个指针(在c和c++常会用键头描述指针的指向关系)。其中一个指针指向的是下一个元素,另一个指针指向前一个元素。这种元素+指针,指针又指向下一个元素的东西,我们称之为链表,而在一个元素中,既有指针指向后一个元素,又有指针指向前一个元素,这种东西就叫双向链表。即通过一个元素,我们可以访问元素的后区域也可以访问元素的前区域。
在这里插入图片描述
回过头来看看上述程序:
如下图,7行的list,本质上是构建了3个node(1、2、3),其中这3个node,1有一个指针指向2,2有个指针指向3,但是同时2也有指针指向1,3也有指针指向2。
在这里插入图片描述

2.11 list与vector相比

2.11.1 list插入、删除成本较低,但随机访问成本较高

如下图,要想在vector的buffer的第二位插入一个元素,那么我需要移动3个元素。
在这里插入图片描述
但如果是双向链表的话,要想在下图第一个node和第二个node中间插入一个元素,那么需要新构造一个node,把新node的首指针指向第一个node中的第3个元素,第一个node的尾指针指向新node的第1个元素;新node和旧的第2个node之间的指针操作同理。实际上,这种插入只是指针间的变换,这些变换和数组长度没关系。而如果想在vector中插入一个元素,和数组的长度是有关的,这个数组包含100个元素和包含10000个元素的插入元素的成本不一样(vector包含10000个元素,如果在buffer的第一位插入元素,那么后面10000个元素都得后挪一位),但是对于list,即使list里面包含10000个元素,我们在list的第二位插入一个东西,它的成本也是一个常量的复杂度,故它的插入成本比较低。

删除元素成本同理。删除第二个node,即把第2个node删除,然后把第1个node的尾指针指向原先第3个node的首指针;把原先第3个node的首指针指向第1个node的尾指针。
在这里插入图片描述
但list的随机访问成本比较高。
如下图,访问vector中的元素(8行):读取vector中的元素很快。

在这里插入图片描述
上图8行:首先拿到了vector内部保存的指针,指针指向了一个数组,然后接下来用一个简单的指针计算,获取指针所指向的那个数组的位置,然后以2个int型的大小,即可取得相应的元素。

但是list:假设list支持随机访问,如写a[100],那么需要先找到第1个元素a[0],往后挪一个即a[1],再往后挪1个即a[2],我们需要一直挪100次,才能得到a[100],因此这样的随机访问成本非常高。

2.11.2 提供了 pop_front / splice 等接口

list中包含的很多接口和vector相似。这里重点说说pop_front / splice等接口。

  1. pop_front:在链表开头插入或弹出一个元素。之前说过,vector不敢提供pop_front / splice接口,因为它的插入删除成本高。但是list插入删除元素成本低,故提供了pop_front / splice接口。
    但是由于list的随机访问成本高,故list没有提供[ ]这样的访问:如下图编译失败
    在这里插入图片描述
  2. splice
    在这里插入图片描述
    splice可以把某一个list里面的内容一次性挪到另外一个list里面,如下图有list1、list2:
    其中,advance(it, 2):把it挪动2位,原本it指向list1的第一个元素1,挪动2位后,it指向list1的第3个元素3。
    list1.splice:把list2里面的内容一次性插到it(list中的第3个元素的位置)的位置上,故最后打印出1 2 10 20 30 40 50 3 4 5。但是此时list就空了。
    在这里插入图片描述
    在这里插入图片描述
    为什么list能通过splice这样的接口?因为list是使用链表实现的,那么就很容易的把链表中的一部分内容截断出来,然后把这部分内容放到另外一个list里面,这个操作很快。
    但是vector不一样,vector这样操作成本很高。比如vector2里面包含100个元素,要把这100个元素扔到vector1里面,基本上数据就要移动100次,这样很慢。

2.11.3 list的写操作通常不会改变迭代器的有效性

vector的一些操作会改变迭代其的有效性。如push_back,在buffer后面插满了元素之后,就得新开辟一块buffer,把原buffer中的元素拷贝过去,但这样的话,原先迭代器就失效了。
但list不一样,如一个迭代器指向第1个node的第一个元素,接下来往这个元素的前面或后面插入新元素,这样只会改变元素内部指针指向的位置,但是这个元素本身还在那,那么这个元素的迭代器还是在那。

故list的写操作通常不会改变迭代器的有效性。但是有些情况下写操作通常也会改变迭代器的有效性,如迭代器指向某个元素,但是我们把该元素删了,那么这个迭代器就无效了。
在这里插入图片描述

2.12 forward_list 容器模板:单向链表

单向链表中一个node只有一个元素指向它的后区域,没有元素有指针指向它的前区域。
在这里插入图片描述

2.12.1 目标:一个成本较低的线性表实现

在这里插入图片描述
在这里插入图片描述
以上程序的行为和list、vector的一样,但forward_list的实现和list不一样——单向链表。其迭代器只支持递增操作,因此无 rbegin/rend(不能通过rbegin/rend反向遍历)。

2.12.2 forward_list其迭代器只支持递增操作,因此无 rbegin/rend

error:
在这里插入图片描述
编译ok:list能反向遍历
在这里插入图片描述

2.12.3 forward_list不支持 size

list支持size:
在这里插入图片描述
forward_list不支持 size:
在这里插入图片描述
这是因为我们不能通过链表来获取链表有多大,包含多少个元素。对于一个链表,要获取它能包含多少个元素,得一个元素一个元素遍历,遍历一个元素就+1,遍历一个元素就+1。。。遍历到最后才知道有多少个元素。

但是这样很慢。那么list如何支持size?链表是list得主体结构,但除了链表,list还有一些其他得数据成员(可能是用来保留链表包含多少个元素),然后调用size函数时,会直接把包含得元素个数返回。

但forward_list不支持 size,理论上forward_list也可以像list那样,用一个数据成员用于保留链表包含多少个元素。但是forward_list得目标是成本较低得线性表实现。

2.12.4 forward_list不支持 pop_back / push_back

pop_back / push_back耗费内存,需要pop_back / push_back,直接用list就好了。

2.12.5 XXX_after 操作

在这里插入图片描述
forward_list的插入和删除操作,要使用XXX_after 操作。r如迭代器指向下图42,那么我们调用erase_after,删除的是42后一个元素53;调用insert_after,那么我们能在42和53之间插入一个元素。
在这里插入图片描述

2.13 deque容器模板: vector 与 list 的折中

下图为deque的布局(deque的元素排布):键头代表链表。和list不太一样,list里面一个node,只包含一个元素;但是deque里面,一个node里面可能包含多个元素,其中白格子表示分配了内存,但是没有往里写入任何东西,灰色代表分配了内存,内存里面填满了具体元素。故deque融合了list和vector。

在这里插入图片描述
但是deque实现时是通过了一个类似vector的数组保留了一系列指针,每一个指针都指向了一个有效的元素组(元素组里面有几个元素),这些元素可能是存储了我们的数据。
在这里插入图片描述
那么这样的设计有什么好处?

2.13.1 push_back / push_front 速度较快

  1. 为什么push_back比较快?
    如下图,我们看最后一行,这个构造就是典型的vector构造,push_back的话只需往里面添加元素即可,如果最后一行插满了,则需开辟新内存,但是开辟新内存后的操作和vector不一样,vector是要把所有元素都拷贝到新内存上。
    在这里插入图片描述
    而deque不需要,deque开辟一块新内存,需要把下图红圈维护一下,在红圈5的下面也开辟一块新内存,把它指向新开辟的存储元素的那块内存。然后往里面插入元素即可。
    相当于与vector相比,deque没有把原buffer的元素复制到新开辟的内存里面,而是直接在新内存插入想插入的新元素?
    在这里插入图片描述
  2. push_front类似,如上图右边第1行,往前插元素,插满了之后再在前面再开一块内存往前插入元素。
    deque的push_front比较快,是和vector比,和list比并没有明显优势。

2.13.2 deque支持[]随机访问

如访问元素100(第100个元素到底是啥值),如下图,我知道第一个块里面包含3个值,然后100-3=97,第2个块包含6个元素,97/6看商和余是多少,即可在下图找到相应位置:
在这里插入图片描述
整个过程 只涉及到简单计算,并没有设计内存读取,因此会比较快。但访问速度和vector有差距。

2.13.3 在序列中间插入、删除速度较慢

每个元素要前挪或后挪。

2.13.4 什么情况下会使用deque?

大部分情况我们会使用vector或list。vector随机访问比较快;list插入删除操作比较快,且沿着顺序访问元素实际上并不慢。

大部分情况下我们希望获得类似vector的功能,又希望容器的push_front相对来讲比较快,此时才尝试使用deque。

2.14 basic_string 容器模板:实现了字符串相关的接口

2.14.1 使用 char 实例化出 std::string

在这里插入图片描述

2.14.2 提供了如 find , substr 等字符串特有的接口

从容器角度而言,string和vector有很大的相似之处。basic_string 容器模板:实现了字符串相关的接口:
在这里插入图片描述
在这里插入图片描述

2.14.3 提供了数值与字符串转换的接口

  1. to_string
    如下图:传入int型整数value,long型整数value,double型value等等,都会通过to_string转换为字符串。
    在这里插入图片描述
  2. stoi、stol、stoll
    把string转换成相应的数。
    stoi:把string转换成int
    stol:把string转换成long
    stoll:把string转换成long long
    在这里插入图片描述
    在这里插入图片描述

与之类似,也有相应的函数将string转换成double、short等等。

2.14.4 针对短字符串的优化(short string optimization: SSO )

我们在构造vector时,vector里面包含多少个元素这件事情很难确定;但字符串里面包含多少个元素?通常来讲不会太长。大部分情况下我们使用basic_string,都会保留短字符串。

因此编译器在实现basic_string时都会针对短字符串进行优化。
我们参考:https://www.cnblogs.com/cthon/p/9181979.html
下图是string的结构:(和vector很像)
在这里插入图片描述
在这里插入图片描述

3 关联容器

下图是顺序容器:
7行a[1]:给定了一个容器a,传入整数值1(),返回1所对应的值2(这个值是int型对象)。
在这里插入图片描述
关联容器:并不限定是整数。如下图:map就是典型的关联容器。
下图10行,map实例化,容器对象为m
11行:访问容器m
在这里插入图片描述
在这里插入图片描述
上图10行把a和3关联起来了,10行定义的map类型的对象m实际上是包含两个类型的——char(键的类型)、int(值的类型),即相当于构造了一个映射,把一个字符映射成相应整数。{‘a’, 3}指当字符是a时,构造为3;当字符是b时,构造为4。此时11行调用a时即返回3。

但从容器角度上来看8行和11行,并没有本质差别,都是定义一个键,来返回相应的对象。但是对于顺序容器,它的键是固定好的,即从0开始逐一递增;但是对于关联容器,我们可以引入更多方法来定义键(如字符串char、int):
在这里插入图片描述
在这里插入图片描述
11行相当于:10行初始化m,作为map对象,这个map对象里面包含一个元素,该元素对应的键是2,没有元素对应键是0或1,它的元素所对应的值为取值为3的int型对象。而对于顺序容器,如a[2],我们知道键为2,那么一定存在键是0、1的情况。

3.1 使用键进行索引

关联容器引入8中数据类型。基本上可以分为两类:

  1. set / map / multiset / multimap
  2. unordered_set / unordered_map / unordered_multiset / unordered_multimap

3.2 set / map / multiset / multimap 底层使用红黑树实现

3.2.1 set

set是一个集合,里面包含一系列元素。

如下图:
6行:定义了一个int的集合:在这里插入图片描述
在这里插入图片描述
前面提到,关联容器和顺序容器的主要区别是:关联容器是使用键来索引的(索引:给定一个键,返回一个值),而关联容器set也是一样。

但是上图程序6行,这个set容器中,键是啥?值又是啥?

答:由6行int知,是任何int值,值是布尔值——true和false,即如果键包含的int值在我们的集合里面,则会返回;如果键包含的int值不在我们的集合里面,则会返回false。

另:集合set里面的元素顺序不重要。换句话,写如下6行代码和7行代码,在后续使用时,行为是一致的。
在这里插入图片描述
再另:set里面的元素不能重复:下图编译运行没问题,但是如果访问s里面包含多少个元素(s.size)或遍历,返回的是4(因为100重复了)。
在这里插入图片描述

3.2.1.1 set是通过红黑树数据结构来实现的。

在这里插入图片描述
树:一种树(树是一个特殊的数据结构),这个树(二叉树)有一个树根11,有两个枝杈:11左边那一块(左子树)、11右边那一块(右子树);而子树也是树,左子树的树根是2,这个树根也有枝杈。。我们从树根开始走,最终会走到1、5、8这些结点(树的叶子)。
红黑树:树里面有些结点标记为红,有些结点标记为黑。对于set的使用,我们不需要关注结点为什么标记为红和黑,我们关注的是红黑树的树根和叶子上存储的都是元素,这些元素有两个特性:

  1. 从树根11开始,左子树所有的元素都会小于树根11;右子树里面所有元素的值都会大于树根的元素11;
  2. 接下来对树进行分层,11为第0层;2、14为第1层;1、7、15为第2层;5、8为第3层。然后对于任意一个树根,如树根11,它的左子树包含3层,右子树包含2层,左右子树最多相差一层(对于树根2,其左子树包含1层,右子树包含2层,也是相差1层)。

为什么红黑树会有这样的特点?

  1. 访问元素时,便于查找,即给定一个元素后,我们需要知道这个元素是否在我们的集合中。我们可以把这个元素跟树根的元素进行比较,如果我们要查找的元素等于树根,那么直接返回size就可以;如果小于树根,那么直接在左子树里查;如果大于树根,则在右子树里查。但无论是大于还是小于,我们都是把原本要查找的集合缩小到一半再进行查找(为什么会缩小一半?因为红黑树能够保证左子树和右子树相对平衡,从任何一个树根看,左边包含的元素和右边包含的元素基本上差不多)
  2. 由于红黑树,set还支持遍历。通常而言,容器都支持遍历(vector、list等),set也不例外。如下图6行定义一个set,然后for循环遍历:
    在这里插入图片描述
    在这里插入图片描述
    由上图可以,在遍历时,集合中的元素是按由小到大打印出来的。

为什么会这样?

实际上红黑树是一个树,它在遍历时会使用数据结构中经典的树的遍历算法——树的中序遍历:先遍历树的左子树,再遍历树根,最后再遍历右子树。对于上述set而言,它的左子树里面的所有元素都小于树根;右子树里面所有元素都会大于树根。因此遍历出来的结果一定是从小到大顺序遍历的。

3.2.1.2 通常来说,元素需要支持使用 < 比较大小

set是使用红黑树保存元素的。使用红黑树保存元素时,我们需要判断两两元素的大小,这样才知道如何把元素放到红黑树当中,换句话说,set里面的元素需要能够支持大小的比较,通常来说,元素需要支持使用 < 比较大小。

如下图6行的int支持<比较元素大小:在这里插入图片描述
但set里面不能放任意元素:
4行定义一个结构体Str;10行定义Str这样类型接口;下图代码合法:
在这里插入图片描述
但这样不行:(Str不支持使用<比较大小)
在这里插入图片描述
在这里插入图片描述
由上图,set接受3个模板参数:Key, Compare, Allocator。

  1. Key:10行中的Str
    在这里插入图片描述

  2. Allocator:用于分配内存,内存回收。通常std里面的标准模板都会引入Allocator进行内存管理。

  3. Compare:Compare使用std::less来进行比较,换句话说,如果上图10行改为int,这个s等价于set ,使用std::less去对比两个元素的关系,
    在这里插入图片描述
    在这里插入图片描述
    如对于上图:假设set里面包含11,接下来往集合里面插入一个元素2,调用Compare(2, 11),把2传入set,调用Compare本质上是调用std::less来进行比较。
    在这里插入图片描述
    那么std::less干了啥?
    std::less:传入了两个int型对象(lhs、rhs),然后对这两个对象进行比较,如果左值小于右值,返回true;左值大于右值,返回false。即上图11行调用std::less传入2和11后,会返回true,
    在这里插入图片描述
    回到下图,假设系统有11,使用std::less进行比较,如果比较结果是true,则把2插到树根11的左边,如果返回的比较结果是false,则把2插入树根11的右边,实际上是通过一个比较器来选择插入左边还是右边。

实际上我们也可以把这个行为进行改变。

改变形式1:10行的greater指,传入的两个元素(参数)进行比较时,第一个元素比第二个元素大,返回true。
在这里插入图片描述
遍历下图程序:返回的结果一定是从大到小打印的,原理和less来比较类似。
在这里插入图片描述
在这里插入图片描述

3.2.1.3 或者采用自定义的比较函数来引入大小关系

改变形式2:
下图代码非法,因为Str不支持比较。
在这里插入图片描述
但我们可以引入自定义比较器:
在这里插入图片描述
9行:比较器传入两个Str对象,返回一个bool值来对这两个Str对象比较大小;

16行:下图为set的构造函数:如果需要使用自定义时,需要传入一个初始化列表(下图蓝色)来初始化元素(即对于上图16行的{Str{}}),同时如果定义了Compare,将会给出Compare的对象comp:
在这里插入图片描述
再如:下图代码没问题,因为给了比较进去。
在这里插入图片描述

故以上,讨论了set中的比较关系。set本身是一个红黑树,有了这个红黑树之后,set中的所有元素都要引入这种比较的方法。我们可以使用缺省的方式(std::less)引入比较,我们也可以使用greater的方式引入比较,此外我们还能为自定义结构引入自定义函数进行元素大小比较。

3.2.1.4 插入元素: insert / emplace / emplace_hint
  1. insert
    下图程序合法:
    17行:构造了一个100,插入到s中
    在这里插入图片描述
  2. emplace
    S
    在讨论vector时,insert时先构造一个元素,然后需要通过拷贝移动的方式,把元素放到vector的buffer里面去。为了避免这种拷贝和移动,vector引入了emplace这个概念。set也一样,我们引入emplace后,可以避免一些拷贝和移动。

如:下图17行直接传入100(我们可以使用100来构造Str),s会使用这个100来构造Str的一个结点,这样直接构造出来就避免了拷贝移动。
在这里插入图片描述

  1. emplace_hint
    我们构造红黑树之后,插入一个元素,系统需确定出这个元素插入到哪里。系统怎么确定位置?一次次的和树中现有的元素进行比较,比较完之后才确定插入哪里。在这里插入图片描述
    hint相当于一个提示,通过一种方法来告诉set大约需要插入到哪里。这个提示是通过一个迭代器给出的。告诉系统大约需要插入到哪里,可以减少比较次数。

如我们要插入的是9,系统会先和11比较,发现比11小,再和2比较,再和7比较,再和8比较。

但是如果我们能通过一个迭代器告诉系统,我大约要插入到7所在的树支上,那么一开始这个9就会和7进行比较,再和8比较。那么就能大概确定出插入位置。

但emplace_hint有好处也有坏处:
使用emplace_hint,是通过一个迭代器来给出一个提示大约要插到哪里。
在这里插入图片描述
如果给的是错误提示,那么会增加系统性能负担:
在这里插入图片描述

3.2.1.5 删除元素: erase

在这里插入图片描述
erase大体上有两种形式。一种是传入一个key((3))。set是一种关联容器,它的key就是容器中包含的元素,传入一个key之后,就会在容器里找一下有没有这个元素,有的话就删除掉。

在这里插入图片描述
除此之外,也可以传入迭代器来进行删除((1)),如上上图18行,即我们在容器中查找100这个元素,将其删掉。
我们还可以这么写:
在这里插入图片描述
上图含义是删除容器里面的第一个元素。

如对于下图红黑树,上图18删除的是1(从小到大排序?)
在这里插入图片描述

3.2.1.6 访问元素: find / contains
  1. find
    如下图(2),传入一个Key,返回一个迭代器
    在这里插入图片描述
    返回的迭代器指向一个元素,这个元素里面的值实际上就是我们要查找的元素的值。如果没有查找到元素,就会返回end迭代器。
    在这里插入图片描述
    如下图,16、17行:由于17行的1包含在16行的集合s中,因此17行的ptr就是一个迭代器,指向s中的一个结点(红黑树的一个结点,这个结点里面包含的元素就是1),我们18行解引用ptr:
    在这里插入图片描述
    如果没有查找到元素,就会返回end迭代器:
    在这里插入图片描述
    在这里插入图片描述
    19行不会执行:因为100不在集合s里面,因此ptr返回的是标记了s.end所对应的迭代器。故执行完上图17行,在执行18行时不满足if语句条件。
    在这里插入图片描述
  2. contains
    如下图,传入一个Key之后,由Key返回一个bool值,来表示Key包不包含在容器中。
    在这里插入图片描述
    如下图:
    在这里插入图片描述
    在这里插入图片描述
    输出0即表示50不是容器中的元素。
    在这里插入图片描述
    在这里插入图片描述
    set不是像之前所讲的顺序容器那样通过[]来访问元素,而是通过find和contains来访问元素。
    一方面:关联容器的键并不是有序的整数序列,因此它需要通过特殊方式访问;
    另一方面:为什么顺序容器访问元素时没有通过find函数?如果在顺序容器中包含find,我们想查看vector中是否包含某个元素,如100,这件事实际上是很慢的一件事(vector可能很大),只能一个元素一个元素这样去比较。

但是set不一样,set本质是红黑树,通过红黑树就能实现相对比较快的查询。因此set提供find和contains接口。

3.2.1.7 修改元素: extract

顺序容器修改元素非常简单。假设a是一个vector,a[5] = 3;即可修改元素。但是set要修改元素,比较tricking,我们不能直接修改。

注意: set 迭代器所指向的对象是 const 的,不能通过其修改元素

如下:
在这里插入图片描述
在这里插入图片描述
但是不能像下面这样:无法编译。17行的s.begin是只读的。
在这里插入图片描述
为什么
s.begin是只读的?

set是使用红黑树进行构造的,s.begin指向的是树中的一系列结点。如果它不是只读的,如果能够对结点里面的元素进行改变的话,那么理论上可以改变成任意的值,但是改成某个值之后,就可能造成整个红黑树的结构性破坏。如,假设我们要把结点2改成100,这样就不满足11的左子树的元素比11小的原则了。
在这里插入图片描述
故c++的set有专门的修改元素的方法:extract

如果不考虑extract的话,想修改树里面的元素,我们可以这么改:假设要把2修改掉,我们可以先调用erase,把2删除掉;然后再insert,插入一个新的值。

但上述修改集合中的元素的方法性能不是最好的。为什么?首先这是红黑树,树里面每个结点都由一个数(这个数是结点里面包含的元素)表示(为了简化起见),但是这个结点里面并不仅仅包含这个元素的信息(还要包含它的左子树和右子树的指针;还要包含一些信息来进行树的遍历),如果我们erase之后,那么整个结点所对应的内存都会被释放掉。接下来insert时,需要构造一个新结点,把我们要插入的元素放进去,同时还要开辟空间保留指针和线索二叉树的信息,这样整个过程是比较慢的。

故set提供了extract方法。
如下图,extract传入的是一个迭代器,返回的是node_type(结点的类型),结点的类型里面除了包含元素之外,还包含结点的左右子树的指针和线索二叉树的信息:
在这里插入图片描述
我们可以调用node_type来修改结点的值:

构造了一个set——cont,其中包含1、2、3,
在这里插入图片描述
蓝色这一块可以理解为把cont打印出来:1,2,3
在这里插入图片描述
然后调用extrat(1),返回了一个结点nh,然后调用这个结点nh的value方法,把nh结点里面的值进行修改,变成4。接下来再打印cont,由于我们使用extract把1提取出来了,因此1不包含在set里面了,故打印出来的是2、3。然后如下图蓝色调用insert,把extract出来的结点nh通过move的方式变为右值传入cont中,insert就可以把这个结点再一次插入到我们的树里面。
在这里插入图片描述
在这里插入图片描述
实际上如果把上图extract改成erase,接下来insert时插入4,整个程序的行为还是一样,先打印出1 2 3,再打印出2 3,再打印出2 3 4,但性能略逊。

3.2.2 map

set的实现是基于红黑树,map的实现也是基于红黑树。但set只表示一个集合,但是map本质上是一个映射,我需要给定一个键,返回一个值。

3.2.2.1 树中的每个结点存储了std::pair这样类型的对象

下图每个长方形就是结点。每个结点里面包含2个数值,其中一个是用作键,一个是用作值。我们会使用键来构造红黑树的结构,如下图第一行那个结点的key是5,那么这个结点的左子树中的结点的键都比5小,右子树中的结点的键比5大(在构造红黑树时不考虑value的值)。
在这里插入图片描述
如:
6行:构造一个map,map是一个映射,在构造时需要给两个信息:一个是它的键的类型,一个是它的值的类型,如下图键的类型是int,值的类型是bool。
7行:遍历map容器中的所有元素。我们通过ptr作为m的迭代器来去访问m中的元素。但在set里面,我们可以使用*ptr把ptr指针指向的元素打印出来,但是在map里面不能让通过这样的解引用操作把指针所指向的元素打印出来。ptr代表一个迭代器,指向结点,结点中包含2个元素,实际上树中的每个结点存储了std::pair这样类型的对象

9行:可以得到一个p,p的类型是std::pair<const int, bool>(这是由于下图,我们可知,第3行的Key对应的是key type,T对应的是mapped type),下图6行知,我们的key type是int,mapped type是bool,在求std::pair<const int, bool>时,会把key type加个const。
在这里插入图片描述
在这里插入图片描述
实际上,pair的定义中,有两个成员对象,一个first,一个second。first指向的是上图9行的pair中的第一个元素const int,second指向第二个元素bool。
在这里插入图片描述
那么我们可以这样遍历p:
在这里插入图片描述
在这里插入图片描述
1 3 4 对应key,1 1 0对应的是bool值。

由于map是采用红黑树保存,同时构造红黑树时,本质上是使用键(1 3 4 )来作为红黑树的元素比较的线索,因此打印时,1 3 4会由小到大排序。

上述的遍历方案需要显式写出begin,end,我们还可以使用如下遍历方式:
在这里插入图片描述
在这里插入图片描述
这样本质上,系统会自动将7行转成begin,end的形式。

我们之前还讨论过基于绑定的复制方法。如,一个函数返回一个结构体,或者返回一个pair,那么在一些情况下,我们可以写出如下代码:
4行:定义一个fun函数
13行:调用fun函数
在这里插入图片描述
13行我们也可以进行函数绑定:这样即把fun函数返回的两个值分别赋到res1和res2里面。
在这里插入图片描述
在c++ insight中,我们可以看到,左9行的p对应右12行。本质上可以认为,下图蓝色的东西是用一个pair来对p进行赋值。
在这里插入图片描述
在刚才的程序里面,实际上也是用4行的pair来对13行的res1和res2进行赋值:
在这里插入图片描述
既然我们上图可以使用绑定的方式来获取fun函数的返回值,那么在14行,也可以使用绑定的方式获取m中的每个元素。即我们还可以将上图程序改为:
在这里插入图片描述
在insight里可以看到,上图代码的行为是使用pair构造了一个临时对象_operator8(12行),然后使用k获取临时对象的第一个元素(13行),使用v获取临时对象的第二个元素(14行):
在这里插入图片描述
但是写成上上图8行那样,有个问题:k,v是使用拷贝的方式获取的结果,在一些情况下对int和bool值(6行)来讲,这种拷贝没问题。但是在一些情况下,我们希望能够对map里面的值进行修改或者说发现拷贝很慢,我们可以在auto后面加一个&,这样k和v都相当于引用,这样就能避免拷贝
在这里插入图片描述
在这里插入图片描述
以上是关于map的简单介绍,map是一个红黑树,树中的每个结点存储了std::pair这样类型的对象,既然是一个pair,那么就有一个first,一个second,first用来表示key,second用来表示value。

其中在构造红黑树时,要往里插入任何元素时,我们需要pair之间比较大小,但是在进行比较大小时,我们不需要考虑value,我们只针对key来比较大小,根据比较大小的结果插入红黑树的结点。

故在map中,map的键是需要支持比较大小的。

3.2.2.2 缺省情况下,map的键 (pair.first) 需要支持使用 < 比较大小

如下图6行的int(键)支持比较大小;
4行:自定义一个结构体(结构体不支持比较大小),故8行中的值要换成Str{}
在这里插入图片描述
上述代码可以编译运行。因为虽然Str不支持比较大小,但是int支持比较大小,8行的{3, Str{}},{4, Str{}},{1, Str{}}是pair,我们只要求pair里面的第一个元素支持大小比较即可。

但如果换成下图代码:不行,Str(键)不支持大小比较
在这里插入图片描述
在这里插入图片描述
如果我们确实需要构造这样的map,我们需要自定义一个Compare(比较大小的工具),然后放入上上图8行的map的第3个模板参数里面,这样即可使用map支持使用自定义的数据结构来进行大小的比较。

3.2.2.3 采用自定义的比较函数来引入大小关系

3.2.2.4 插入和删除元素

map的元素插入和删除操作类似set。
如下图,插入insert操作,传入的是value_type,
在这里插入图片描述
value_type本质上对应的是std::pair:
在这里插入图片描述
为什么?实际上,map使用红黑树来保存数据,其中每一个数据都是一个pair,因此在插入元素时:下图代码合法
在这里插入图片描述
上图代码会隐式转换为:
在这里插入图片描述
删除map中的元素也是类似:
下图(1)iterator pos:我们要删除第几个元素
(3)中的删除元素方法,只需要给出key即可
在这里插入图片描述
如通过下图方式插入一个元素3:
在这里插入图片描述
接下来要把这个元素删除:10行调用erase删除元素3
在这里插入图片描述

3.2.2.5 访问元素: find / contains / [] / at

  1. find
    (1):给一个Key(键)返回一个iterator迭代器。即如果我们能找到这样的键,iterator就指向一个合法的结点,接下来可以通过iterator的first和second获取map对于的键和值。
    在这里插入图片描述
    如下图:
    m中有3(键),故可以通过fist和second把ptr中的元素打出来:
    在这里插入图片描述
    3是键,1对应bool值true:
    在这里插入图片描述
    如find没有找到3,和set一样,会返回map.end迭代器,表示什么都没有找到。
  2. contains
    (1):也是传入一个Key,返回一个bool值表示是否找到Key所对应的结点
    在这里插入图片描述
  3. 使用[]访问元素
    对于map来讲,它和vector很像,都是给定键来去访问容器中的值。只不过vector的键是从0开始的一连串的数字序列;map中是自定义的键。由于二者相似,因此本质上来讲,我们可以使用[]来访问容器中的元素。
    在这里插入图片描述
    打印出1,1对应的是true(9行)。
    在这里插入图片描述
  4. 使用at访问元素
    使用[],我们只是进行内存移动,我们通过计算内存,然后访问元素。但是由于vector里面包含的元素可能是有限的,比如vector中包含3个元素,我们写[100],那么会造成内存访问越界。
    但是如果使用at,那么在内存越界时,系统会抛出异常。
    map类似,如果我们是使用at,但是我们为at提供的参数并不包含在map当前的树里面时,at会抛出异常。
    如下图程序没问题:
    在这里插入图片描述
    还是能打印出true所对应的整数值:
    在这里插入图片描述
    但是如果m.at(100),那么系统直接报错:
    在这里插入图片描述
    在这里插入图片描述
    我们再回来看看[]。
    对于vector来讲,如果我们使用[]来读和写,同时在[]里面传入一个非法的索引值,那么会造成在读和写时,内存访问越界。
    但是map是一个树,如果传入一个非法的索引值,本质上不会产生所谓的内存越界,因为树的结构就是一个一个结点构成的,那么构成这一系列结点后,如下图,结点中的key为5,3,7,2,4,那么如果[100],100不在当前树的结点key里面,那[]的行为是往树里面插入一个新结点,这个新结点的key就是100,我们下图来看看operator[]的说明:
    在这里插入图片描述
    1):如果结点中key不存在,那么就会插入value_type(key, T())value_type(key, T())这个东西是value_type,value_type是使用key来访问,如使用100来访问,那么100就作为pair的first,然后使用T()作为pair的second,T本质上是值初始化(如果是类类型,会使用缺省构造函数来去构造T(),otherwise会使用0初始化T())。
    如:
    在这里插入图片描述
    在这里插入图片描述
    但是11行:m[100]:在m这个树里面结点找,有没有结点的键是100的,如果没有,那么会在m(下图9行)构造结点(该结点键所对应的值是100,所对应放入值就是T(),而T()会使用0初始化,下图9行知T对应值类型int),换句话说,如果11行写m[100],系统会打印出int(),因为我们在m中插入一个结点,这个结点的键是100,值是int(),接下来会把这个值返回。int()这个值是使用0初始化的额,换句话说会打印出int型整数0。
    在这里插入图片描述
    在这里插入图片描述

3.2.2.6 注意map迭代器所指向的对象是 std::pair ,其键是 const 类型

如下图,map的value_type很多时候在写std::pair时,都是写的const Key(即键的类型是const),值类型T不去管。
在这里插入图片描述
这是因为我们不希望系统使用ptr来改变结点中键的值,这和之前讨论的set类似,如果set中键的值发生改变,那么这个红黑树的行为就不可预期了。与之类似在m里面,我们也不希望通过迭代器去改变结点中键值,故键是const类型。但是反过来讲,我们可以使用迭代器改变value的值,因为改变value的值不会改变树的结构(树在构造时只会使用键进行判断)。
在这里插入图片描述

3.2.2.7 [] 操作不能用于常量对象

如下图4行,定义一个fun函数,传入一个map。fun函数接受一个map类型的对象m,作为参数
13行:调用fun函数
但在fun函数里面,我们不能如6行那样写:(会报错)
在这里插入图片描述
因为[]的行为是:如果键(3)存在,那么返回键所对应的值的对象,如果键不存在,会往m中插入一个相应的对象。而键存不存在,这是运行期行为,但是在解析下图6行代码时,是在编译期解析,因为4行,传入的是const的引用,因此系统不会让m[3]这样的代码编译通过,故在编译期不能保证3这个键是一定存在的,换句话说在运行期时键3可能不存在,如果编译期告诉你代码不能通过,那么相当于通过这样的方式,就改变了map里面的内容。那么这一点就和我们之前声明的const相违背,故[]操作不能用于常量对象(因为4行声明了常量引用,故我们不能使用[]操作)。

那么我们想获取m中键3所对应的元素,我们可以如下图这样:
9行:使用ptr->second来获取键3所对应的值:
在这里插入图片描述
在这里插入图片描述

3.3 multiset / multimap

3.3.1 multiset / multimap与 set / map 类似,但允许重复键

下图:
6行:使用1 3 1来初始化set容器,之前说过set里面不允许重复的键,因此会把两个1视为1个 ,在遍历s中的元素时只会打印1个1 3:
在这里插入图片描述
在这里插入图片描述
但是如果换为multiset:(允许重复键)
在这里插入图片描述
在这里插入图片描述
multiset / multimap也是使用红黑树实现的,那么会把容器中的元素按键进行排序,放入树中(键小的放左边,键大的放右边)。故打印出的元素是从小到大排序。

3.7.2 元素访问

multiset / multimap允许重复键,而我们在进行元素访问时,通常都要给定一个键来访问容器中包含的元素,set和map的键不能产生重复,相应来讲,在进行元素访问时,结果只有得到这个元素或每查找到这个元素;但是对于multiset / multimap允许重复键,因此在查找时,元素访问的接口的行为可能发生改变,

3.7.2.1 find 返回首个查找到的元素

传入一个键,返回一个迭代器,这个迭代器如果指向的不是set或map中的end(结尾处的迭代器),那么即表示查找到元素。

即对于set和map,调用find函数查找元素,只有两个结果:要么查找到,要么没找到。

但对于multiset,调用find可能查找到一个键,能够查找到元素,但是这个元素可能有多个,find函数能够确保返回第一个查找到的元素。如:
7行:ptr返回的是s中首个1;
8行:++ptr后,ptr返回的是s中的第2个1。
在这里插入图片描述

3.7.2.2 count 返回元素个数
  1. set
    7行:键是1,返回容器中包含元素1的结点的个数。而set中的结点的键不能重复,因此count返回的值为1(和之前讨论的contain没有本质区别)
    在这里插入图片描述
  2. multiset / multimap
    7行返回值为2
    在这里插入图片描述
    在这里插入图片描述
3.7.2.3 lower_bound / upper_bound / equal_range 返回查找到的区间

对于multiset,使用for循环遍历(7行)
在这里插入图片描述
在这里插入图片描述
相同的键相连打印出来。相当于相邻的1构成一个区间,3又构成另一个区间。

实际上,使用lower_bound / upper_bound / equal_range就是查找区间。

  1. lower_bound / upper_bound
    如下图,b、e是两个有效的迭代器,b和e构成了一个区间:
    7行、8行:查找了键为1的若干元素的区间
    在这里插入图片描述
    在这里插入图片描述
    再如 :在s中找取值为3的区间
    在这里插入图片描述
    在这里插入图片描述
    再如:
    在这里插入图片描述
    在这里插入图片描述
    上图7、8行所确定的区间包含0个元素,故什么都不会打印出来。
    但是使用lower_bound / upper_bound 来确定区间,要写两行代码(如上程序的7、8行),比较麻烦,我们可以使用equal_range简化。
  2. equal_range
    在这里插入图片描述
    (1):equal_range返回一对迭代器,迭代器的第一个元素对应区间的开头;迭代器的第二个元素对应区间的结尾。因此:
    这个东西就不叫
    既然返回的是一个pair,那么我们还可以通过绑定的方法,把pair在一行中进行解析:
    在这里插入图片描述
    在这里插入图片描述

3.3 unordered_xxx 底层使用 hash 表实现

unordered:无排序

红黑树其实也是有排序,但是unordered_xxx没有顺序,unordered_xxx是通过哈希表来实现快速查找。

3.8 unordered_set / unordered_map / unordered_multiset / unordered_multimap

  1. unordered_set
    如往下图插入某一个键(一个整数或字符串),我们需要通过哈希函数的方法,把这个键转换成相应的整数值,对这个哈希函数的最低级要求是输入同样的键输出同样的整数值,有了整数值之后,

如下图,有一个数组buckets vector,里面每个东西成为bucket,每个bucket都是指针,里面连着一个list,称为bucket list,list里面存储的是我们要插入的键,那么buckets vector有多长?这是unordered_set / unordered_map底层所要维护的。

如一个,经过哈希函数转换成一个整数值(假设为6),然后有一系列bucket vector(数组),系统知道这个数组有多长,接下来首先拿转换后的整数值和数组的长度相除取余,如转换出来的整数值为6,6和bucket的长度相除取余之后,得到的结果是6,这个6的含义是,我要放到buckets vector等于6的这样的bucket里面,bucket表示的是篮子,一个篮子可能放多个键,因此要放到下图第6个bucket里面(从0开始计数),而每个bucket都有一个链表,那么我们往链表中添加这个
在这里插入图片描述
我们可以看到,上图,第2个bucket里面对应3个元素,就是依次插入的结果。

以上就是典型的哈希表的实现,通过哈希表可以实现unordered_set / unordered_map / unordered_multiset / unordered_multimap这样类似的逻辑。

但无论是通过红黑树实现还是通过哈希表来实现,如set中的元素的顺序不太重要,unordered_set里面的元素也没有什么顺序,因为返回的哈希的值,和buckets vector的长度相除取余之后,可能哈希值小的东西排在后面,也可能哈希值大的东西排在前面。

此外,unordered_set/unordered_map里面的元素(键)不会出现重复,能够快速查找;unordered_multiset / unordered_multimap允许键出现重复。

那么我们怎么判断键是否会出现重复?我们需要引入一个判等操作:给定两个元素之后,可以通过判等操作来判定这两个元素是否相等,如果在查找时二者相等,则认为找到了该元素;如果在插入时二者相等,则不进行元素插入。

故unordered_set/unordered_map的键支持的不是<或比较的操作了,它需要支持两个操作:

  1. 我们有一个相应的函数能够为它转换为哈希值
  2. 支持键的判等
    只要有这两个操作,我们就可以构造相应的哈希表。

如下:
6行:定义了unordered_set容器;
7行:遍历s中的元素
在这里插入图片描述
在这里插入图片描述
以上,打印出来的值没有顺序,但1(s中有两个1)只记录了一次。

为什么能使用int来实例化unordered_set?实际上是c++为int提供了转换成哈希值的函数,同时int支持判等操作。

3.8.1 unordered_set和unordered_map与 set / map 相比查找性能更好

unordered_set和unordered_map与 set / map相比,本质上都是要提供一个容器,给定键能够实现快速查找。但是unordered_set和unordered_map在一些情况下,查找性能比set / map查找性能更好。

但是set、map基本上是使用红黑树来实现,红黑树能够缩小一半元素再进行查找。而使用哈希查找,查找的元素范围能进一步缩小。

如在unordered_set,我们首先转换成哈希函数,这个转换过程比较快,转换之后就能一次性查找bucket,在bucket vector里面很快就能定位出到底应该在哪个链表里面查询该元素。

同时,unordered_set和unordered_map在设计时能够确保bucket里面的元素相对比较少,很多情况下bucket里面的元素只有1个,此时直接在bucket里面查一下这个链表,对这个链表可能只需遍历一到两次,看看这个链表里面是否有我们需要查找的元素。通常而言,哈希表这样的查找的复杂度是o(1)。

3.8.2 但与 set / map 相比,unordered_set和unordered_map插入操作一些情况下会慢

实际上我们要快速实现查找,有个基本前提是,如下图红圈,我们的bucket所关联到的链表的长度不能很长,因为如果链表里面包含n个元素,那么在查找到bucket之后,我们需要一次对红圈里面的n个元素进行比较(使用判等操作来进行比较是否查到),实际上判等这一步是比较慢的。
在这里插入图片描述
如果在不断插入元素时,可能会导致在某些情况下,某个bucket里面包含的元素个数非常多了,此时unordered_set和unordered_map会重新哈希,把buckets vector扩充,得到一个新的buckets vector,再把原buckets vector所有元素重新计算哈希,重新放到新的扩充后的buckets vector里面,然后把原buckets vector释放掉,这样的过程是相对比较慢的。

一般在读取操作时,unordered_set和unordered_map性能更好。

3.8.3 其键需要支持转换为 hash 值和判等操作

  1. 3.8.3.1 转换为 hash 值
    unordered_set的模板定义包含Key、Hash(缺省情况下使用的是std::hash进行定义)、KeyEqual(进行判等操作)、Allocator(分配内存存储容器元素)
    在这里插入图片描述
    unordered_map也类似。
  2. 判等

3.8.4 除 == , != 外,不支持容器级的关系运算

set能提供容器级的比较操作:
7、8行:先对容器内的元素排序,再逐一元素比较
在这里插入图片描述
即s1<s2。
但是unordered_set不支持容器级的比较。因为我们并不要求键支持小于大于的比较操作。

3.8.4.1 但 ==, != 速度较慢

unordered_set支持 ==, != 操作:
在这里插入图片描述
在这里插入图片描述

3.8.5 自定义 hash 与判等函数

4行的Str是不能作为unordered_set的键,因为没有提供方法将结构体Str转换为哈希值,也没有提供相应的判等函数。
在这里插入图片描述
在这里插入图片描述
我们可以提供一个自定义的转换成哈希值的方法:
9行:哈希值一般是整数,故9行的MyHash方法即将将x转换为整数
14行:提供判等函数MyEqual,
在这里插入图片描述
在这里插入图片描述
在自定义了哈希和判等函数后,我们可以使用下图(1)来实现unordered_set
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
简单化:
在这里插入图片描述

4 适配器与生成器

针对现有的一些容器进行一些调整,使得产生一些新的接口,结果。

4.1 类型适配器

4.1.1 basic_string_view ( C++17 )

4.1.1.1 可以基于 std::string , C 字符串,迭代器构造

basic_string_view是一个类模板,basic_string是一个字符串容器,也是一个模板,如果我们使用char来实例化这个模板,则得到我们经常使用的string(c++的字符串类型)。与之类似,basic_string_view也是一个模板,我们可以使用不同类型来进行实例化,如下图是已经实例化好的类型:(如果使用char来实例化,就得到string_view。。。)
在这里插入图片描述
string_view和string有何区别?
string_view是一种类型适配器,把不同类型的字符串统一到相同类型的字符串上处理。
下图7行:判断str是否为空
在这里插入图片描述
在这里插入图片描述
上图15行是c形式的字符串,类型是char6;16行是c++的字符串,调用fun函数时,无论15、16行实参是什么类型,都可以适配成5行这样的类型:
在这里插入图片描述
另,上图5行的形参接收的不是一个引用,而是string_view这样的对象。string_view是一个非常cheap的数据结构:
在这里插入图片描述
在这里插入图片描述
string_view大小是16,比较小,为什么是16?实际上string_view只包含2个信息,这两个信息分别标识了字符串开头的迭代器和结尾的位置。string_view中的view不包含原始的string,只是原始的string的视图,不拥有原始string的所有权。string_view只是在原始的字符串基础上开辟了一个窗口,然后通过某种形式让fun看到这个窗口。故在传递时,不要传递string_view的引用,因为解引用时更耗时间。我们直接传递类型的对象就可以了。

string_view还可以使用迭代器构造:(19行)
在这里插入图片描述
在这里插入图片描述

4.1.1.2 提供成本较低的操作接口

在这里插入图片描述

4.1.1.3 不可进行写操作

basic_string_view一般用作函数的形参,注意不要用临时变量构造basic_string_view和返回,这是非常危险的,如下图7行的s是局部的临时变量(对象),在fun函数结束时,s就会被销毁。basic_string_view里面存的不是s副本,而是存储了表示当前s的开头和结尾的指针。如果我们在后续代码调用fun函数获取string_view,对string_view进行一系列操作时,由于s已经被销毁了,因此通过string_view读出来的东西是未定义的。
在这里插入图片描述
我们可以这么写:
7行:从第0个元素开始取,作为字符串开头,字符串一共包含3个字符。
在这里插入图片描述
在这里插入图片描述
res能打印出123,这是因为string_view里面记载的就是某个字符串的指针,12行的s作为一个字符串传递给5行的fun时,input里面实际上包含了s的指针(相当于两个迭代器,一个指向s的开头,一个指向s的结尾的下一个),接下来7行调用input.substr时,返回了一个string_view,这个string_view也包含两个迭代器,但是这两个迭代器实际上也指向了s,只要s没有被销毁掉,我们使用res就是非常安全的。故上图代码合法。

但是如果将上图代码改为下图,则不合法:(调用12行语句时系统会调用fun函数,在调用fun函数过程中,12行的"12345"是一直存在的,但执行到13行时,临时对象std::string(“12345”)就已经被销毁了,此时res无效)
在这里插入图片描述
故通过不要使用string_view作为返回类型。

4.1.2 span ( C++20 )

4.1.2.1 可基于 C 数组、 array 等构造

在这里插入图片描述
像vector、array内部元素连续,我们可以使用span对其类型进行封装,但是list、set、map(里面的元素非连续)则不能使用span来进行类型封装。
8行:遍历input
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
或者将上图18行改为:
在这里插入图片描述
在这里插入图片描述
实际上,span可以使用很多方法来进行构造:
(4):使用数组[N]来构造
(3):使用一对迭代器构造
在这里插入图片描述

4.1.2.2 可读写

span和string_view很像,它也是轻量级的。span内部也只是保留两个指针,分别指向序列中的第一个元素和最后一个元素。

但是span支持读和写操作(string_view只读)。
在这里插入图片描述
在这里插入图片描述

4.2 接口适配器

对底层容器的接口进行修改。

4.2.1stack / queue / priority_queue对底层序列容器进行封装,对外展现栈、队列与优先级队列的接口

  1. stack(后进先出的数据结构)
    下图,模板参数T代表stack里面所存储的数据类型;Container有个缺省的值:std::deque,Container是stack的底层容器。stack本身是一个接口适配器,适配的是底层容器——将底层容器的接口改变。这个底层容器就是Container。
    在这里插入图片描述
    下图10行,p可以压栈和出栈int类型的对象,但是这个int类型的对象本质上会保存在底层容器Container里面:
    在这里插入图片描述
    在这里插入图片描述
  2. queue(队列:后进后出)
    在这里插入图片描述
    在这里插入图片描述

4.2.2 priority_queue 在使用时其内部包含的元素需要支持比较操作

priority_queue(优先级队列)
如下图:模板参数有一个T,有一个底层容器Comtainer,有一个比较的方法Compare(缺省是less:使用<进行比较)。
在这里插入图片描述
优先级队列往优先级队列插入元素,取出元素时,默认从大到小出队列,其内部包含的元素需要支持比较操作。
在这里插入图片描述
在这里插入图片描述
上图的output的第二行是使用了greater来比较,返回的是从小到大排序的值。

4.3 数值适配器 (c++20) : std::ranges::XXX_view, std::ranges::views::XXX, std::views::XXX

适配器不仅像之前所讨论的那样,能提供公共的类型,提供底层的接口,我们还能对其中包含的元素进行修改。
数值适配器从C++20开始引入,主要在下面三个命名空间中:

  1. std::ranges::XXX_view
  2. std::ranges::views::XXX
  3. std::views::XXX
    在这里插入图片描述

4.3.1 可以将一个输入区间中的值变换后输出

在这里插入图片描述
为什么数值适配器能改变容器中的元素?如何做到的?
下图:
6行:使用vector来作为一个容器
9行:打印容器中的元素
在这里插入图片描述
在这里插入图片描述
接下来使用std::ranges::XXX_view:
5行:给定一个数i,判断i是不是偶数
13行:filter_view需要接收两个参数:原有容器v,预测器(如isEven)。系统会将v中的每个元素都调用isEven,如果调用isEven返回true,才会把该元素保留下来,然后对filter_view的结果进行遍历
在这里插入图片描述
在这里插入图片描述
再如:
在这里插入图片描述
在这里插入图片描述

#include <ranges>
#include <iostream>
 
int main()
{
    auto const ints = {0,1,2,3,4,5};
    auto even = [](int i) { return 0 == i % 2; };
    auto square = [](int i) { return i * i; };
 
    // "pipe" syntax of composing the views:
    for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
        std::cout << i << ' ';
    }
 
    std::cout << '\n';
 
    // a traditional "functional" composing syntax:
    for (int i : std::views::transform(std::views::filter(ints, even), square)) {
        std::cout << i << ' ';
    }

	// 输出均为 0 4 16
}

4.3.2 数值适配器可以组合,引入复杂的数值适配逻辑

4.4 生成器 (c++20)

4.4.1 std::ranges::itoa_view, std::ranges::views::itoa, std::views::itoa

4.4.2 可以在运行期生成无限长或有限长的数值序列

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cashapxxx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值