【C++初阶:STL —— list】list的介绍及使用 | list的深度剖析及模拟实现 | list与vector的对比 上

简介: 【C++初阶:STL —— list】list的介绍及使用 | list的深度剖析及模拟实现 | list与vector的对比

文章目录

【写在前面】

在学完 list,大家对 STL 中的迭代器的认知会进一步提高。list 用的虽然不多,但是它的底层有很多经典的东西,尤其是它的迭代器。list 的结构对我们来说应该问题不大,因为在《数据结构》时我们就已经了解过链表了,它的结构是一个带头双向循环链表,之前我们也实现过。

对于 list 没有 reserve 和 resize,因为它的底层不是连续的空间,它是用一个申请一个,不用一个就释放一个。也没有 operator[],因为它不支持随机访问。同时它有头插、头删、尾插、尾删、任意位置的插入、删除。因为 list 是带头双向循环链表

有了前面 string 和 vector 的铺垫,我们这里对于 list 的使用就大概过一下即可,因为它们比较类似,重点主要放在 list 的深度剖析及模拟实现。

其实来严格来说 C++ 的 list 有两个:

  1. <list> 是带头双向循环链表,是我们本章需要学习的知识
  2. <forward_list> 是单链表,它是 C++11 所增加的,它的使用场景一点也不多,查看文档,可以看到它不支持尾插、尾删,因为对于单链表效率很低。并且它的任意位置插入、删除是在当前位置之后,因为当前位置之前得找前一个,也是一个 O(N) 的实现。单链表对比双向链表的唯一优势就是每个节点少存一个指针。

一、list的介绍及使用

💦 list的介绍

list的文档介绍

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

💦 list的使用

1、list的构造
constructor 接口说明
list() 构造空的 list
list(size_type n, const value_type& val = value_type()) 构造 list 中包含 n 个值为 val 的元素
list(const list& x) 拷贝构造函数
list(InputIterator first, InputIterator last) 用 (first, last) 区间中的元素构造 list
#include<iostream>
#include<vector>
#include<list>
using namespace std;
namespace std
{
  void test_list1()
  {
    list<int> lt1;
    list<int> lt2(10, 5);
    list<int> lt3(lt2.begin(), lt2.end());
    //同样支持用其它容器的迭代器区间去构造,因为它是模板,并且它可以支持如下写法
    vector<int> v = { 1, 2, 3, 4, 5 };
    list<int> lt4(v.begin(), v.end());  
    list<int> lt5(lt4);
  }
}
int main()
{
  std::test_list1();
  return 0; 
}
2、list iterator的使用
iterator 接口说明
begin + end 返回第一个元素的迭代器 + 返回最后一个元素下一个位置的迭代器
rbegin + rend 返回第一个元素的 reserve_iterator,即 end 位置,返回最后一个元素下一个位置的 reverse_iterator,即 begin 位置
#include<iostream>
#include<vector>
#include<list>
using namespace std;
namespace std
{
  void test_list1()
  {
    vector<int> v = { 1, 2, 3, 4, 5 };
    list<int> lt(v.begin(), v.end());
    list<int>::iterator it1 = lt.begin();
    //注意对于string和vector我们可以用小于或不等于做为判断条件,但是list只能用不等于做为判断条件,这时因为不同的容器中空间连续与否的原因
    while(it1 != lt.end())
    {
      cout << *it1 << " ";
      ++it1;  
    }
    cout << endl;
    list<int>::reverse_iterator it2 = lt.rbegin();
    while(it2 != lt.rend())
    {
      cout << *it2 << " ";
      ++it2;  
    }
    cout << endl;
    //list里已经不再支持operator[]
    for(auto e : lt)
    {
      cout << e << " ";
    }
    cout << endl;
  }
}
int main()
{
  std::test_list1();
  return 0; 
}
3、list capacity
capacity 接口说明
empty 检测 list 是否为空,是返回 true,否则返回 false
size 返回 list 中有效节点的个数
4、list element access
element access 接口说明
front 返回 list 的第一个节点的中值的引用
back 返回 list 的最后一个节点中值的引用
5、list modifiers
modifiers 接口说明
push_front 在 list 首元素前插入值为 val 的元素
pop_front 删除 list 中第一个元素
push_back 在 list 尾部插入值为 val 的元素
pop_back 删除 list 中最后一个元素
insert 在 list position 位置中插入值为 val 的元素
erase 删除 list position 位置的元素
swap 交换两个 list 中的元素
clear 清空 list 中的有效元素
#include<iostream>
#include<algorithm>
#include<vector>
#include<list>
#include<functional>
#include<time.h>
using namespace std;
namespace std
{
  void test_list1()
  {
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    lt.push_front(10);
    lt.push_front(20);
    lt.push_front(30);
    lt.push_front(40);
    for(auto e : lt)
    {
      cout << e << " "; 
    }
    cout << endl;
    lt.pop_back();
    lt.pop_back();
    lt.pop_back();
    lt.pop_back();
    lt.pop_front();
    lt.pop_front();
    lt.pop_front();
    lt.pop_front();
    //lt.pop_front();//err,注意在头删、尾删时要保证list里还有数据,否则这里会报断言错误
    for(auto e : lt)
    {
      cout << e << " "; 
    }
  }
  void test_list2()
  {
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    //list里也没有提供find,所以这里使用algorithm里的
    list<int>::iterator pos = find(lt.begin(), lt.end(), 2);
    if(pos != lt.end())
    {
      lt.insert(pos, 20);
    }
    for (auto e : lt)
    {
      cout << e << " ";
    }
    cout << endl;
    //clear并不会把头节点清除,这里还可以继续push_back
    lt.clear();
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    for(auto e : lt)
    {
      cout << e << " "; 
    }
    cout << endl;
  }
  void test_list3()
  {
    list<int> lt1;
    lt1.push_back(1);
    lt1.push_back(2);
    list<int> lt2;
    lt2.push_back(2);
    lt2.push_back(1);
    list<int> lt3;
    lt3.push_back(5);
    lt3.push_back(1);
    lt3.push_back(3);
    lt3.push_back(3);
    //对于swap,在C++98中建议使用容器里的,而不建议使用算法里的。它们效果一样,但是效率不一样,具体见如下说明
    lt1.swap(lt2);
    //swap(lt1, lt2);
    for(auto e : lt1)
    {
      cout << e << " "; 
    }
    cout << endl;
    for(auto e : lt2)
    {
      cout << e << " "; 
    }
    cout << endl;
    //注意所有的排序都满足,>是降序,<是升序,这里默认是升序
    //这个也是一个类模板,它是一个仿函数,所在头<functional>后面我们会实现,sort所在头<algorithm>
    /*greater<int> g;
    lt3.sort(g);*/
    lt3.sort(greater<int>());//同上,可以直接写成匿名对象
    for(auto e : lt3)
    {
      cout << e << " "; 
    }
    cout << endl;
    //unique的功能是去重,所在头<algorithm>,去重的前提是排序,升序降序都行,如果不排序它只去去相邻的数据
    lt3.unique();
    for(auto e : lt3)
    {
      cout << e << " "; 
    }
    cout << endl;
    //erase需要先find,而remove可以直接删除,有就全删,没有就不删,所在头<algorithm>
    lt3.remove(3);
    for (auto e : lt3)
    {
      cout << e << " ";
    }
    cout << endl;
    //reverse的功能是逆置,对于带头双向循环链表的逆置比单链表简单的多,所在头<algorithm>
    lt3.reverse();
    for (auto e : lt3)
    {
      cout << e << " ";
    }
    cout << endl;
    //merge的功能是合并
    //splice的功能是转移,它转移的是节点不是数据,很特殊的场景下才会使用到,我们以后在了解LRU可能还会再接触到
  }
  void test_OP()
  {
    srand(time(0));
    const int N = 10000;
    list<int> lt;
    vector<int> v;
    v.resize(N);
    for (int i = 0; i < N; i++)
    {
      v[i] = rand();
      lt.push_back(v[i]);
    }
    int begin1 = clock();
    sort(v.begin(), v.end());//vector用算法里的,它底层使用的是快排的三数取中法
    int end1 = clock();
    int begin2 = clock();
    lt.sort();//list用容器里的
    //sort(lt.begin(), lt.end());//err,本质sort会用两个迭代器相减,而list的迭代器不支持减
    int end2 = clock();
    cout << "vector sort:" << end1 - begin1 << endl;
    cout << "list sort:" << end2 - begin2 << endl;
  }
}
int main()
{
  std::test_list1();
  std::test_list2();
  std::test_list3();
  std::test_OP();
  return 0; 
}

📝说明

  1. List item
    为什么 C++98 建议使用各自容器里的 swap,而不建议使用算法里的 swap ❓
    如下可以看到算法里 swap 的 C++98 的实现,无论是 string、vector、list 使用它会涉及深拷贝问题,而且这里的深拷贝代价极大,需要深拷贝 3 次 —— 当 lt1 和 lt2 交换,这里会把 lt1 拷贝构造一份 c,然后把 lt2 赋值于 lt1,c 赋值于 lt2,完成交换。
    而如果是容器里的 swap,需要交换 lt1 和 lt2,只需要头指针交换即可。假设是 vector,只要把 lt1 和 lt2 对应的 _start、_finish、_endofstorage 交换即可。相比算法里的 C++98 里的 swap,这里可以认为没有任何代价。
    所以说,我们为什么在以后工作中多数不会去写数据结构,而还要学《数据结构》这门学科。因为如果你没有自己去实现数据结构,你不了解链表的结构,我跟你说这 2 个 swap 有深拷贝差异,你可能都听不懂,没有学过《数据结构》的人永远不能理解为什么 top-k 问题用了堆好像效率就高很多。所以我们从 C 语言一路走来,各种各样的模拟实现(小的有 strstr、strcmp;大的有二叉树、list),其实不是为了造更好的轮子,而是能更好的理解并高效的使用。

  2. List item
    迭代器补充 ❗
    容器迭代器的分类:
    a) 使用功能的角度可分为,(正向、反向) + const
    b) 容器底层结构的角度可分为,单向、双向、随机
      比如单链表迭代器、哈希表迭代器就是单向,特征是能 ++,不能 --;双向链表迭代器、map 迭代器就是双向,特征是能 ++、–;string、vector、deque 迭代器就是随机迭代器,特征是能 ++、–、+、-,一般随机迭代器底层都是一个连续的空间。
    可以看到算法里的 sort、reverse 的声明,它的模板参数的命名不是 T,也不是 iterator,对于模板参数的命名可以任意,但是它的命名是有含意的。比如说你要用 sort 这个算法,你要用的是随机迭代器;你要用 reverse 这个算法,你要用的是双向迭代器。随机迭代器也可以使用 reverse,因为随机迭代器是一个双向迭代器,因为它满足双向迭代器的所有功能;同理,双向迭代器也是单向迭代器、随机迭代器也是单向迭代器。也就意味着这里模板参数的命令是单向迭代器,那么你的容器可以是单向、双向、随机;模板参数的命令是双向迭代器,那么你的容器可以是双向、随机;模板参数的命令是随机迭代器,那么你的容器可以是随机。后面学了继承,就可以知道它们满足一个继承关系。

    所以这里要明白一个关系

6、list迭代器失效

前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点无效,即该节点被删除了。因为 list 的底层结构为带头结点的双向循环链表,因此在 list 中进行插入时是不会导致 list 的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

#include<iostream>
#include<algorithm>
#include<list>
using namespace std;
namespace std
{
  void test_list1()
  {
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> lt(array, array + sizeof(array) / sizeof(array[0]));
    list<int>::iterator pos = find(lt.begin(), lt.end(), 5);
    if(pos != lt.end())
    {
      //不会失效
      lt.insert(pos, 45);
    }
    for(auto e : lt)
    {
      cout << e << " "; 
    }
    cout << endl;
  }
  void test_list2()
  {
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> lt(array, array + sizeof(array) / sizeof(array[0]));
    list<int>::iterator pos = find(lt.begin(), lt.end(), 5);
    if(pos != lt.end())
    {
      //会失效,可以重新指定迭代器pos
      lt.erase(pos);
      //pos = lt.erase(pos);//ok
    }
    cout << *pos << endl;
    cout << endl;
  }
}
int main()
{
  std::test_list1();
  std::test_list2();
  return 0; 
}

📝说明


相关文章
|
6月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
342 2
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
687 73
|
10月前
|
存储 DataX C语言
vector与list的简单介绍
vector是表示大小可以变化的数组的序列容器。就像数组一样,vector对其元素使用连续的存储位置,这意味着也可以使用指向其元素的常规指针上的偏移量来访问其元素,并且与数组中的元素一样高效。但与数组不同的是,它们的大小可以动态变化,它们的存储由容器自动处理。在内部,vector使用动态分配的数组来存储其元素。当插入新元素时,可能需要重新分配此数组才能增大大小,这意味着分配一个新数组并将所有元素移动到该数组。
351 0
vector与list的简单介绍
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
551 3
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
865 1
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
12月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
459 12
|
10月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
249 0
|
10月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
387 0