7 STL【list介绍】【list创建/迭代器/成员函数/底层实现】【访问/添加/删除list元素】【forward_list】

0 - 前言

参考:C++ list(STL list)容器完全攻略(超级详细)

1 - list是什么

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

在这里插入图片描述

list 容器中各个元素的前后顺序是靠指针来维系的,每个元素都配备了 2 个指针,分别指向它的前一个元素和后一个元素。其中第一个元素的前向指针总为 null,因为它前面没有元素;同样,尾部元素的后向指针也总为 null。

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

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

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

list 容器以模板类 list(T 为存储元素的类型)的形式在<list>头文件中,并位于 std 命名空间中。因此,在使用该容器之前,代码中需要包含下面两行代码:

#include <list>
using namespace std;

2 - 创建list

//创建一个没有任何元素的空 list 容器
list<int> values;
//创建一个包含 n 个元素的 list 容器
list<int> values(10);
//创建一个包含 n 个元素的 list 容器,并为每个元素指定初始值
list<int> values(10, 5);
//在已有 list 容器的情况下,通过拷贝该容器可以创建新的 list 容器
list<int> value1(10);
list<int> value2(value1);
//通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 list 容器
int a[] = {
    
     1,2,3,4,5 };
list<int> values(a, a+5);

array<int, 5>arr{
    
     11,12,13,14,15 };//拷贝其它类型的容器,创建 list 容器
list<int>values(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}

3 - list成员函数

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

4 - list迭代器

在这里插入图片描述

这些成员函数通常是成对使用的,即 begin()/end()、rbegin()/rend()、cbegin()/cend()、crbegin()/crend() 各自成对搭配使用。其中,begin()/end() 和 cbegin/cend() 的功能是类似的,同样 rbegin()/rend() 和 crbegin()/crend() 的功能是类似的

和array、vector、deque 容器的迭代器相比,list 容器迭代器最大的不同在于,其配备的迭代器类型为双向迭代器,而不再是随机访问迭代器

这意味着,假设 p1 和 p2 都是双向迭代器,则它们支持使用 ++p1、 p1++、 p1–、 p1++、 *p1、 p1==p2 以及 p1!=p2 运算符,但不支持以下操作(其中 i 为整数):

  • p1[i]:不能通过下标访问 list 容器中指定位置处的元素。
  • p1-=i、 p1+=i、 p1+i 、p1-i:双向迭代器 p1 不支持使用 -=、+=、+、- 运算符。
  • p1<p2、 p1>p2、 p1<=p2、 p1>=p2:双向迭代器 p1、p2 不支持使用 <、 >、 <=、 >= 比较运算符。

值得一提的是,list 容器在进行插入(insert())、接合(splice())等操作时,都不会造成原有的 list 迭代器失效,甚至进行删除操作,而只有指向被删除元素的迭代器失效,其他迭代器不受任何影响。因此,在进行插入操作之后,仍使用先前创建的迭代器遍历容器,虽然程序不会出错,但由于插入位置的不同,可能会遗漏新插入的元素。

5 - list底层实现

list底层是用双向链表实现的,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持。

在这里插入图片描述

双向链表的各个节点中存储的不仅仅是元素的值,还应包含 2 个指针,分别指向前一个元素和后一个元素。

template<typename T,...>
struct __List_node{
    
    
    //...
    __list_node<T>* prev;
    __list_node<T>* next;
    T myval;
    //...
}

可以看到,list 容器定义的每个节点中,都包含 *prev、 *next 和 myval。其中,prev 指针用于指向前一个节点;next 指针用于指向后一个节点;myval 用于存储当前元素的值。

6 - 访问list元素

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

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

通过 front() 和 back() 成员函数,可以分别获得 list 容器中第一个元素和最后一个元素的引用形式

#include <iostream>
#include <list>
using namespace std;
int main()
{
    
    
    std::list<int> 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;
}

输出:
1 4
10 20

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

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

对于修改容器指定元素的值,list 模板类提供有专门的成员函数 assign()

7 - list添加元素

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

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

insert

在这里插入图片描述

#include <iostream>
#include <list>
#include <array>
using namespace std;
int main()
{
    
    
    std::list<int> values{
    
     1,2 };
    //第一种格式用法
    values.insert(values.begin() , 3);//{3,1,2}
    //第二种格式用法
    values.insert(values.end(), 2, 5);//{3,1,2,5,5}
    //第三种格式用法
    std::array<int, 3>test{
    
     7,8,9 };
    values.insert(values.end(), test.begin(), test.end());//{3,1,2,5,5,7,8,9}
    //第四种格式用法
    values.insert(values.end(), {
    
     10,11 });//{3,1,2,5,5,7,8,9,10,11}
}

splice

和 insert() 成员方法相比,splice() 成员方法的作用对象是其它 list 容器,其功能是将其它 list 容器中的元素添加到当前 list 容器中指定位置处
在这里插入图片描述

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

#include <iostream>
#include <list>
using namespace std;
int main()
{
    
    
    //创建并初始化 2 个 list 容器
    list<int> mylist1{
    
     1,2,3,4 }, mylist2{
    
    10,20,30};
    list<int>::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
}

8 - list删除元素

在这里插入图片描述

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

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

erase() 成员函数是按照被删除元素所在的位置来执行删除操作,如果想根据元素的值来执行删除操作,可以使用 remove() 成员函数。emove(aim)是删除链表中的aim元素,若有多个aim,都会删除

#include <iostream>
#include <list>
using namespace std;
int main()
{
    
    
    list<char>values{
    
    'a','b','c','d'};
    values.remove('c');
    for (auto begin = values.begin(); begin != values.end(); ++begin)
    {
    
    
        cout << *begin << " ";
    }
    return 0;
}
输出:
a b d

unique() 函数也有以下 2 种语法格式:

void unique()
void unique(BinaryPredicate)//传入一个二元谓词函数

以上 2 种格式都能实现去除 list 容器中相邻重复的元素,仅保留一份。但第 2 种格式的优势在于,我们能自定义去重的规则

#include <iostream>
#include <list>
using namespace std;
//二元谓词函数
bool demo(double first, double second)
{
    
    
    return (int(first) == int(second));
}
int main()
{
    
    
    list<double> mylist{
    
     1,1.2,1.2,3,4,4.5,4.6 };
    //删除相邻重复的元素,仅保留一份
    mylist.unique();//{1, 1.2, 3, 4, 4.5, 4.6}
    for (auto it = mylist.begin(); it != mylist.end(); ++it)
        cout << *it << ' ';
    cout << endl;
    //demo 为二元谓词函数,是我们自定义的去重规则
    mylist.unique(demo);
    for (auto it = mylist.begin(); it != mylist.end(); ++it)
        std::cout << *it << ' ';
    return 0;
}
结果:
1 1.2 3 4 4.5 4.6
1 3 4

除此之外,通过将自定义的谓词函数(不限定参数个数)传给 remove_if() 成员函数,list 容器中能使谓词函数成立的元素都会被删除

#include <iostream>
#include <list>
using namespace std;
int main()
{
    
    
    std::list<int> mylist{
    
     15, 36, 7, 17, 20, 39, 4, 1 };
    //删除 mylist 容器中能够使 lamba 表达式成立的所有元素。
    mylist.remove_if([](int value) {
    
    return (value < 10); }); //{15 36 17 20 39}
    for (auto it = mylist.begin(); it != mylist.end(); ++it)
        std::cout << ' ' << *it;
    return 0;
}
结果:
15 36 17 20 39

9 - forward_list

forward_list 使用的是单链表,而 list 使用的是双向链表

效率高是选用 forward_list 而弃用 list 容器最主要的原因,换句话说,只要是 list 容器和 forward_list 容器都能实现的操作,应优先选择 forward_list 容器。

由于 forward_list 容器以模板类 forward_list(T 为存储元素的类型)的形式被包含在<forward_list>头文件中,并定义在 std 命名空间中。因此,在使用该容器之前,代码中需包含下面两行代码:

#include <forward_list>
using namespace std;

在这里插入图片描述

在这里插入图片描述

forward_list 容器中是不提供 size() 函数的,但如果想要获取 forward_list 容器中存储元素的个数,可以使用头文件 中的 distance() 函数

#include <iostream>
#include <forward_list>
#include <iterator>
using namespace std;
int main()
{
    
    
    std::forward_list<int> my_words{
    
    1,2,3,4};
    int count = std::distance(std::begin(my_words), std::end(my_words));
    cout << count;
    return 0;
}

Guess you like

Origin blog.csdn.net/weixin_44484715/article/details/115821762