STL list

I. list是什么

参考: list - C++ Reference

翻译成中文是这样的

List(列表)

列表是序列容器 ,支持在序列内任意位置进行**常数时间O(1)**的插入和删除操作,并且支持双向迭代。

list 容器的底层实现为双向链表;双向链表可以将其包含的每个元素存储在互不相关、分散的内存位置中。容器内部通过为每个元素维护「指向前驱元素的链接」和「指向后继元素的链接」,来保持元素的顺序。

它和 forward_list 非常相似:核心区别在于 forward_list单向链表,因此仅支持正向迭代,代价是占用空间更小、效率略高。

与其他基础标准序列容器(arrayvectordeque)相比,list 在「容器内任意位置插入、提取、移动元素(迭代器已定位的位置)」的场景中,性能通常更优,因此也更适合频繁执行这类操作的算法(比如排序算法)。

list 和 forward_list 相比其他序列容器的主要缺点是:不支持通过位置直接访问元素;例如,要访问 list 中的第 6 个元素,必须从一个已知位置(如开头或末尾)迭代到该位置,耗时与两个位置的距离呈线性关系。此外,它们还需要额外内存来存储每个元素的链接信息(对于存储小元素的大型列表,这可能是一个重要的性能开销)。


容器属性 (Container properties)

1. Sequence(序列型)

序列容器中的元素按严格的线性顺序排列,可通过元素在序列中的位置访问单个元素。

2. Doubly-linked list(双向链表)

每个元素都存储了定位下一个和上一个元素的信息,支持在指定元素之前 / 之后(甚至整个范围)进行常数时间的插入和删除操作,但不支持直接随机访问

3. Allocator-aware(分配器感知)

该容器使用分配器对象来动态管理其存储需求。

list存储结构

list容器底层为带头双向链表,带头双向循环链表每个节点包含两个指针,prev和next,前者指向前置结点,后者指向后置结点,并且包含一个哨兵结点(第一个元素节点,且不包含任何数据)

list容器定义

cpp 复制代码
list <typename> name;

这里的typename 可以是任何基本类型 ,例如int、double、char、结构体 等,也可以是STL标准容器 ,例如string、set、queue、vector 等。
注意: 使用前必须加上头文件: #include <list>

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
int main()
{
	int a[3];               // 正常定义的-----静态数组
 
	list<int> str_a;        // list 定义的 int类型的链表
 
	char b[3];
 
	list<char> str_b;
	return 0;
}

list容器常用接口的使用

list构造

我们单独看一下最后一个方式:使用迭代器拷贝构造

cpp 复制代码
string s("hello world");
list<char> lt4(s.begin(),s.end()); //构造string对象某段迭代器区间的内容

在C++11中,可以使用花括号构造内容

cpp 复制代码
list<int> lt{ 1,2,3,4,5 };  // 直接使用花括号进行构造---C++11允许

list的遍历及迭代器的操作

需要注意的是 :链表只能使用迭代器和范围for遍历

迭代器

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
int main()
{
	int array[] = { 1,2,3,4,5,6,7,8 };
	// 用已有的数组 初始化 list 链表
	list<int> lt(array, array + sizeof(array) / sizeof(array[0]));
	// 正向迭代器遍历容器
	list<int>::iterator it = lt.begin();
	// 开始循环遍历
	while (it != lt.end())
	{
		cout << *it << " ";    // 1 2 3 4 5 6 7 8
		it++;
	}
	cout << endl;
	return 0;
}

范围for

cpp 复制代码
#include <iostream>
#include <list>
#include <vector>
using namespace std;
int main()
{
	vector<int> v{ 1,2,3,4,5 };
	list<int> lt(v.begin(), v.end());
	// C++ 范围for 的方式遍历
 
	for (auto& e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	// 输出 :1 2 3 4 5
	return 0;
}

list容器常见操作

这些与之前string vector的 操作基本类似,就不做代码展示

list容量的常见访问操作

① front()------访问list头元素
cpp 复制代码
#include<iostream>
#include<list>
 
using std::cout;
using std::endl;
using std::list;
 
int main() {
    list<int> mylist {1, 2, 3, 4, 5, 6, 7, 8};
    cout<< "初始化后的mylist为:";
    for (auto num : mylist) {
        cout<< num<< " ";
    }
    int front = mylist.front();   // 1
 
    cout<< "\nmylist的头元素为:"<< front;
}
 
② back()------访问list尾元素
cpp 复制代码
#include<iostream>
#include<list>
 
using std::cout;
using std::endl;
using std::list;
 
int main() {
    list<int> mylist {1, 2, 3, 4, 5, 6, 7, 8};
    cout<< "初始化后的mylist为:";
    for (auto num : mylist) {
        cout<< num<< " ";
    }
    int front = mylist.front();  // 8
 
    cout<< "\nmylist的头元素为:"<< front;
}
 

list容器的常见修改操作

这里我们重点来看一看Insert 和 erase,在vector容器的实现中,我们了解到了这两者的实现中

存在迭代器失效的情况,解决方法是将返回值设置为迭代器类型

insert()------添加元素(任意位置

insert共有三种形式:

  • insert(iterator, value);
  • insert(iterator, num, value);
  • insert(iterator, iterator1, iterator2);

方法1和方法2 我们早就领教过了,我们这里来看一下方法三

insert(iterator, iterator1, iterator2)

使用**insert(iterator, iterator1, iterator2);**方法,作用是向iterator迭代器指向元素的前边添加[iterator1,iterator2)之间的元素。

cpp 复制代码
#include <list>
#include <iostream>
 
using std::cout;
using std::list;
using std::endl;
 
int main() {
    list<int> mylist{1, 2, 3, 4};
    cout << "初始化后的mylist为:";
    for (auto num : mylist) {
        cout << num << " ";
    }
    // it指向mylist的第二个元素
    list<int>::iterator it = mylist.begin() + 1;
    // 定义一个辅助list
    list<int> list2{10, 20, 30};
    // it1指向list2的第一个元素
    list<int>::iterator it1 = list2.begin();
    // it2指向list2的最后一个元素后一个位置
    list<int>::iterator it2 = list2.end();
    // 使用insert在2之前添加[it1,it2)之间的元素
    mylist.insert(it, it1, it2);
    cout << "\n使用insert插入元素后:";
    for (auto num : mylist) {
        cout << num << " ";
    }
}
erase()------删除元素(任意位置)

erase 共有 两 种形式:

  • list.erase(iterator)

  • list.erase(iterator1,iterator2)

方法1就是删除指定位置的元素

**方法2就是删除指定位置之间的元素,**包括iterator1指向的元素但不包括iterator2指向的元素,即擦除[iterator1,iterator2),左闭右开,在迭代器区间这是一个颠簸不破的原则

cpp 复制代码
#include <list>
#include <iostream>
 
using std::cout;
using std::list;
using std::endl;
 
int main() {
    list<int> mylist{1, 2, 3, 4};
    cout << "初始化后的mylist为:";
    for (auto num : mylist) {
        cout << num << " ";
    }
    // it1指向mylist的第二个元素
    list<int>::iterator it1 = mylist.begin() + 1;
    // it2指向mylist的最后一个元素后一个位置
    list<int>::iterator it2 = mylist.end();
 
    // 删除[it1,it2)之间的元素,即删除2,3,4
    mylist.erase(it1, it2);
    cout << "\n使用erase删除元素后:";
    for (auto num : mylist) {
        cout << num << " ";
    }
}

list 容器的常见操作函数

接下来这些是list容器区别于之前STL容器的操作函数

① splice()------从另一个list中移动元素

splice共有四种形式,分别为:

splice(iterator_pos, otherList) : 将otherList中的所有元素移动到iterator_pos指向元素之前
splice(iterator_pos, otherList, iter1): 从 otherList转移 iter1 指向的元素到当前list。元素被插入到 iterator_pos指向的元素之前。
splice(iterator_pos, otherList, iter_start, iter_end) : 从 otherList转移范围 [iter_start, iter_end) 中的元素到 当前列表。元素被插入到 iterator_pos指向的元素之前。

注意:

1. splice 操作不会进行元素的复制或移动,只是修改指针连接,因此效率较高。
2. 在 splice 操作后,被移动的元素将不再属于原始列表。
3. splice 操作后,原始列表和插入列表的大小会相应改变。
4. 插入操作的时间复杂度为 O(1)

cpp 复制代码
#include <iostream>
#include <list>
 
int main() {
    std::list<int> list1 = {1, 2, 3, 4, 5};
    std::list<int> list2 = {10, 20, 30};
    std::list<int> list3 = {1, 2, 3, 4, 5};
    std::list<int> list4 = {10, 20, 30};
    std::list<int> list5 = {1, 2, 3, 4, 5};
    std::list<int> list6 = {10, 20, 30};
 
    /*
       用法一 : splice(iterator_pos, otherList)
    */
    // 将list2中所有元素插入到list1的第一个位置
    list1.splice(list1.begin(), list2);
    // 输出结果
    std::cout<< "转移list2元素到list1之后的list1: ";
    for (int i : list1) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    std::cout<< "转移list2元素到list1之后的list1: ";
    for (int i : list2) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
 
    /*
        用法二:splice(iterator_pos, otherList, iter1)
    */
    auto it = list3.begin();
    // 将迭代器向后移动两个位置,指向第三个元素
    std::advance(it, 2);
    // 将list4的第一个元素(10)插入到list3的第三个位置
    list3.splice(it, list4, list4.begin());
    // 输出结果
    std::cout<< "转移list4元素到list3之后的list3: ";
    for (int i : list3) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    std::cout<< "转移list4元素到list3之后的list4: ";
    for (int i : list4) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    /*
        用法三:splice(iterator_pos, otherList, iter_start, iter_end)
    */
    // 将list5中第二个到第四个元素移动到list6的末尾
    auto first = list5.begin();
    std::advance(first, 1);
    auto last = list5.begin();
    std::advance(last, 4);
    list6.splice(list6.end(), list5, first, last);
 
    // 输出结果
    std::cout<< "转移list5元素到list6之后的list5: ";
    for (int i : list5) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    std::cout<< "转移list5元素到list6之后的list6: ";
    for (int i : list6) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}
② remove()------移除特定值的元素
cpp 复制代码
#include <iostream>
#include <list>
 
int main() {
    std::list<int> mylist {1, 2, 3, 4, 5};
    // 移除列表中所有值为2的元素
    mylist.remove(2);
    std::cout << "移除之后的mylist: ";
    for (const auto& element : mylist) {
        std::cout << element << " ";
    }
    std::cout << std::endl;
    return 0;
}
③ remove_if()------移除满足特定标准的元素
cpp 复制代码
使用方式:remove_if(func)

传入一个函数,满足这个函数就删除

④ unique()------删除连续的重复元素

unique()方法用于移除链表中相邻且重复的元素,仅保留一个副本

cpp 复制代码
#include <iostream>
#include <list>
 
int main() {
    std::list<int> myList = {1, 2, 2, 3, 3, 4, 5, 5, 5};
    std::cout << "初始化后的list为: ";
    for (auto i : myList) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    myList.unique();
    std::cout << "执行unique后的list为: ";
    for (auto i : myList) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}
⑤ sort()------对元素进行排序

sort操作我们在vector中有提到,需要传入的是随即迭代器,该迭代器类型满足之前迭代器的所有特性,简直万能啊

sort()方法用于对链表中的元素进行排序。默认情况下,它按升序对元素进行排序,使用元素类型的 < 运算符进行比较。示例如下:

cpp 复制代码
#include <iostream>
#include <list>
 
int main() {
    std::list<int> myList = {5, 3, 2, 4, 1};
    std::cout << "初始化后的list为 ";
    for (auto i : myList) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    myList.sort();
    std::cout << "排序后的list为: ";
    for (auto i : myList) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}
 
⑥ merge()------合并list

注意:merge()方法只能用于已排序的list。如果list未排序,则合并的结果将是不正确的。

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
int main() {
    std::list<int> list1 = {1, 3, 5};
    std::list<int> list2 = {2, 4, 6};
    cout << "初始化后的list1为:";
    for (auto num : list1) {
        cout << num << " ";
    }
    std::cout << std::endl;
    cout << "初始化后的list2为:";
    for (auto num : list2) {
        cout << num << " ";
    }
    std::cout << std::endl;
    // 将 list2 合并到 list1 中
    list1.merge(list2);
    // 输出合并后的列表
    cout << "合并后的list1为:";
    for (const auto& element : list1) {
        std::cout << element << " ";
    }
    std::cout << std::endl;
    return 0;
}
 
⑦ reverse()------将该链表的所有元素的顺序反转 【C++11】

不做展示

⑧ assign()------将值赋给容器

assign()方法用于将链表中的元素替换为新的元素序列。它可以接受不同形式的参数,提供了两种重载形式。

第一种形式接受迭代器范围作为参数,用于将另一个容器或数组中的元素复制到链表中。它会将链表清空,并将指定范围内的元素复制到链表中。void assign(InputIterator first, InputIterator last);
第二种形式接受一个元素数量和一个值作为参数,用于将指定数量的相同值的元素分配给链表。它会将链表清空,并将指定数量的元素赋值为指定的值。void assign(size_type count, const T& value);

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
 
int main() {
    std::list<int> myList;
    // 使用迭代器范围进行分配
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    myList.assign(numbers.begin(), numbers.end());
    std::cout << "第一次执行assign之后的list为: ";
    for (auto i : myList) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    // 使用元素数量和值进行分配
    myList.assign(3, 100);
    std::cout << "第二次执行assign之后的list为: ";
    for (auto i : myList) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}
 

vector 和 list 对比

对比维度 std::vector std::list
底层实现 动态数组,连续内存空间 双向链表,非连续内存节点
元素访问 支持随机访问operator[]/at()),时间复杂度 O(1) 仅支持顺序遍历访问 ,不支持随机访问,访问任意元素时间复杂度 O(n)
插入 / 删除操作 - 尾部插入 / 删除:平均 O(1) (扩容时为 O (n))- 中间 / 头部插入 / 删除:O(n)(需移动后续所有元素) - 任意已知迭代器位置插入 / 删除:O(1)(仅修改指针,无需移动元素)- 头部 / 尾部操作均为 O (1)
内存开销 仅存储元素本身,额外开销极低;预分配空间可能存在内存浪费 每个元素需额外存储两个指针(前驱、后继节点地址),内存开销大
迭代器稳定性 插入 / 删除操作可能导致迭代器失效(扩容、中间操作会使后续迭代器失效) 插入 / 删除操作仅使被删除元素的迭代器失效,其他迭代器完全稳定
缓存友好性 连续内存,CPU 缓存命中率极高,遍历速度极快 非连续内存,缓存命中率低,遍历速度远慢于 vector
适用场景 - 频繁随机访问元素- 主要在尾部插入 / 删除- 对内存占用敏感 - 频繁在任意位置插入 / 删除元素- 不依赖随机访问- 需保证迭代器稳定
排序性能 极快(随机访问 + 缓存友好,std::sort 最优适配) 极慢(无随机访问,std::sort 无法高效使用,需用成员函数sort()
空间扩容 自动预分配(通常 2 倍扩容),避免频繁内存申请 无预分配,按需分配节点,无内存浪费
相关推荐
水饺编程2 小时前
第4章,[标签 Win32] :SysMets3 程序讲解05,水平滚动
c语言·c++·windows·visual studio
前端老石人2 小时前
HTML 入门指南:从规范视角建立正确知识体系
开发语言·前端·html
lihao lihao2 小时前
进程地址空间
数据结构·c++·算法
Byte不洛2 小时前
LeetCode双指针经典题
c++·算法·leetcode·双指针
Tanecious.2 小时前
蓝桥杯备赛:Day7- P10424 [蓝桥杯 2024 省 B] 好数
c++·蓝桥杯
沐知全栈开发2 小时前
MySQL 索引
开发语言
Albert Edison2 小时前
【C++11】特殊类设计
开发语言·c++·单例模式·饿汉模式·懒汉模式
代码改善世界2 小时前
【C++初阶】vector 核心接口和模拟实现
开发语言·c++
Lyyaoo.2 小时前
【设计模式】工厂模式
java·开发语言·设计模式