【C++】List -- 详解(上)

简介: 【C++】List -- 详解(上)

一、list 的介绍及使用

cplusplus.com/reference/list/list/?kw=list

  1. list 是可以在常数时间 O(1) 内在任意位置进行插入删除的序列式容器,并且该容器可以前后双向迭代
  2. list 的底层是带头双向循环链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  3. list forward_list 非常相似:最主要的不同在于 forward_list 是单链表,只能朝前迭代,不支持尾插、尾删,对比双向链表的唯一优势就是每个节点少存一个指针。但是forward_list 在实际中用的很少,单链表尾插和尾删效率太低,尤其是尾删,不实用。
  4. 与其他的序列式容器相比(array,vector,deque),list 在容器内任意位置进行插入、删除、移动元素方面的执行效率通常表现更好。
  5. 与其他序列式容器相比,list 和 forward_list 最大的缺陷不支持任意位置的随机访问,比如:要访问 list 的第 6 个元素,必须从已知的位置(比如头部 / 尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大 list 来说这可能是一个重要的因素)。


1、list 的使用

list 中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为 list 中一些常见的重要接口。


(1)list 的构造

// list的构造
void TestL1()
{
    list<int> l1;                        // 构造空的l1
    list<int> l2(4, 100);                // l2中放4个值为100的元素
    list<int> l3(l2.begin(), l2.end());  // 用l2的[begin(), end())左闭右开的区间构造l3
    list<int> l4(l3);                    // 用l3拷贝构造l4
 
    // 以数组为迭代器区间构造l5
    int array[] = {16, 2, 77, 29};
    list<int> l5(array, array + sizeof(array) / sizeof(int));
 
    // 列表格式初始化C++11
    list<int> l6{1, 2, 3, 4, 5};
}

(2)迭代器的使用

可以暂时将迭代器理解成一个指针,该指针指向 list 中的某个节点

// list迭代器的使用
void PrintList(const list<int>& l)
{
    // 注意这里调用的是list的 begin() const,返回list的const_iterator对象
    for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it)
    {
        cout << *it << " ";
        // *it = 10; // 不能改变该值 -- 否则编译不通过
    }
    cout << endl;
}
void TestL2()
{
    int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    list<int> l(array, array + sizeof(array) / sizeof(array[0]));
 
    // 使用正向迭代器正向list中的元素
    // list<int>::iterator it = l.begin(); // C++98中语法
    auto it = l.begin();                   // C++11之后推荐写法
    while (it != l.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
 
    // 使用反向迭代器逆向打印list中的元素
    // list<int>::reverse_iterator rit = l.rbegin();
    auto rit = l.rbegin();
    while (rit != l.rend())
    {
        cout << *rit << " ";
        ++rit;
    }
    cout << endl;
 
    // 以数组为迭代器区间构造l5
    list<int> l(array, array + sizeof(array) / sizeof(int));
 
    // 用迭代器方式打印元素
    list<int>::iterator it = l.begin();
    while (it != l.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
 
    // C++11范围for的方式遍历
    for (auto& e : l)
    {
        cout << e << " ";
    }
    cout << endl;
}

遍历链表只能用迭代器范围for

注意

  1. begin 与 end 为正向迭代器,对迭代器执行 ++ 操作,迭代器移动
  2. rbegin(end) 与 rend(begin) 为反向迭代器,对迭代器执行 ++ 操作,迭代器移动

(3)容量操作

cplusplus.com/reference/list/list/empty/

cplusplus.com/reference/list/list/size/


(4)访问操作

list 不支持 [] 运算符,因为 list 容器物理地址不是连续的。

cplusplus.com/reference/list/list/front/

cplusplus.com/reference/list/list/back/


(5)修改操作

注意list 可以在任意位置进行 O(1) 插入删除操作。

// list插入和删除
// push_back/pop_back/push_front/pop_front
void TestL3()
{
    int array[] = {1, 2, 3};
    list<int> L(array, array + sizeof(array) / sizeof(array[0]));
    
    L.push_back(4); // 在list的尾部插入4
    L.push_front(0); // 在list的头部插入0
    PrintList(L);
    
    L.pop_back(); // 删除list尾部节点
    L.pop_front(); // 删除list头部节点
    PrintList(L);
}
// insert /erase 
void TestL4()
{
    int array1[] = {1, 2, 3};
    list<int> L(array1, array1 + sizeof(array1) / sizeof(array1[0]));
   
    auto pos = ++L.begin(); // 获取链表中第二个节点
    cout << *pos << endl;
  
    L.insert(pos, 4); // 在pos前插入值为4的元素
    PrintList(L);
   
    L.insert(pos, 5, 5); // 在pos前插入5个值为5的元素
    PrintList(L);
   
    vector<int> v{7, 8, 9};
    L.insert(pos, v.begin(), v.end()); // 在pos前插入[v.begin(), v.end)区间中的元素
    PrintList(L);
   
    L.erase(pos); // 删除pos位置上的元素
    PrintList(L);
   
    L.erase(L.begin(), L.end()); // 删除list中[begin, end)区间中的元素,即删除list中的所有元素
    PrintList(L);
}
为什么 C++98 建议使用各自容器里的 swap,而不建议使用算法里的 swap?

可以看到算法里 swap 的 C++98 的实现,无论是 string、vector、list 使用它都会涉及深拷贝问题,而且这里的深拷贝代价极大,需要深拷贝 3 次 —— 当 l1 和 l2 交换,这里会把 l1 拷贝构造一份 c,然后把 l2 赋值于 l1,c 赋值于 l2,完成交换。

而如果是容器里的 swap,需要交换 l1 和 l2,只需要头指针交换即可。假设是 vector,只要把 l1 和 l2 对应的 _start、_finish、_endofstorage 交换即可。相比算法里的 C++98 里的 swap,这里可以认为没有任何代价。

// resize/swap/clear
void TestL5()
{
    // 用数组来构造list
    int array1[] = {1, 2, 3};
    list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
    PrintList(l1);
   
    list<int> l2;
    l1.swap(l2); // 交换l1和l2中的元素
    PrintList(l1);
    PrintList(l2);
  
    l2.clear(); // 将l2中的元素清空
    cout << l2.size() << endl;
}

🔺list 的迭代器失效

先将迭代器暂时理解成类似于指针, 迭代器失效即迭代器所指向的节点无效,即该节点被删除了

因为 list 的底层结构为带头结点的双向循环链表,因此在 list 中进行 插入 时是 不会 导致 list 的迭代器失效的,只有在 删除 时才 失效,并且 失效的只是指向被删除节点的迭代器 ,其他迭代器不会受到影响

void TestListIterator()
{
    int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    list<int> l(array, array+sizeof(array)/sizeof(array[0]));
    auto it = l.begin();
    while (it != l.end())
    {
        // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
        l.erase(it); 
        ++it;
    }
}
 
// 改正
void TestListIterator()
{
    int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    list<int> l(array, array+sizeof(array)/sizeof(array[0]));
    auto it = l.begin();
    while (it != l.end())
    {
        l.erase(it++); // it = l.erase(it);
    }
}


【补充】
a. 容器迭代器的分类

容器迭代器的分类:

  1. 使用功能的角度可分为:正向 / 反向 + const。
  2. 容器底层结构的角度可分为:单向迭代器、双向迭代器、随机访问迭代器。

单链表 forward_list、哈希表 unordered_set/map 迭代器就是单向(Unidirectional Iterator),特征是能 ++,不能 --;

双向链表 list、红黑树 set/map 迭代器就是双向(Bidirectional Iterator),特征是能 ++ / --;

string、vector、deque 迭代器就是随机迭代器(Random Access Iterator),特征是能可以++ / -- / + / -,一般随机迭代器底层都是一个连续的空间。

迭代器的本质是:

在不暴露容器底层实现细节的情况下,提供了一种统一的方式来访问 / 修改容器中存储的数据。

容器、迭代器、算法间的关系:

算法不能直接访问和修改容器中存储的数据,而迭代器就是容器和算法之间的胶合剂。

stl_iterator 源码中对于迭代器的分类:


b. 迭代器的实现方式分析

迭代器有两种实现方式,具体应根据容器底层数据结构的实现来确定。

因为每个特定的容器,都有特定的内部内存结构,也就意味着遍历 / 迭代过程的细节是不一样的。

方式一:前提条件:原生指针指向的空间物理地址是连续的,比如 string、vector

  • 所以让原生指针 ++ 可以直接走到下一个位置,-- 可以直接回到上一个位置,* 解引用可以直接得到这个位置的数据。比如 vector:
  • 此时的原生指针就是一个天然的迭代器(但并不是原生指针厉害,而是因为它指向的空间物理地址是连续的)。

方式二:原生指针指向的空间物理地址不连续,比如:list、map / set

  • 所以让原生指针 ++ 不能直接走到下一个位置,-- 不能直接回到上一个位置,* 解引用不能直接得到这个位置的数据,只能得到一个结构体(结构体中包含数据和指针域)。比如 list:
  • 此时原生指针不是一个天然的迭代器,所以我们需要用一个类封装原生指针,重载相关运算符,控制对象使用这些运算符的行为,让这个类的对象,用起来像指针一样
  • 为了让迭代器的使用形式与指针完全相同,因此在自定义的类中必须实现以下方法:
  1. 指针可以解引用,则迭代器的类中必须重载 operator*()。
  2. 指针可以通过 -> 访问其所指空间成员,则迭代器类中必须重载 oprator->()。
  3. 指针可以 ++ 向后移动,则迭代器类中必须重载 operator++() 与 operator++(int) ,至于 operator–() / operator–(int) 是否需要重载,要根据容器具体的底层结构来抉择,比如 list 双向链表可以向前移动,所以需要重载 operator--() 和 operator--(int),如果是 forward_list 就不需要重载 --。
  4. 迭代器需要进行是否相等的比较,因此还需要重载 operator==() 与 operator!=()。

(6)容器操作(了解)
a. sort

cplusplus.com/reference/list/list/sort/

  • 等价元素的结果顺序是稳定的:即等价元素保留它们在调用之前的相对顺序。
  • 整个操作不涉及任何元素对象的构造、销毁或复制。元素在容器内移动。

sort() 对 list 容器中的元素进行排序,效率很低链表排序也没有太大的作用和价值

函数默认是排升序(<),如果想要排降序(>),需要传仿函数。

void test()
{
    int arr[] = { 10, 2, 5 };
  list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
 
  // > 排降序
    // 写法一:
  greater<int> g; // 类模板,在<functional>头文件中
  lt.sort(g);
 
    // 写法二:
  lt.sort(greater<int>()); // 匿名对象 
  // lt: 10 5 2    
}

b. unique

cplusplus.com/reference/list/list/unique/

去重,必须要先排序,才能去完重复值。

void test()
{
    int arr[] = { 2, 4, 3, 2, 4 };
  list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
  lt.sort(); // 去重前,必须要先排序
  lt.unique(); // 去除重复值
    // lt: 2 3 4
}

c. remove

cplusplus.com/reference/list/list/remove/

从容器中移除所有值等于 val 的元素。

void test()
{
  int arr[] = { 1, 2, 3, 3, 4 };
  list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
  lt.remove(3); // lt: 1 2 4
}

d. reverse

cplusplus.com/reference/list/list/reverse/

反转容器中元素的顺序。  

void test()
{
  int arr[] = { 1, 2, 3, 4, 5 };
  list<int> lt(arr, arr + sizeof(arr) / sizeof(int));
    lt.reverse(); // rlt: 5 4 3 2 1
}

e. merge

cplusplus.com/reference/list/list/merge/

合并排序序列(合并前两个 list 容器都要排好序)。


【C++】List -- 详解(下)https://developer.aliyun.com/article/1514688?spm=a2c6h.13148508.setting.24.4b904f0ejdbHoA

相关文章
|
1月前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
50 2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
|
1月前
|
存储 算法 C++
【C++打怪之路Lv10】-- list
【C++打怪之路Lv10】-- list
20 1
|
1月前
|
存储 C++ 容器
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器1
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
54 5
|
1月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
54 2
|
1月前
|
C++
【C++】C++ STL 探索:List使用与背后底层逻辑(三)
【C++】C++ STL 探索:List使用与背后底层逻辑
|
1月前
|
C++
【C++】C++ STL 探索:List使用与背后底层逻辑(二)
【C++】C++ STL 探索:List使用与背后底层逻辑
|
1月前
|
存储 编译器 C++
【C++】C++ STL 探索:List使用与背后底层逻辑(一)
【C++】C++ STL 探索:List使用与背后底层逻辑
|
1月前
|
存储 缓存 C++
C++番外篇——list与vector的比较
C++番外篇——list与vector的比较
21 0
|
1月前
|
C++
C++番外篇——list的实现
C++番外篇——list的实现
19 0
|
1月前
|
存储 C++ 容器
C++入门9——list的使用
C++入门9——list的使用
18 0