STL序列式容器array、vector、deque、list 和 forward list

目录

array 

        begin()/end() 和 cbegin()/cend() 

        rbegin()/rend() 和 crbegin()/crend()

vector

创建vector容器的几种方式

emplace_back()和push_back()的区别

insert()

emplace()

deque

创建deque容器的几种方式

 list

list容器的创建

list容器可用的成员函数

list splice()


所谓STL序列式容器,其共同的特点是不会对存储的元素进行排序,元素排列的顺序取决于存储它们的顺序。

不同序列式容器的适用场景不同

需要注意的是,序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器:

  • array(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;
  • vector(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
  • deque(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
  • list(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
  • forward_list(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

序列容器包含一些相同的成员函数,它们的功能也相同,本教程会在某个容器的上下文中详细介绍下面的每个函数,但对于每种类型的容器不会重复介绍它们的细节。

表 2 展示了 array、vector 和 deque 容器的函数成员,它们中至少有两个容器实现了同样的函数成员。
 

表 2 array、vector 和 deque 容器的函数成员
函数成员 函数功能 array vector deque
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
assign() 用新元素替换原有内容。 -
operator=() 复制同类型容器的元素,或者用初始化列表替换现有内容。
size() 返回实际元素个数。
max_size() 返回元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
capacity() 返回当前容量。 - -
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
resize() 改变实际元素的个数。 -
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。 -
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
operator[]() 使用索引访问元素。
at() 使用经过边界检査的索引访问元素。
push_back() 在序列的尾部添加一个元素。 -
insert() 在指定的位置插入一个或多个元素。 -
emplace() 在指定的位置直接生成一个元素。 -
emplace_back() 在序列尾部生成一个元素。 -
pop_back() 移出序列尾部的元素。 -
erase() 移出一个元素或一段元素。 -
clear() 移出所有的元素,容器大小变为 0。 -
swap() 交换两个容器的所有元素。
data() 返回指向容器中第一个元素的指针。 -

列表中 - 表明对应的容器并没有定义这个函数。

list 和 forward_list 容器彼此非常相似,forward_list 中包含了 list 的大部分成员函数,而未包含那些需要反向遍历的函数。表 3 展示了 list 和 forward_list 的函数成员。
 

表 3 list 和 forward_list 的函数成员
函数成员 函数功能 list forward_list
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器。
rbegin() 返回指向最后一个元素的迭代器。 -
rend() 返回指向第一个元素所在位置前一个位置的迭代器。 -
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
before_begin() 返回指向第一个元素前一个位置的迭代器。 -
cbefore_begin() 和 before_begin() 功能相同,只不过在其基础上,增加了 const 属性,即不能用该指针修改元素的值。 -
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 -
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 -
assign() 用新元素替换原有内容。
operator=() 复制同类型容器的元素,或者用初始化列表替换现有内容。
size() 返回实际元素个数。 -
max_size() 返回元素个数的最大值,这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
resize() 改变实际元素的个数。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
front() 返回容器中第一个元素的引用。
back() 返回容器中最后一个元素的引用。 -
push_back() 在序列的尾部添加一个元素。 -
push_front() 在序列的起始位置添加一个元素。
emplace() 在指定位置直接生成一个元素。 -
emplace_after() 在指定位置的后面直接生成一个元素。 -
emplace_back() 在序列尾部生成一个元素。 -
cmplacc_front() 在序列的起始位生成一个元索。
insert() 在指定的位置插入一个或多个元素。 -
insert_after() 在指定位置的后面插入一个或多个元素。 -
pop_back() 移除序列尾部的元素。 -
pop_front() 移除序列头部的元素。
reverse() 反转容器中某一段的元素。
erase() 移除指定位置的一个元素或一段元素。 -
erase_after() 移除指定位置后面的一个元素或一段元素。 -
remove() 移除所有和参数匹配的元素。
remove_if() 移除满足一元函数条件的所有元素。
unique() 移除所有连续重复的元素。
clear() 移除所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
sort() 对元素进行排序。
merge() 合并两个有序容器。
splice() 移动指定位置前面的所有元素到另一个同类型的 list 中。 -
splice_after() 移动指定位置后面的所有元素到另一个同类型的 list 中。 -

array 

array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全(原因后续会讲),且效率并没有因此变差。

和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。

在 array 类模板中,T 用于指明容器中的存储的具体数据类型,N 用于指明容器的大小,需要注意的是,这里的 N 必须是常量,不能用变量表示。

  1. std::array values;

由此,就创建好了一个名为 values 的 array 容器,其包含 10 个浮点型元素。但是,由于未显式指定这 10 个元素的值,因此使用这种方式创建的容器中,各个元素的值是不确定的(array 容器不会做默认初始化操作)。

通过如下创建 array 容器的方式,可以将所有的元素初始化为 0 或者和默认元素类型等效的值:

  1. std::array values {};

使用该语句,容器中所有的元素都会被初始化为 0.0。

当然,在创建 array 容器的实例时,也可以像创建常规数组那样对元素进行初始化:

  1. std::array values {0.5,1.0,1.5,,2.0};

可以看到,这里只初始化了前 4 个元素,剩余的元素都会被初始化为 0.0

除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 array 容器包含的 begin() 和 end() 成员函数不同的是,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。

另外,在 头文件中还重载了 get() 全局函数,该重载函数的功能是访问容器中指定的元素,并返回该元素的引用。

正是由于 array 容器中包含了 at() 这样的成员函数,使得操作元素时比普通数组更安全。

#include 
//需要引入 array 头文件
#include 
using namespace std;
int main()
{
    std::array values{};
    //初始化 values 容器为 {0,1,2,3}
    for (int i = 0; i < values.size(); i++) {
        values.at(i) = i;
    }
    //使用 get() 重载函数输出指定位置元素
    cout << get<3>(values) << endl;
    //如果容器不为空,则输出容器中所有的元素
    if (!values.empty()) {
        for (auto val = values.begin(); val < values.end(); val++) {
            cout << *val << " ";
        }
    }
}
//3
//0 1 2 3

在 array 容器的模板类中,和随机访问迭代器相关的成员函数如表 1 所示。
 

表 1 array 支持迭代器的成员函数
成员函数 功能
begin() 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。
end() 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。
rbegin() 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。
rend() 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。
cbegin() 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
cend() 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。
crend() 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。

begin()/end() 和 cbegin()/cend() 

可以在循环中显式地使用迭代器来初始化 values 容器的值:

#include 
//需要引入 array 头文件
#include 
using namespace std;
int main()
{
    arrayvalues;
    int h = 1;
    auto first = values.begin();
    auto last = values.end();
    //初始化 values 容器为{1,2,3,4,5}
    while (first != last)
    {
        *first = h;
        ++first;
        h++;
    }
  
    first = values.begin();
    while (first != last)
    {
        cout << *first << " ";
        ++first;
    }
    return 0; 
}

rbegin()/rend() 和 crbegin()/crend()

array 模板类中还提供了 rbegin()/rend() 和 crbegin()/crend() 成员函数,它们每对都可以分别得到指向最一个元素和第一个元素前一个位置的随机访问迭代器,又称它们为反向迭代器

需要注意的是,在使用反向迭代器进行 ++ 或 -- 运算时,++ 指的是迭代器向左移动一位,-- 指的是迭代器向右移动一位,即这两个运算符的功能也“互换”了。

反向迭代器用于以逆序的方式处理元素。例如:

#include 
//需要引入 array 头文件
#include 
using namespace std;
int main()
{
    arrayvalues;
    int h = 1;
    auto first = values.rbegin();
    auto last = values.rend(); 
    //初始化 values 容器为 {5,4,3,2,1}
    while (first != last)
    {
        *first = h;
        ++first;
        h++;
    }
    //重新遍历容器,并输入各个元素
    first = values.rbegin();
    while (first != last)
    {
        cout << *first << " ";
        ++first;
    }
    return 0;
}

当 array 容器创建完成之后,最常做的操作就是获取其中的元素,甚至有时还会通过循环结构获取多个元素

首先,可以通过容器名[]的方式直接访问和使用容器中的元素,这和 C++ 标准数组访问元素的方式相同,例如:

        values[4] = values[3] + 2.O*values[1];

此行代码中,第 5 个元素的值被赋值为右边表达式的值。需要注意的是,使用如上这样方式,由于没有做任何边界检查,所以即便使用越界的索引值去访问或存储元素,也不会被检测到。

为了能够有效地避免越界访问的情况,可以使用 array 容器提供的 at() 成员函数,例如 

  1. values.at (4) = values.at(3) + 2.O*values.at(1);

这行代码和前一行语句实现的功能相同,其次当传给 at() 的索引是一个越界值时,程序会抛出 std::out_of_range 异常。因此当需要访问容器中某个指定元素时,建议大家使用 at(),除非确定索引没有越界。

另外,array 容器提供了 data() 成员函数,通过调用该函数可以得到指向容器首个元素的指针。通过该指针,我们可以获得容器中的各个元素

我们知道,array 容器提供的 size() 函数能够返回容器中元素的个数(函数返回值为 size_t 类型),所以能够像下面这样去逐个提取容器中的元素,并计算它们的和:

  1. double total = 0;
  2. for(size_t i = 0 ; i < values.size() ; ++i)
  3. {
  4. total += values[i];
  5. }

size() 函数的存在,为 array 容器提供了标准数组所没有的优势,即能够知道它包含多少元素。

并且,接受数组容器作为参数的函数,只需要通过调用容器的成员函数 size(),就能得到元素的个数。除此之外,通过调用 array 容器的 empty() 成员函数,即可知道容器中有没有元素(如果容器中没有元素,此函数返回 true),如下所示:

  1. if(values.empty())
  2. std::cout << "The container has no elements.\n";
  3. else
  4. std::cout << "The container has "<< values.size()<<"elements.\n";

然而,很少会创建空的 array 容器,因为当生成一个 array 容器时,它的元素个数就固定了,而且无法改变,所以生成空 array 容器的唯一方法是将模板的第二个参数指定为 0,但这种情况基本不可能发生。

array 容器之所以提供 empty() 成员函数的原因,对于其他元素可变或者元素可删除的容器(例如 vector、deque 等)来说,它们使用 empty() 时的机制是一样的,因此为它们提供了一个一致性的操作。

除了借助 size() 外,对于任何可以使用迭代器的容器,都可以使用基于范围的循环,因此能够更加简便地计算容器中所有元素的和,比如:

  1. double total = 0;
  2. for(auto&& value : values)
  3. total += value;

vector

vector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。

vector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)

创建vector容器的几种方式

创建 vector 容器的方式有很多,大致可分为以下几种。

1) 如下代码展示了如何创建存储 double 类型元素的一个 vector 容器:

  1. std::vector values;

如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。

注意,这是一个空的 vector 容器,因为容器中没有元素,所以没有为其分配空间。当添加第一个元素(比如使用 push_back() 函数)时,vector 会自动分配内存。

在创建好空容器的基础上,还可以像下面这样通过调用 reserve() 成员函数来增加容器的容量:

  1. values.reserve(20);

这样就设置了容器的内存分配,即至少可以容纳 20 个元素。注意,如果 vector 的容量在执行此语句之前,已经大于或等于 20 个元素,那么这条语句什么也不做;另外,调用 reserve() 不会影响已存储的元素,也不会生成任何元素,即 values 容器内此时仍然没有任何元素。

还需注意的是,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能已经被复制或移到了新的内存地址。所以后续再使用这些迭代器时,最好重新生成一下。


2) 除了创建空 vector 容器外,还可以在创建的同时指定初始值以及元素个数,比如:

  1. std::vector primes {2, 3, 5, 7, 11, 13, 17, 19};

这样就创建了一个含有 8 个素数的 vector 容器。

3) 在创建 vector 容器时,也可以指定元素个数:

  1. std::vector values(20);

如此,values 容器开始时就有 20 个元素,它们的默认初始值都为 0。

注意,圆括号 () 和大括号 {} 是有区别的,前者(例如 (20) )表示元素的个数,而后者(例如 {20} ) 则表示 vector 容器中只有一个元素 20。

如果不想用 0 作为默认值,也可以指定一个其它值,例如:

  1. std::vector values(20, 1.0);

第二个参数指定了所有元素的初始值,因此这 20 个元素的值都是 1.0。

值得一提的是,圆括号 () 中的 2 个参数,既可以是常量,也可以用变量来表示,例如:

  1. int num=20;
  2. double value =1.0;
  3. std::vector values(num, value);


4) 通过存储元素类型相同的其它 vector 容器,也可以创建新的 vector 容器,例如:

  1. std::vectorvalue1(5, 'c');
  2. std::vectorvalue2(value1);

由此,value2 容器中也具有 5 个字符 'c'。在此基础上,如果不想复制其它容器中所有的元素,可以用一对指针或者迭代器来指定初始值的范围,例如:

  1. int array[]={1,2,3};
  2. std::vectorvalues(array, array+2);//values 将保存{1,2}
  3. std::vectorvalue1{1,2,3,4,5};
  4. std::vectorvalue2(std::begin(value1),std::begin(value1)+3);//value2保存{1,2,3}

由此,value2 容器中就包含了 {1,2,3} 这 3 个元素。

相比 array 容器,vector 提供了更多了成员函数供我们使用,它们各自的功能如表 1 所示。
 

表 1 vector 容器的成员函数
函数成员 函数功能
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回实际元素个数。
max_size() 返回元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
resize() 改变实际元素的个数。
capacity() 返回当前容量。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
reserve() 增加容器的容量。
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。
operator[ ] 重载了 [ ] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素。
at() 使用经过边界检查的索引访问元素。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
data() 返回指向容器中第一个元素的指针。
assign() 用新元素替换原有内容。
push_back() 在序列的尾部添加一个元素。
pop_back() 移出序列尾部的元素。
insert() 在指定的位置插入一个或多个元素。
erase() 移出一个元素或一段元素。
clear() 移出所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
emplace() 在指定的位置直接生成一个元素。
emplace_back() 在序列尾部生成一个元素。

push_back()

该成员函数的功能是在 vector 容器尾部添加一个元素

#include 
#include 
using namespace std;
int main()
{
    vector values{};
    values.push_back(1);
    values.push_back(2);
    for (int i = 0; i < values.size(); i++) {
        cout << values[i] << " ";
    }
    return 0;
}

emplace_back()和push_back()的区别

emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。 

显然完成同样的操作,push_back() 的底层实现过程比 emplace_back() 更繁琐,换句话说,emplace_back() 的执行效率比 push_back() 高。因此,在实际使用时,建议大家优先选用 emplace_back()。

insert()

insert() 函数的功能是在 vector 容器的指定位置插入一个或多个元素。该函数的语法格式有多种

语法格式 用法说明
iterator insert(pos,elem) 在迭代器 pos 指定的位置之前插入一个新元素elem,并返回表示新插入元素位置的迭代器。
iterator insert(pos,n,elem) 在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器。
iterator insert(pos,first,last)  在迭代器 pos 指定的位置之前,插入其他容器(不仅限于vector)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器。
iterator insert(pos,initlist) 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号{}括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器。

#include  
#include  
#include  
using namespace std;
int main()
{
    std::vector demo{1,2};
    //第一种格式用法
    demo.insert(demo.begin() + 1, 3);//{1,3,2}
    //第二种格式用法
    demo.insert(demo.end(), 2, 5);//{1,3,2,5,5}
    //第三种格式用法
    std::arraytest{ 7,8,9 };
    demo.insert(demo.end(), test.begin(), test.end());//{1,3,2,5,5,7,8,9}
    //第四种格式用法
    demo.insert(demo.end(), { 10,11 });//{1,3,2,5,5,7,8,9,10,11}
    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    return 0;
}

emplace()

emplace() 是 C++ 11 标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素。emplace() 每次只能插入一个元素,而不是多个。

既然 emplace() 和 insert() 都能完成向 vector 容器中插入新元素,那么谁的运行效率更高呢?答案是 emplace()。在说明原因之前,通过下面这段程序,就可以直观看出两者运行效率的差异:

通过 insert() 函数向 vector 容器中插入 testDemo 类对象,需要调用类的构造函数和移动构造函数(或拷贝构造函数);而通过 emplace() 函数实现同样的功能,只需要调用构造函数即可。

简单的理解,就是 emplace() 在插入元素时,是在容器的指定位置直接构造元素,而不是先单独生成,再将其复制(或移动)到容器中。因此,在实际使用中,推荐大家优先使用 emplace()。

无论是向现有 vector 容器中访问元素、添加元素还是插入元素,都只能借助 vector 模板类提供的成员函数,但删除 vector 容器的元素例外,完成此操作除了可以借助本身提供的成员函数,还可以借助一些全局函数。

基于不同场景的需要,删除 vecotr 容器的元素,可以使用表 1 中所示的函数(或者函数组合)。
 

表 1 删除 vector 容器元素的几种方式
函数 功能
pop_back() 删除 vector 容器中最后一个元素,该容器的大小(size)会减 1,但容量(capacity)不会发生改变。
erase(pos) 删除 vector 容器中 pos 迭代器指定位置处的元素,并返回指向被删除元素下一个位置元素的迭代器。该容器的大小(size)会减 1,但容量(capacity)不会发生改变。
swap(beg)、pop_back() 先调用 swap() 函数交换要删除的目标元素和容器最后一个元素的位置,然后使用 pop_back() 删除该目标元素。
erase(beg,end) 删除 vector 容器中位于迭代器 [beg,end)指定区域内的所有元素,并返回指向被删除区域下一个位置元素的迭代器。该容器的大小(size)会减小,但容量(capacity)不会发生改变。
remove() 删除容器中所有和指定元素值相等的元素,并返回指向最后一个元素下一个位置的迭代器。值得一提的是,调用该函数不会改变容器的大小和容量。
clear() 删除 vector 容器中所有的元素,使其变成空的 vector 容器。该函数会改变 vector 的大小(变为 0),但不是改变其容量。

pop_back() 成员函数的用法非常简单,它不需要传入任何的参数,也没有返回值。

如果想删除 vector 容器中指定位置处的元素,可以使用 erase() 成员函数,该函数的语法格式为:

#include 
#include 
using namespace std;
int main()
{
    vectordemo{ 1,2,3,4,5 };
    auto iter = demo.erase(demo.begin() + 1);//删除元素 2
    //输出 dmeo 容器新的size
    cout << "size is :" << demo.size() << endl;
    //输出 demo 容器新的容量
    cout << "capacity is :" << demo.capacity() << endl;
    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    //iter迭代器指向元素 3
    cout << endl << *iter << endl;
    return 0;
}

#include 
#include 
using namespace std;
int main()
{
    std::vector demo{ 1,2,3,4,5 };
    //删除 2、3
    auto iter = demo.erase(demo.begin()+1, demo.end() - 2);
    cout << "size is :" << demo.size() << endl;
    cout << "capacity is :" << demo.capacity() << endl;
    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    return 0;
}

如果要删除容器中和指定元素值相同的所有元素,可以使用 remove() 函数,该函数定义在  头文件中。例如:

#include 
#include 
#include 
using namespace std;
int main()
{
    vectordemo{ 1,3,3,4,3,5 };
    //交换要删除元素和最后一个元素的位置
    auto iter = std::remove(demo.begin(), demo.end(), 3);
    cout << "size is :" << demo.size() << endl;
    cout << "capacity is :" << demo.capacity() << endl;
    //输出剩余的元素
    for (auto first = demo.begin(); first < iter;++first) {
        cout << *first << " ";
    }
    return 0;
}

remove() 的实现原理是,在遍历容器中的元素时,一旦遇到目标元素,就做上标记,然后继续遍历,直到找到一个非目标元素,即用此元素将最先做标记的位置覆盖掉,同时将此非目标元素所在的位置也做上标记,等待找到新的非目标元素将其覆盖。因此,如果将上面程序中 demo 容器的元素全部输出,得到的结果为 1 4 5 4 3 5

另外还可以看到,既然通过 remove() 函数删除掉 demo 容器中的多个指定元素,该容器的大小和容量都没有改变,其剩余位置还保留了之前存储的元素。我们可以使用 erase() 成员函数删掉这些 "无用" 的元素。

remove()用于删除容器中指定元素时,常和 erase() 成员函数搭配使用。

deque

deque 是 double-ended queue 的缩写,又称双端队列容器。

前面章节中,我们已经系统学习了 vector 容器,值得一提的是,deque 容器和 vecotr 容器有很多相似之处,比如:

  • deque 容器也擅长在序列尾部添加或删除元素(时间复杂度为O(1)),而不擅长在序列中间添加或删除元素。
  • deque 容器也可以根据需要修改自身的容量和大小。


和 vector 不同的是,deque 还擅长在序列头部添加或删除元素,所耗费的时间复杂度也为常数阶O(1)。并且更重要的一点是,deque 容器中存储元素并不能保证所有元素都存储到连续的内存空间中。

当需要向序列两端频繁的添加或删除元素时,应首选 deque 容器。

创建deque容器的几种方式

创建 deque 容器,根据不同的实际场景,可选择使用如下几种方式。

1) 创建一个没有任何元素的空 deque 容器:

  1. std::deque d;

和空 array 容器不同,空的 deque 容器在创建之后可以做添加或删除元素的操作,因此这种简单创建 deque 容器的方式比较常见。

2) 创建一个具有 n 个元素的 deque 容器,其中每个元素都采用对应类型的默认值:

  1. std::deque d(10);

此行代码创建一个具有 10 个元素(默认都为 0)的 deque 容器。

3) 创建一个具有 n 个元素的 deque 容器,并为每个元素都指定初始值,例如:

  1. std::deque d(10, 5)

如此就创建了一个包含 10 个元素(值都为 5)的 deque 容器。

4) 在已有 deque 容器的情况下,可以通过拷贝该容器创建一个新的 deque 容器,例如:

  1. std::deque d1(5);
  2. std::deque d2(d1);

注意,采用此方式,必须保证新旧容器存储的元素类型一致。

5) 通过拷贝其他类型容器中指定区域内的元素(也可以是普通数组),可以创建一个新容器,例如:

  1. //拷贝普通数组,创建deque容器
  2. int a[] = { 1,2,3,4,5 };
  3. std::dequed(a, a + 5);
  4. //适用于所有类型的容器
  5. std::arrayarr{ 11,12,13,14,15 };
  6. std::dequed(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}

基于 deque 双端队列的特点,该容器包含一些 array、vector 容器都没有的成员函数。

函数成员 函数功能
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回实际元素个数。
max_size() 返回容器所能容纳元素个数的最大值。这通常是一个很大的值,一般是 232-1,我们很少会用到这个函数。
resize() 改变实际元素的个数。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。
at() 使用经过边界检查的索引访问元素。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
assign() 用新元素替换原有内容。
push_back() 在序列的尾部添加一个元素。
push_front() 在序列的头部添加一个元素。
pop_back() 移除容器尾部的元素。
pop_front() 移除容器头部的元素。
insert() 在指定的位置插入一个或多个元素。
erase() 移除一个元素或一段元素。
clear() 移出所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
emplace() 在指定的位置直接生成一个元素。
emplace_front() 在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。
emplace_back() 在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。

和 vector 相比,额外增加了实现在容器头部添加和删除元素的成员函数,同时删除了 capacity()、reserve() 和 data() 成员函数。

和 array、vector 相同,C++ 11 标准库新增的 begin() 和 end() 这 2 个全局函数也适用于 deque 容器。这 2 个函数的操作对象既可以是容器,也可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。

deque 容器还有一个std::swap(x , y) 非成员函数(其中 x 和 y 是存储相同类型元素的 deque 容器),它和 swap() 成员函数的功能完全相同,仅使用语法上有差异。 

#include 
#include 
using namespace std;
int main()
{
    //初始化一个空deque容量
    dequed;
    //向d容器中的尾部依次添加 1,2,3
    d.push_back(1); //{1}
    d.push_back(2); //{1,2}
    d.push_back(3); //{1,2,3}
    //向d容器的头部添加 0 
    d.push_front(0); //{0,1,2,3}
    //调用 size() 成员函数输出该容器存储的字符个数。
    printf("元素个数为:%d\n", d.size());
   
    //使用迭代器遍历容器
    for (auto i = d.begin(); i < d.end(); i++) {
        cout << *i << " ";
    }
    cout << endl;
    return 0;
}

和 vector 容器不同,deque 容器没有提供 data() 成员函数,同时 deque 容器在存储元素时,也无法保证其会将元素存储在连续的内存空间中,因此尝试使用指针去访问 deque 容器中指定位置处的元素,是非常危险的。 

 list

list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中。

基于这样的存储结构,list 容器具有一些其它容器(array、vector 和 deque)所不具备的优势,即它可以在序列已知的任何位置快速插入或删除元素(时间复杂度为O(1))。并且在 list 容器中移动元素,也比其它容器的效率高。

使用 list 容器的缺点是,它不能像 array 和 vector 那样,通过位置直接访问元素。举个例子,如果要访问 list 容器中的第 6 个元素,它不支持容器对象名[6]这种语法格式,正确的做法是从容器中第一个元素或最后一个元素开始遍历容器,直到找到该位置

实际场景中,如何需要对序列进行大量添加或删除元素的操作,而直接访问元素的需求却很少,这种情况建议使用 list 容器存储序列。

list容器的创建

根据不同的使用场景,有以下 5 种创建 list 容器的方式供选择。

1) 创建一个没有任何元素的空 list 容器:

  1. std::list values;

和空 array 容器不同,空的 list 容器在创建之后仍可以添加元素,因此创建 list 容器的方式很常用。

2) 创建一个包含 n 个元素的 list 容器:

  1. std::list values(10);

通过此方式创建 values 容器,其中包含 10 个元素,每个元素的值都为相应类型的默认值(int类型的默认值为 0)。

3) 创建一个包含 n 个元素的 list 容器,并为每个元素指定初始值。例如:

  1. std::list values(10, 5);

如此就创建了一个包含 10 个元素并且值都为 5 个 values 容器。

4) 在已有 list 容器的情况下,通过拷贝该容器可以创建新的 list 容器。例如:

  1. std::list value1(10);
  2. std::list value2(value1);

注意,采用此方式,必须保证新旧容器存储的元素类型一致。

5) 通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 list 容器。例如:

  1. //拷贝普通数组,创建list容器
  2. int a[] = { 1,2,3,4,5 };
  3. std::list values(a, a+5);
  4. //拷贝其它类型的容器,创建 list 容器
  5. std::arrayarr{ 11,12,13,14,15 };
  6. std::listvalues(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}

list容器可用的成员函数

表 2 中罗列出了 list 模板类提供的所有成员函数以及各自的功能。
 

表 2 list 容器可用的成员函数
成员函数 功能
begin() 返回指向容器中第一个元素的双向迭代器。
end() 返回指向容器中最后一个元素所在位置的下一个位置的双向迭代器。
rbegin() 返回指向最后一个元素的反向双向迭代器。
rend() 返回指向第一个元素所在位置前一个位置的反向双向迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin()  和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
size() 返回当前容器实际包含的元素个数。
max_size() 返回容器所能包含元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
assign() 用新元素替换容器中原有内容。
emplace_front() 在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。
push_front() 在容器头部插入一个元素。
pop_front() 删除容器头部的一个元素。
emplace_back() 在容器尾部直接生成一个元素。该函数和 push_back() 的功能相同,但效率更高。
push_back() 在容器尾部插入一个元素。
pop_back() 删除容器尾部的一个元素。
emplace() 在容器中的指定位置插入元素。该函数和 insert() 功能相同,但效率更高。
insert()  在容器中的指定位置插入元素。
erase() 删除容器中一个或某区域内的元素。
swap() 交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。
resize() 调整容器的大小。
clear() 删除容器存储的所有元素。
splice() 将一个 list 容器中的元素插入到另一个容器的指定位置。
remove(val) 删除容器中所有等于 val 的元素。
remove_if() 删除容器中满足条件的元素。
unique() 删除容器中相邻的重复元素,只保留一个。
merge() 合并两个事先已排好序的 list 容器,并且合并之后的 list 容器依然是有序的。
sort() 通过更改容器中元素的位置,将它们进行排序。
reverse() 反转容器中元素的顺序。

只有运用迭代器,才能访问 list 容器中存储的各个元素 

#include 
#include 
using namespace std;
int main()
{
    //创建 list 容器
    std::list values{'h','t','t','p',':','/','/','c','.','b','i','a','n','c','h','e','n','g','.','n','e','t'};
    //使用begin()/end()迭代器函数对输出list容器中的元素
    for (std::list::iterator it = values.begin(); it != values.end(); ++it) {
        std::cout << *it;
    }
    cout << endl;
    //使用 rbegin()/rend()迭代器函数输出 lsit 容器中的元素
    for (std::list::reverse_iterator it = values.rbegin(); it != values.rend();++it) {
        std::cout << *it;
    }
    return 0;
}

程序中比较迭代器之间的关系,用的是 != 运算符,因为它不支持 < 等运算符。另外在实际场景中,所有迭代器函数的返回值都可以传给使用 auto 关键字定义的变量,因为编译器可以自行判断出该迭代器的类型 

值得一提的是,list 容器在进行插入(insert())、接合(splice())等操作时,都不会造成原有的 list 迭代器失效,甚至进行删除操作,而只有指向被删除元素的迭代器失效,其他迭代器不受任何影响。

不同于之前学过的 STL 容器,访问 list 容器中存储元素的方式很有限,即要么使用 front() 和 back() 成员函数,要么使用 list 容器迭代器。

list 容器不支持随机访问,未提供下标操作符 [] 和 at() 成员函数,也没有提供 data() 成员函数。

#include 
#include 
using namespace std;
int main()
{
    std::list mylist{ 1,2,3,4 };
    int &first = mylist.front();
    int &last = mylist.back();
    cout << first << " " << last << endl;
    first = 10;
    last = 20;
    cout << mylist.front() << " " << mylist.back() << endl;
    return 0;
}

可以看到,通过 front() 和 back() 的返回值,我们不仅能分别获取当前 list 容器中的首尾元素,必要时还能修改它们的值。

除此之外,如果想访问 list 容存储的其他元素,就只能使用 list 容器的迭代器

#include 
#include 
using namespace std;
int main()
{
    const std::list mylist{1,2,3,4,5};
    auto it = mylist.begin();
    cout << *it << " ";
    ++it;
    while (it!=mylist.end())
    {
        cout << *it << " ";
        ++it;  
    }
    return 0;
}

list 模板类中,与“添加或插入新元素”相关的成员方法有如下几个:

  • push_front():向 list 容器首个元素前添加新元素;
  • push_back():向 list 容器最后一个元素后添加新元素;
  • emplace_front():在容器首个元素前直接生成新的元素;
  • emplace_back():在容器最后一个元素后直接生成新的元素;
  • emplace():在容器的指定位置直接生成新的元素;
  • insert():在指定位置插入新元素;
  • splice():将其他 list 容器存储的多个元素添加到当前 list 容器的指定位置处。


以上这些成员方法中,除了 insert() 和 splice() 方法有多种语法格式外,其它成员方法都仅有 1 种语法格式,下面程序演示了它们的具体用法。

#include 
#include 
using namespace std;
int main()
{
    std::list values{1,2,3};
    values.push_front(0);//{0,1,2,3}
    values.push_back(4); //{0,1,2,3,4}
    values.emplace_front(-1);//{-1,0,1,2,3,4}
    values.emplace_back(5);  //{-1,0,1,2,3,4,5}
   
    //emplace(pos,value),其中 pos 表示指明位置的迭代器,value为要插入的元素值
    values.emplace(values.end(), 6);//{-1,0,1,2,3,4,5,6}
    for (auto p = values.begin(); p != values.end(); ++p) {
        cout << *p << " ";
    }
    return 0;
}

list splice()

和 insert() 成员方法相比,splice() 成员方法的作用对象是其它 list 容器,其功能是将其它 list 容器中的元素添加到当前 list 容器中指定位置处。

我们知道,list 容器底层使用的是链表存储结构,splice() 成员方法移动元素的方式是,将存储该元素的节点从 list 容器底层的链表中摘除,然后再链接到当前 list 容器底层的链表中。这意味着,当使用 splice() 成员方法将 x 容器中的元素添加到当前容器的同时,该元素会从 x 容器中删除。

#include 
#include 
using namespace std;
int main()
{
    //创建并初始化 2 个 list 容器
    list mylist1{ 1,2,3,4 }, mylist2{10,20,30};
    list::iterator it = ++mylist1.begin(); //指向 mylist1 容器中的元素 2
   
    //调用第一种语法格式
    mylist1.splice(it, mylist2); // mylist1: 1 10 20 30 2 3 4
                                 // mylist2:
                                 // it 迭代器仍然指向元素 2,只不过容器变为了 mylist1
    //调用第二种语法格式,将 it 指向的元素 2 移动到 mylist2.begin() 位置处
    mylist2.splice(mylist2.begin(), mylist1, it);   // mylist1: 1 10 20 30 3 4
                                                    // mylist2: 2
                                                    // it 仍然指向元素 2
   
    //调用第三种语法格式,将 [mylist1.begin(),mylist1.end())范围内的元素移动到 mylist.begin() 位置处                  
    mylist2.splice(mylist2.begin(), mylist1, mylist1.begin(), mylist1.end());//mylist1:
                                                                             //mylist2:1 10 20 30 3 4 2
   
    cout << "mylist1 包含 " << mylist1.size() << "个元素" << endl;
    cout << "mylist2 包含 " << mylist2.size() << "个元素" << endl;
    //输出 mylist2 容器中存储的数据
    cout << "mylist2:";
    for (auto iter = mylist2.begin(); iter != mylist2.end(); ++iter) {
        cout << *iter << " ";
    }
    return 0;
}

对 list 容器存储的元素执行删除操作,需要借助该容器模板类提供的成员函数。幸运的是,相比其它 STL 容器模板类,list 模板类提供了更多用来实现此操作的成员函数(如表 1 所示)。
 

表 1 实现 list 容器删除元素的成员函数
成员函数 功能
pop_front() 删除位于 list 容器头部的一个元素。
pop_back() 删除位于 list 容器尾部的一个元素。
erase() 该成员函数既可以删除 list 容器中指定位置处的元素,也可以删除容器中某个区域内的多个元素。
clear() 删除 list 容器存储的所有元素。
remove(val) 删除容器中所有等于 val 的元素。
unique() 删除容器中相邻的重复元素,只保留一份。
remove_if() 删除容器中满足条件的元素。

erase() 成员函数有以下 2 种语法格式:

iterator erase (iterator position);
iterator erase (iterator first, iterator last);

利用第一种语法格式,可实现删除 list 容器中 position 迭代器所指位置处的元素

#include 
#include 
using namespace std;
int main()
{
    listvalues{ 1,2,3,4,5 };
    //指向元素 1 的迭代器
    auto del = values.begin();
    //迭代器右移,改为指向元素 2
    ++del;
    values.erase(del); //{1,3,4,5}
    for (auto begin = values.begin(); begin != values.end(); ++begin)
    {
        cout << *begin << " ";
    }
    return 0;
}

 利用第二种语法格式,可实现删除 list 容器中 first 迭代器和 last 迭代器限定区域内的所有元素(包括 first 指向的元素,但不包括 last 指向的元素)

#include 
#include 
using namespace std;
int main()
{
    listvalues{ 1,2,3,4,5 };
    //指定删除区域的左边界
    auto first = values.begin();
    ++first;//指向元素 2
    //指向删除区域的右边界
    auto last = values.end();
    --last;//指向元素 5
    //删除 2、3 和 4
    values.erase(first, last);
    for (auto begin = values.begin(); begin != values.end(); ++begin)
    {
        cout << *begin << " ";
    }
    return 0;
}

forward_list 是 C++ 11 新添加的一类容器,其底层实现和 list 容器一样,采用的也是链表结构,只不过 forward_list 使用的是单链表,而 list 使用的是双向链表

通过图 1 不难看出,使用链表存储数据最大的特点在于,其并不会将数据进行集中存储(向数组那样),换句话说,链表中数据的存储位置是分散的、随机的,整个链表中数据的线性关系通过指针来维持。

因此,forward_list 容器具有和 list 容器相同的特性,即擅长在序列的任何位置进行插入元素或删除元素的操作,但对于访问存储的元素,没有其它容器(如 array、vector)的效率高。

另外,由于单链表没有双向链表那样灵活,因此相比 list 容器,forward_list 容器的功能受到了很多限制。比如,由于单链表只能从前向后遍历,而不支持反向遍历,因此 forward_list 容器只提供前向迭代器,而不是双向迭代器。这意味着,forward_list 容器不具有 rbegin()、rend() 之类的成员函数。

你可能感兴趣的:(c++,c++,数据结构,链表)