STL之顺序容器

        容器(container)是容纳、包含一组元素的对象。容器类库中包括7种基本容器:向量(vector)、双端队列(deque)、列表(list)、集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)。这7种容器可以划分为两种基本类型:顺序容器(sequence container)和关联容器(associative container)。顺序容器将一组具有相同类型的元素以严格的线性形式组织起来,向量、双端队列和列表就属于这一种。关联容器具有根据一组索引来快速提取元素的能力,集合和映射就是这种。

 

设S表示一种容器类型(例如向量vector<int>),s1和s2都是S类型的实例,容器支持的基本功能如下:

S s1                   容器都有一个默认构造函数,用于构造一个没有任何元素的空容器。

s1 op s2             对两个容器之间的元素按字典顺序进行比较,op可取值为:==、!=、<、>、<=、>=

s1.begin()          返回指向s1第一个元素的迭代器

s1.end()             返回指向s1最后一个元素的下一个位置的迭代器

s1.clear()           将容器s1里的内容清空

s1.empty()         返回一个布尔值,表示s1容器是否为空

s1.size()            返回s1中元素的个数

s1.swap(s2)      将s1与s2中的元素交换

 

与类型为S的容器相关的迭代器类型为:

S:: iterator                表示与S相关的普通迭代器类型,迭代器指向的元素类型为T

S:: const_iterator     表示与 S相关的常迭代器类型,迭代器指向元素的类型为const T,因此只能通过迭代器读取元素,不能通过迭代器改写元素

       

        容器作为一种STL的概念,基于容器中的元素的组织方式,容器又具有关联容器和顺序容器两个子概念。另一方面,按照与容器所关联的迭代器类型划分,容器又具有“可逆容器”这一子概念,而“可逆容器”又具有“随机访问容器”的子概念。使用一般容器的begin()和end()函数所得到的迭代器都是前向迭代器,也就是说可以对容器的元素进行单向的遍历。而可逆容器所提供的迭代器则是双向迭代器,可以对容器的元素进行双向的遍历。

        对一个可逆容器进行逆向遍历时,可以通过对其迭代器使用“--”运算来操作,但有时这样做并不是很方便,因为STL算法的输入都是用正向区间来表示的。为此,STL为每个可逆容器都提供了逆向迭代器,逆向迭代器都可以通过下面的函数得到:

s1.rbegin()         得到指向容器的最后一个元素的逆向迭代器

s1.rend()            得到指向容器的第一个元素的前一个位置的逆向迭代器

逆向迭代器的类型名的表示方法为:

S:: reverse_iterator                表示与S相关的普通迭代器类型,迭代器指向的元素类型为T

S:: const_reverse_iterator     表示与 S相关的常迭代器类型,迭代器指向元素的类型为const T,因此只能通过迭代器读取元素,不能通过迭代器改写元素

逆向迭代器实际上是普通迭代器的适配器,逆向迭代器的“++”运算被映射为普通迭代器的“--”,逆向迭代器的“--”被映射为普通迭代器的“++”。例如,如果希望把一个整型向量容器s1的内容逆向输出到标准输出,可以这样实现:

copy(s1.rbegin(),s1.rend(),ostream_iterator<int> (cout ," "));

随机访问容器所提供的迭代器是随机访问迭代器,支持对容器的元素进行随机访问。使用随机访问容器,可以直接通过一个整数来访问容器中的指定元素:s1[n]来获得容器中第n个元素,等价于s1.begin()[n]。

 

各容器的的头文件和所属概念:

容器名意义头文件所属概念
vector向量<vector>随机访问容器,顺序容器
deque双端队列<deque>随机访问容器,顺序容器
list列表<list>可逆容器,顺序容器
set集合<set>可逆容器,关联容器
multiset多重集合<set>可逆容器,关联容器
map映射<map>可逆容器,关联容器
multimap多重映射<map>可逆容器,关联容器

顺序容器与其基本功能

        STL中的顺序容器包括向量vector、双端队列deque和列表list,它们在逻辑上可看作是一个长度可扩展的数组,容器中的元素都线性排列。程序员可随意决定每个元素在容器中的位置,也可以随时向指定位置插入新的元素和删除已有的元素。每种类型的容器都是一个类模板,都具有一个模板参数,表示容器的元素类型。

       我们用S表示容器类型名,用s表示S的实例对象,用T表示S容器的元素类型,用t表示T类型的一个实例对象,用n表示一个整型数据,用p1和p2表示指向s中的元素的迭代器,用q1和q2表示任何指向T类型元素的输入迭代器。据此,来介绍一下顺序容器的基本功能。

(1)构造函数

         顺序容器除了具有默认构造函数之外,还可以使用给定的元素构造,也可以使用已有的迭代器的区间所表示的序列来构造,如:

S s(n,t)          构造一个有n个t元素的容器实例s

S s(n)            构造一个有n个元素的容器实例s,每个元素的类型都是T

S s(q1,q2)     用[q1,q2)区间内的数据作为s的元素来构造s

(2)赋值函数

         可以使用assign()函数将指定的元素赋值给顺序容器,顺序容器中原先的元素会被清除,赋值函数的3种形式是与构造函数一一对应的。

s.assign(n,t)           赋值后的容器由n个t元素构成

s.assign(n)             赋值后的容器有由个元素构成,每个元素类型都是T

s.assign(q1,q2)      赋值后的容器的元素为区间[q1,q2)的所有数据

(3)元素的插入

         向顺序容器中可以一次插入一个或多个指定元素,也可以将一个迭代器区间内所表示的序列插入。插入时需要通过一个指向当前容器元素的迭代器来指示插入的位置。

s.insert(p1,t)                     在 s容器中p1所指向的位置插入一个新的元素t,插入后的元素夹在p1和p1-1所指向的元素之间,该函数会返回一个迭代器指向新插入的元素

s.insert(p1,n,t)                  在s容器中p1所指向的位置插入n个新的元素t,插入后的元素夹在p1和p1-1所指向的元素之间,该函数没有返回值

s.insert(p1,q1,q2)             在s容器中p1所指向的位置顺序插入区间[q1,q2)内的数据,插入后的元素夹在p1和p1-1所指向的元素之间

(4)元素的删除

         使用erase()函数可以从容器中删除指定的元素或清空容器。删除指定元素时需要通过指向当前容器元素的迭代器来指示被删除元素的位置或区间。

s.erase(p1)           删除s容器中p1所指向的元素,返回被删除元素的下一个元素的迭代器

s.erase(p1,p2)      删除s容器中[p1,p2)区间内的所有元素,返回最后一个被删除的元素的下一个元素的迭代器,即删除前p2所指向的迭代器

(5)改变容器的大小

         通过resize()函数来改变容器的大小:

s.resize(n)        将容器的大小改变为n,如果原有的元素个数大于n,则容器末尾多余的元素会被删除,如果原有的元素个数小于n,则在容器末尾会用T()填充。

(6)首尾元素的直接访问

         可以使用顺序容器的成员函数来快速的访问容器的首尾元素:

s.front()      获得容器首元素的引用

s.back()     获得容器尾元素的引用

(7)在容器尾部插入、删除元素

         虽然insert()函数和erase()函数可以在任意位置插入和删除元素,但是由于在顺序容器中尾部插入、删除元素的额操作更为常用,因此STL提供了更加便捷的成员函数来供尾插和尾删。

s.push_back(t)        向容器尾部插入元素t

s.pop_back()           将容器尾部的元素删除

(8)在容器头部插入、删除元素

         列表list和双端队列deque这两个容器支持高效地在容器头部插入新的元素或删除容器头部地元素,但是向量vector不支持。支持这一操作的容器为“前插顺序容器”。

s.push_front(t)        向容器头部插入元素t

s.pop_front()           将容器头部的元素删除

#include <deque>
#include <list>
#include <iterator>
#include <iostream>
using namespace std;
template<class T>
void Show(char* msg, const T& s)
{
    cout << msg;
    ostream_iterator<int> output(cout, " ");
    copy(s.begin(), s.end(), output);
    cout << endl;
}
int main()
{
    deque<int> dq;
    int x;
    for (int i = 0; i < 10; i++)
    {
	cin >> x;
	dq.push_front(x);		//头插
    }
    Show("双端队列输出:", dq);

    //用双端队列dq中的元素逆序构建列表ls
    list<int> ls(dq.rbegin(), dq.rend());
    Show("列表输出:",ls);

    //将列表ls中相邻的两个元素交换
    list<int>::iterator iter = ls.begin();
    while (iter != ls.end())
    {
	int e = *iter;			//得到第一个元素
	iter = ls.erase(iter);	        //将该元素删除,得到指向下一个元素的迭代器
	ls.insert(++iter, e);	        //将删除的元素插入到下一个位置
    }
    Show("列表输出:", ls);

    //用列表容器ls中的元素给dq赋值
    dq.assign(ls.begin(), ls.end());
    Show("双端队列输出:", dq);

    return 0;
}

 该程序首先创建了一个双端队列容器dq,然后从标准输入流读入了10个整数,分别把它们以头插的方式插入到dq容器中,因此输出dq中元素的时候发现顺序与输入的顺序相反。然后,使用dq的逆向迭代器rbegin和rend构造列表容器ls,输出ls发现顺序与输入数据的顺序相同。然后使用迭代器在ls容器中遍历,对每两个元素分别执行删除和插入操作,最后达到的效果就是将ls容器中的元素两两交换。最后再使用顺序容器的assign成员给dq赋值。运行结果如下:

三种顺序容器的特性

(1)向量(vector)

        向量容器是一种支持高效的随机访问和高效向尾部加入新元素的容器。向量容器一般实现为一个动态分配的数组,向量中的元素连续地存放在这个数组中,因此对向量容器进行随机访问时具有和动态访问数组几乎一样的效率。

        向量容器在每次扩展空间时,实际分配的空间一般大于所需的空间。另一方面,将已有元素从向量容器中删除时,多出的闲置空间并不会被释放,因为再插入新的元素时可能会重新占用这些空间。因此,向量容器对象已分配的空间所能容纳的元素个数常常会大于容器中实际元素的个数。于是,称已分配的空间所能容纳的元素个数称为容器的容量,容器中实际元素的个数叫做容器的大小。获得容器的大小可以通过size()函数,通过capacity()函数来获得容器的容量。s.reserve(n),表示若当前的容量大于或等于n,什么也不做,否则那就扩大s的容量,使得s的容量不小于n。所以,在准备向向量容器中插入大量数据之前,如果能够粗略的估计出插入元素之后向量容器的大小,那么就可以在插入前使用reserve()函数来确保这部分空间被分配,避免在插入过程中多次重新分配空间,提高效率。

向量容器中插入新元素时,插入位置之后的元素都要被顺序向后移动,因此在总体上向量容器的插入操作的效率并不高。插入位置越靠前,执行插入所需的时间就越多,但是在向量容器尾部插入元素的效率还是比较高的。如果插入操作引起了向量容量的扩展,那么在执行之前所获得的一切迭代器和指向向量元素的指针、引用都会失效,因为空间被重新分配了,元素的内存地址发生了改变,如果插入操作未引起向量容器容量的扩展,那么只有处于插入位置之后的迭代器和指针、引用会失效,对插入位置之前的元素不会有影响。所谓失效,是指继续使用这样的迭代器或指针的话,结果是不确定的。

#include <vector>
#include <iterator>
#include <iostream>
using namespace std;
int main()
{
    vector<int> a;		//定义一个向量容器a
    a.reserve(3);		//确保a的容量至少是3
    a.push_back(1);			//尾插1
    a.push_back(2);			//尾插2
    vector<int>::iterator iter1 = a.begin();	    //iter1为指向a的第一个元素的迭代器
    int* p1 = &a[0];				    //p1为指向a的第一个元素的指针
    vector<int>::iterator iter2 = a.begin() + 1;    //iter2为指向a的第二个元素的迭代器
    int* p2 = &a[1];			            //p2为指向a的第二个元素的指针
    a.insert(a.begin() + 1, 3);		//在a的第二个元素的位置插入3,即此时a中元素顺序为1 3 2
    cout << *iter1 << " " << *p1 << endl;	    //输出是正确的
    cout << *iter2 << " " << *p2 << endl;	    //理论上输出错误,因为iter2和p2均已失效
    return 0;
}

        执行insert()函数后并没有引起向量容器容量的变化,所以并不是所有的迭代器和引用、指针都失效,由于是在第二个元素之前插入的新元素,所以指向第一个元素的迭代器、指针和引用仍然有效。而iter2和p2指向的元素此时已经位于插入位置之后了,因此插入操作执行之后,iter2和p2在被重新赋值之前就不能再使用了。其实,此时的iter2和p2仍能正常输出,指向的元素是3,但是这种结果是不被C++标准所保障的。相同的,对于删除操作,被删除元素后面的元素都会被向前移动来补充空位。被删除的元素位置越靠前,产出操作所需的时间就越多。删除操作不会引起向量容量的改变,因此被删除元素之前的迭代器、指针和引用都能够继续使用,而被删除元素之后的迭代器、指针和引用都会失效。

(2)双端队列(deque)

        双端队列是一种支持向两端高效的插入数据、支持随机访问的容器。双端队列的内部实现不如向量容器那么直观,双端队列的数据被表示为一个分段数组,容器中的元素分段存放在一个个大小固定的数组中,此外容器还需要维护一个存放这些数组首地址的索引数组。由于分段数组的大小是固定的,且它们的首地址被连续存储在索引数组中,因此可以对双端队列进行随机访问,但这种随机访问的效率比起向量容器要低得多。deque是一种双向开口(先入先出,即:头删尾插)的连续性空间。所谓的连续性,不过时让用户感觉为连续的,实际上是不连续的。duque的底层采用了“中央控制器”和缓冲区的结合方式,对外造成了整体连续的假象。“中央控制器”实际上就是使用了map。map占用一小段连续的空间,其中每个元素都是一个指针,用来记录每个缓冲区的地址,而且缓冲区的大小是固定的,默认为512B。所以每一块中存储的元素个数是相同的。

        向两端插入新元素时,如果这一端的分段数组未满,则可以直接插入,如果这一端的分段数组已满,只需创建新的分段数组,并把该分段数组的地址加入到索引数组中即可。无论哪种情况都不需要对已有的元素进行移动,因此在双端队列的两端插入新的元素都具有较高的效率。执行向两端插入元素的操作时,会使所有的迭代器失效,但不会使任何指向已有元素的指针、引用失效,指针和引用不会失效是因为向两端加入新元素时不会改变已有元素在分段数组中的位置。而迭代器之所以会失效是因为向两端插入新元素可能会引起索引数组中已有元素位置的改变,而迭代器需要依赖索引数组。当删除双端队列容器两端的元素时,由于不需要发生元素的移动,效率也是非常高的。执行删除操作时,只会使被删除元素的迭代器或指针、引用失效,而不会使其他元素的迭代器、指针、引用失效。

        当向双端队列的中间插入元素时,需要将插入点到某一端之间的所有元素向容器的这一端移动,因此向中间插入元素的效率较低,而且往往插入位置越靠近中间效率越低。这样的插入操作不仅会使所有的迭代器失效,也会使所有的指针、引用失效,这是因为向中间插入的操作会移动已有元素,并且向哪一端移动是依据STL的实现而定的。当删除双端队列的中间的元素时,情况也类似,由于被删除元素到某一端之间的所有元素都要向中间移动,删除的位置越靠近中间效率越低,删除操作也会使迭代器和指针、引用失效。

#include <deque>
#include <vector>
#include <iterator>
#include <algorithm>
#include <iostream>
using namespace std;
int main()
{
    int n,x;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n;i++)
    {
	cin >> x;
	a[i] = x;
    }
    cout << a.size() << endl;
    for (vector<int>::iterator iter = a.begin(); iter != a.end(); iter++)
    {
	cout << *iter << " ";
    }
    cout << endl;
    sort(a.begin(), a.end());	        //将输入的整数排序
    deque<int> dq;
    for (vector<int>::iterator iter = a.begin(); iter != a.end(); iter++)
    {
	if (*iter % 2 == 0)
	{
	    dq.push_back(*iter);
	}
	else
	{
	    dq.push_front(*iter);
	}
    }
    //将双端队列dq输出
    copy(dq.begin(), dq.end(), ostream_iterator<int>(cout, " "));
    cout << endl;
    return 0;
}

先输入待输入数据的大小,然后依次输入这些数据,存储在vector中,之后直接调用sort()函数对vector中的元素排序,排序结果不输出,然后对排序结果进行处理,将偶数以尾插的方式存储进双端队列的后面,将奇数以前插的方式存储进双端队列的前面,达到的效果就是和电影院的座位号安排是一样的,奇数在一边,偶数在另一边。

(3)列表(list)

        列表是一种不能随机访问但可以高效地在任意位置插入和删除元素的容器。列表容器一般就是实现为一个链表。在列表中插入一个新的元素,只需要为新的元素建立一个新的链表结点,并修改前后两个结点地指针,而无须移动任何已有的元素,因此效率很高,而且不会使任何已有元素的迭代器、指针和引用失效。删除列表元素时,需要释放被删除元素结点所占用地空间,然后修改前后两个结点的指针,也无须移动任何元素,效率也很高,执行删除操作时只会使被删除元素的迭代器、指针和引用失效,不会影响其他迭代器、指针和引用。

另外,列表还支持一种特殊的操作——接合(splice),即将一个列表容器的一部分元素连续的从该列表中删除后插入到另一个列表容器中。设s1和s2分别是list类型的列表容器实例,p是指向s1中元素的迭代器,q1和q2是指向s2元素的迭代器,则有

s1.splice(p,s2)            将s2列表中的所有元素插入到s1列表中p-1和p之间,将s2列表清空

s1.splice(p,s2,q1)       将s2列表中q1所指向的元素插入到s1列表中p-1和p之间,将q1所指向的元素从s2列表中删除

s1.splice(p,s2,q1,q2)  将s2列表中[q1,q2)区间内的所有元素插入到s1列表中p-1和p之间,将[q1,q2)区间内的元素从s2列表中删除

执行接合操作时,原先指向被接入s1列表中的那些元素的迭代器、指针和引用都会失效,其他迭代器、指针和引用不会失效。列表容器还支持其他特殊的操作,如删除(remove)、条件删除(remove_if)、排序(sort)、去重(unique)、归并(merge)、逆序(reverse)等。

#include <list>
#include <vector>
#include <string>
#include <iostream>
using namespace std;
int main()
{
    string name_1[] = { "Alice", "Helen", "Lucy", "Susan" };
    string name_2[] = { "Bob", "David", "Levin", "Mike" };
    list<string> s1(name_1, name_1 + 4);
    list<string> s2(name_2, name_2 + 4);
    list<string>::iterator iter1 = s1.begin();			//iter1指向s1的首
    cout << "iter1=" << *iter1 << endl;
    list<string>::iterator iter2 = s2.begin();			//iter2指向s2的首
    cout << "iter2=" << *iter2 << endl;
    cout << "原始列表s1为:";
    copy(s1.begin(), s1.end(), ostream_iterator<string>(cout, " "));
    cout << endl;
    cout << "原始列表s2为:";
    copy(s2.begin(), s2.end(), ostream_iterator<string>(cout, " "));
    cout << endl;

    list<string>::iterator iter1_n = s1.begin();	//iter1_n指向新的s1的首
    cout << "iter1_n=" << *iter1_n << endl;
    advance(iter1_n, 2);		//将iter1_n前进两个元素,使其指向s1的第三个元素
    cout << "移动后的iter1_n=" << *iter1_n << endl;

    ++iter2;			//iter2指向s2的第二个元素
    cout << "移动后的iter2=" << *iter2 << endl;

    list<string>::iterator iter3 = iter2;			//用iter2来初始化iter3
    advance(iter3, 2);						//iter3指向s2的第四个元素
    cout << "iter3=" << *iter3 << endl;

    //将[iter2,iter3)区间内的元素接合到iter1的位置
    s1.splice(iter1, s2, iter2, iter3);
    //输出
    copy(s1.begin(), s1.end(), ostream_iterator<string>(cout, " "));
    cout << endl;
    copy(s2.begin(), s2.end(), ostream_iterator<string>(cout, " "));
    cout << endl;
    return 0;
}

三种顺序容器的比较

一般说来,如果需要执行大量的随机访问操作,而且当扩展容器时只需要向容器尾部加入新的元素,就应当选择向量容器vector;

如果需要少量的随机访问操作,需要在容器两端插入或删除元素,则应当选择双端队列容器deque;

如果不需要对容器进行随机访问操作,但是需要在中间位置插入或者删除元素,就应当选择列表容器list

操作向量(vector)双端队列(deque)列表(list)
随机访问较慢不能
头部插入(push_front)没有此操作,只能用insert快。会使已有的迭代器失效,已有的指针和引用不受影响快。已有迭代器、指针、引用都不会失效
头部删除(pop_front)没有此操作,只能用erase快。只会使被删除元素的迭代器、指针、引用失效快。只会使被删除元素的迭代器、指针、引用失效
尾部插入(push_back)快。当发生容量扩充时,会使所有已有的迭代器、指针、引用失效快。会使已有的迭代器失效,已有的指针和引用不受影响快。已有迭代器、指针、引用都不会失效
尾部删除(pop_back)快。只会使被删除元素的迭代器、指针、引用失效快。只会使被删除元素的迭代器、指针、引用失效快。只会使被删除元素的迭代器、指针、引用失效
任意位置插入(insert)插入位置越接近头部,效率越慢。当发生容量扩充时,会使所有已有的迭代器、指针、引用失效插入位置越靠近中间,效率越慢。会使所有的迭代器、指针、引用失效快。已有迭代器、指针、引用都不会失效
任意位置删除(erase)删除位置越靠近头部,效率越慢。只会使被删除元素的迭代器、指针、引用失效删除位置越靠近中间,效率越慢。会使所有的迭代器、指针、引用失效快。只会使被删除元素的迭代器、指针、引用失效
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值