【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; 
}

📝说明


相关文章
|
2月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
9月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
272 2
|
9月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
506 73
|
6月前
|
存储 DataX C语言
vector与list的简单介绍
vector是表示大小可以变化的数组的序列容器。就像数组一样,vector对其元素使用连续的存储位置,这意味着也可以使用指向其元素的常规指针上的偏移量来访问其元素,并且与数组中的元素一样高效。但与数组不同的是,它们的大小可以动态变化,它们的存储由容器自动处理。在内部,vector使用动态分配的数组来存储其元素。当插入新元素时,可能需要重新分配此数组才能增大大小,这意味着分配一个新数组并将所有元素移动到该数组。
285 0
vector与list的简单介绍
|
10月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
9月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
408 3
|
10月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
616 1
|
10月前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
1365 1
|
运维 关系型数据库 Java
PolarDB产品使用问题之使用List或Range分区表时,Java代码是否需要进行改动
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。