list 的使用:把节点、位置和操作联系起来的综合叙述(上)

文章目录

    • [一、从链表认识 list](#一、从链表认识 list)
    • 二、不同构造方式
      • [2.1 比较一些容易混淆的构造方式](#2.1 比较一些容易混淆的构造方式)
      • [2.2 打印构造结果,验证复制是否独立](#2.2 打印构造结果,验证复制是否独立)
    • 三、访问与遍历:迭代器是位置,不是下标
      • [3.1 front() / back() 返回元素引用](#3.1 front() / back() 返回元素引用)
      • [3.2 begin() / end() 表示半开区间](#3.2 begin() / end() 表示半开区间)
      • [3.3 遍历方式](#3.3 遍历方式)
    • 四、增删改查:先找位置,再执行操作
      • [4.1 修改元素与修改序列](#4.1 修改元素与修改序列)
    • [4.2 自定义类型插入:push_back 与 emplace_back](#4.2 自定义类型插入:push_back 与 emplace_back)
    • 五、迭代器有效性与遍历删除
      • [5.1 list 的稳定性来自节点,而不是元素数值](#5.1 list 的稳定性来自节点,而不是元素数值)
      • [5.2 删除当前元素后,让返回值接管遍历位置](#5.2 删除当前元素后,让返回值接管遍历位置)
    • 六、排序、去重、合并与节点转移
      • [6.1 为什么 list 有成员 sort](#6.1 为什么 list 有成员 sort)
      • [6.2 reverse 保持位置,不等于保持遍历顺序](#6.2 reverse 保持位置,不等于保持遍历顺序)
      • [6.3 unique 相邻去重,完全去重前提是有序](#6.3 unique 相邻去重,完全去重前提是有序)
      • [6.4 merge 的前提是两个序列按相同规则有序](#6.4 merge 的前提是两个序列按相同规则有序)
      • [6.5 splice 转移节点:单个节点,或是一整段](#6.5 splice 转移节点:单个节点,或是一整段)
    • [七、list 的相关功能成本](#七、list 的相关功能成本)

代码仓库:《list类测试与实现》

一、从链表认识 list


std::list<T> 是标准库的双向序列容器,包含头文件 <list>,T 表示元素类型。

带哨兵位双向循环链表的每个节点保存一个元素,并通过连接找到前后节点。各元素在物理空间存储时不要求位于连续地址,所以不能通过"起始地址加偏移"找到第几个元素,也不能用 [] 或 at() 按下标访问。

创建局部 list 对象后,容器负责创建和销毁元素、管理节点存储;对象离开作用域时自动清理。

二、不同构造方式


2.1 比较一些容易混淆的构造方式

和 string 与 vector一样,list 也有着丰富的构造方式:

写法 初始元素 要留意的地方
std::list<int> l; 空 对象存在,但没有元素
std::list<int> l(5); 五个 0 创建五个元素,不是预留五个位置
std::list<int> l(5, 2); 五个 2 圆括号:数量与初值
std::list<int> l{5, 2}; 5、2 花括号:这里选中元素列表构造
std::list<int> b(a); 复制 a 的元素 节点独立管理
std::list<int> b = a; 同样复制 a 新对象的初始化,不是已有对象赋值
std::list<int> l(first, last); 复制 [first,last) 不包含 last 位置

整数的数量构造得到 0;自定义类型则按对应构造规则创建元素。区间构造必须提供有效范围,不能跨两个无关容器取起点和终点。

2.2 打印构造结果,验证复制是否独立

cpp 复制代码
#include <iostream>
#include <list>

int main()
{
    //使用循环给l1初始化
    std::list<int> l1;
    for (int i = 0; i < 5; ++i) l1.push_back(1);
    
    //初始化l2 5个2,再拷贝构造给l3
    std::list<int> l2(5, 2);
    std::list<int> l3(l2);
    
    //使用已有容器构造
    int a[] = {3, 4, 5};
    std::list<int> l4(a, a + 3);
    
    //使用列表初始化
    std::list<int> l5{5, 2};
    std::list<int> l6(5);
    std::cout << l6.size() << ' ' << l6.front() << std::endl;
    
    //依次打印查看l3、l4、l5
    for (int x : l3) std::cout << x << ' ';
    std::cout << std::endl;
    for (int x : l4) std::cout << x << ' ';
    std::cout << std::endl;
    for (int x : l5) std::cout << x << ' ';
    std::cout << std::endl;
    
    l3.front() = 9;
    std::cout << l2.front() << ' ' << l3.front() << std::endl;
    return 0;
}

运行示例:

  1. std::list<int> l6(5); → 创建5 个元素,值初始化为 0 ,输出 5 0
  2. l2(5,2):5 个 2;l3(l2) 拷贝构造 ,l3为独立链表,元素与l2相同
  3. l4(a, a+3):区间[a,a+3),数组{3,4,5} -> 3 4 5
  4. l5{5,2}:花括号初始化列表,两个元素:5、2
  5. l3.front() = 9; 修改 l3 第一个节点,l2.front()仍然是2,l3.front()变成9

三、访问与遍历:迭代器是位置,不是下标


3.1 front() / back() 返回元素引用

非 const 容器的 front()、back() 返回的是可修改引用,因此 l.front() = 10 会改变元素。const 容器则返回只读引用。

cpp 复制代码
std::list<int> l;
if (!l.empty())
    l.back() = 10;

empty() 判断是否为空;size() 返回当前元素数量。list 没有 capacity() 和 reserve();节点随着元素增删来管理。

3.2 begin() / end() 表示半开区间

begin() 指向首元素,end() 是尾后位置。空链表中二者相等。

list 的迭代器是双向迭代器 :支持 *it、it->成员、++it、--it、相等比较。但它不支持随机跳转和大小关系比较,因此 it + 2、it1 - it2、it < end() 都不是正确写法。

3.3 遍历方式

与 vector 不同的是,list迭代器并不支持下标访问或者加减一个数值来得到元素位置。

cpp 复制代码
#include <iostream>
#include <list>

int main()
{
	 std::list<int> l1;
	 l1.push_back(1);
	 l1.push_back(1);
	 l1.push_back(1);
	 l1.push_back(1);
	 l1.push_back(1);
	 for (auto n : l1)
	 {
	  	std::cout << n << ' ';
	 }
	 std::cout << std::endl;

	 std::list<int> l2(5, 2);
	 std::list<int> l3(l2);
	
	 std::list<int>::iterator it = l3.begin();
	 while (it != l3.end())
	 {
		 std::cout << *it << ' ';
		 ++it;
	 }
	 std::cout << std::endl;
	 return 0;
}

运行示例:

  1. 范围 for for(auto n : l1)

  2. 迭代器 while (it != l3.end ())

    • list迭代器:双向迭代器 ,只支持 ++it / --it,不支持 it + 2 这种随机访问。
    • 判断条件:it != l3.end(),不能用 < list 迭代器不支持大小比较,只能相等 / 不等判断。
容器 迭代器类型 支持运算
vector 随机访问迭代器 it < end()、it + n
list 双向迭代器 只能++it --it,禁止 < > 比较

四、增删改查:先找位置,再执行操作


4.1 修改元素与修改序列

操作 效果与返回值 边界
push_front(x) / push_back(x) 头插 / 尾插;返回 void 空链表也可使用
pop_front() / pop_back() 删除首 / 尾元素;返回 void 必须非空
insert(pos,x) 在 pos 前插入,返回新元素迭代器 pos 可为 end
insert(pos,n,x) 在 pos 前插入 n 个 x 注意 n 是数量
insert(pos,first,last) 插入指定来源范围的副本 来源范围有效,不能来自同一个目标 list
erase(pos) 删除 pos,返回后继 pos 必须指向本容器元素,不能是 end
erase(first,last) 删除 [first,last),返回后续位置 空范围可删除;范围必须有效
clear() 删除全部元素 空容器也可调用
resize(n,x) 缩小时删尾部,增大时追加 x 无 x 时按默认规则初始化新元素
assign(n,x) / assign(first,last) 用新内容替换全部旧内容 区间版本不使用本容器自身迭代器作为来源
swap(other) 交换两个容器的内容 不是逐个复制元素

4.2 自定义类型插入:push_back 与 emplace_back

cpp 复制代码
#include <iostream>
#include <list>

struct A
{
    int _a1;
    int _a2;
    A(int a1 = 0, int a2 = 1) : _a1(a1), _a2(a2) {}
};
int main()
{
    std::list<A> l;
    A a(1, 10);
    l.push_back(a);
    l.push_back(A(2, 20));
    l.emplace_back(3, 30);
    for (auto it = l.begin(); it != l.end(); ++it)
        std::cout << it->_a1 << ' ' << it->_a2 << std::endl;
}

push_back 只接收一个元素对象;而emplace_back(3,30) 会把参数用于在节点中构造 A。

这里只需掌握调用层面的差别,不展开底层参数转发。

五、迭代器有效性与遍历删除


5.1 list 的稳定性来自节点,而不是元素数值

  • 插入一个节点,原节点无需整体搬移,已有元素的迭代器和引用也保持有效。
  • 删除一个节点,只让指向被删元素的迭代器、引用和指针失效。但即使后来在相同地址新建节点,也不能据此继续使用旧迭代器。
操作 指向元素的迭代器 / 引用 / 指针
插入、头尾插、emplace 原有元素的位置保持有效
erase、pop、remove、unique 指向被删除元素的失效,其余保留
clear 所有元素位置失效
resize 增大保留原有位置;缩小使被删尾部位置失效
成员sort、成员 reverse 元素位置保持有效,遍历顺序可改变
splice、merge 被转移元素的位置仍有效,但归属变为目标容器
swap 元素位置仍有效,但元素归属交换
assign、复制赋值 不依赖操作前的元素位置,完成后重新获取

5.2 删除当前元素后,让返回值接管遍历位置

cpp 复制代码
#include <iostream>
#include <list>

int main()
{
    std::list<int> l{1, 2, 3, 4, 5, 6};
    //取list开头位置迭代器
    auto saved = l.begin();
    //取开头迭代器地址
    int* address = &*saved;
    //观察插入删除开头元素后迭代器地址是否变化
    l.insert(saved, 0);
    std::cout << (*saved == 1) << ' ' << (&*saved == address) << std::endl;
    l.erase(l.begin());
    std::cout << (*saved == 1) << ' ' << (&*saved == address) << std::endl;
    
    auto it = l.begin();
    while (it != l.end())
    {
        //删除list中的偶数
        if (*it % 2 == 0) 
        {
            it = l.erase(it);
        }
        else ++it;
    }
    //遍历打印此时list元素
    for (int x : l) std::cout << x << ' ';
    std::cout << std::endl;
    //再次观察此时开头元素迭代器地址变化
    std::cout << *saved << ' ' << (&*saved == address) << std::endl;
    return 0;
}

运行示例:

  1. saved = l.begin() 指向元素1,address保存1的内存地址。
    l.insert(saved, 0); 在 1 前面插入 0

list 插入不失效原有迭代器

saved依旧指向节点1,节点内存地址没变,所以*saved==1、&*saved == address,输出1 1。

  1. l.erase(l.begin()); 删除链表最前面的0。
    删除的是另一个节点 0 ,不是 saved 指向的节点 1。
    saved迭代器依旧有效,节点内存没有动,输出1 1。

erase只会让被删掉的那个迭代器失效 ,别的迭代器完好。这里 erase 的是begin()(节点 0),不是saved(节点 1)。

  1. while 循环删除偶数

原链表:1,2,3,4,5,6 → 删除偶数后剩下:1 3 5。

  1. 最后打印saved,它依旧指向节点 1,地址不变,输出1 1。

六、排序、去重、合并与节点转移


6.1 为什么 list 有成员 sort

算法标准库中的std::sort 要求随机访问迭代器,不能用于 list。需要对 list 元素排序时应使用 l.sort(),默认按升序排列;需要降序可写 l.sort(std::greater<int>()),包含 <functional>。

std::greater<int>() 生成一个比较对象,判断左边是否大于右边。暂时把它理解成排序规则即可。

成员 sort 是稳定排序,比较意义下相等的元素保持原相对顺序,比较次数为 O(N log N)。

6.2 reverse 保持位置,不等于保持遍历顺序

l.reverse() 反转序列。已有元素迭代器继续关联原元素。

std::reverse(l.begin(),l.end()) 也可用于 list,因为这个算法只要求双向迭代器,但它通过交换元素值工作。因此一个有效迭代器仍指向原来的元素对象,读到的值却可能改变。

写法 做什么 迭代器有效性 *it 的值
l.reverse() 调换链表指针,节点不动 迭代器有效,指向原元素 *it不变
std::reverse(l.begin(),l.end()) 交换元素的值 迭代器有效 *it被改写

两种写法迭代器都有效,但语义不一样。

6.3 unique 相邻去重,完全去重前提是有序

对 1 1 2 1 1 3 执行 unique 得到 1 2 1 3:两段连续的 1 各保留一个,分隔开的 1 不会合并。这样可以尽量可能保证原链表中的元素顺序不被打乱。

如果需求是不关心原顺序、相同值全局只留一个,可以先使用 sort 再 unique,把相同值聚到一起之后再去重。

6.4 merge 的前提是两个序列按相同规则有序

first.merge(second) 是把 second 的节点并入 first,然后得到有序序列,执行完后 second 变空;这不是简单把 second 接到尾部,而是把两个有序链表合并成一个有序链表。

合并保持元素迭代器有效,被转移的元素现在属于 first。默认合并不会把重复值自动去掉。

cpp 复制代码
#include <algorithm>
#include <functional>
#include <iostream>
#include <list>

void print(const std::list<int>& l)
{
    for (int x : l) std::cout << x << ' ';
    std::cout << std::endl;
}

int main()
{
    //初始化list
    std::list<int> l{ 1, 1, 2, 1, 1, 3 };
    //未排序去重
    l.unique();
    print(l);
    //排序后去重
    l.sort();
    l.unique();
    print(l);
    //成员sort默认升序,使用仿函数改为降序
    l.sort(std::greater<int>());
    print(l);

    auto it = l.begin();
    int* address = &*it;
    //反转链表后观察开头元素迭代器地址变化
    l.reverse();
    std::cout << *it << ' ' << (&*it == address) << std::endl;
    std::reverse(l.begin(), l.end());
    std::cout << *it << ' ' << (&*it == address) << std::endl;

    //初始化两个相同规则的有序链表,合并后打印情况
    std::list<int> first{ 1, 3, 5 };
    std::list<int> second{ 2, 4, 6 };
    first.merge(second);
    print(first);
    std::cout << second.empty() << std::endl;
}

运行结果:

使用 list::unique() 原序列:1 1 2 1 1 3,只消除相邻的重复,输出:1 2 1 3。

全局去重,先sort(), 再unique(),排序后变成 1 1 2 3,去重后输出:1 2 3;

使用 list::merge() 合并两个有序链表,1 3 5 + 2 4 6 -> 1 2 3 4 5 6 。

text 复制代码
原始 {1,1,2,1,1,3}
l.unique();            →  1 2 1 3
l.sort();l.unique()    →  1 2 3
l.sort(greater<int>()) →  3 2 1

l.reverse()        → 1 2 3
std::reverse(...)  → 3 2 1

merge之后
first           →   1 2 3 4 5 6
second.empty()  →   1(true)

6.5 splice 转移节点:单个节点,或是一整段

写法 转移内容
a.splice(pos,b) b 的全部元素,b 变空
a.splice(pos,b,it) b 中 it 指向的一个元素
a.splice(pos,b,first,last) b 的 [first,last) 范围

它们都插到 pos 前,不复制元素,也不要求序列是否有序。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <list>

void print(const std::list<int>& l)
{
    for (int x : l) std::cout << x << ' ';
    std::cout << std::endl;
}
int main()
{
    //初始化两个有序链表
    std::list<int> first{1, 2, 3};
    std::list<int> second{10, 20};
    //取second开头元素迭代器与其地址
    auto moved = second.begin();
    int* address = &*moved;
    
    auto pos = first.begin();
    ++pos;
    //将second合并到first的pos前
    first.splice(pos, second);
    print(first);
    //输出观察此时second是否为空以及迭代器变化
    std::cout << second.empty() << ' ' << (&*moved == address) << std::endl;
    
    //对自身进行合并(元素位置调换)
    auto it = std::find(first.begin(), first.end(), 3);
    if (it != first.end()) 
    {
        first.splice(first.begin(), first, it);
    }
    print(first);
    // moved 现已属于 first,应该用 first 的范围边界操作它。
    auto next = first.erase(moved);
    print(first);
}

运行示例:

  1. first.splice(pos,second)
    把second全部节点转移 到first的 pos 迭代器前面;转移完成后second变成空。

节点不拷贝,只是改指针 ;原来second上的迭代器moved依旧有效 ,只是现在归属first容器。

  1. splice 允许源容器与目标容器是同一个 list ,直接把本 list 内部某个节点剪切,挪到别的位置,不需要拷贝,效率极高。
  2. 虽然moved原本来自 second,但节点已经转移到 first,因此可以传给first.erase(moved);

七、list 的相关功能成本


list 的成本有许多和 vector 不同的地方,实际还需要根据具体场景选择使用。

工作 list 的成本与条件
已知有效位置,插入 / 删除一个元素 常数复杂度,不搬移其余元素;仍有构造析构和分配释放的实际成本
查找某值,再删除它 查找 O(N),删除本身 O(1)
访问第 k 个元素 逐步遍历,不能随机访问
单节点 splice / 整表 splice 常数复杂度
跨容器区间 splice 对转移数量线性;同容器区间转移可为常数复杂度
  • 与 vector 的有价值区别集中在这里:后者连续存储、支持随机访问,物理空间连续,高速缓存利用率高;
  • list 提供稳定节点位置和节点转移,按需申请和释放空间,但每个节点额外保存链接,也常需要独立分配。

std::list 标准库参考

相关推荐
東隅已逝,桑榆非晚1 小时前
数据结构队列
数据结构·笔记·学习
老王爱玩车1 小时前
字符串和字符串函数
c语言·开发语言·数据结构·学习
welfare9622 小时前
新号别搞:vector
c++
H.莓飛2 小时前
【数据结构】二叉树
linux·数据结构·算法
welfare9622 小时前
新号别搞:string
c++
xh didida2 小时前
Linux -- 进程概念
linux·服务器·开发语言·c++
常州晟凯电子科技2 小时前
sigmaStar SSC8836Q/SSC8826Q开发笔记之环境搭建和SDK编译
笔记·嵌入式硬件·物联网·视频编解码
凉茶钱2 小时前
【C++】string 常用函数详解
开发语言·c++·string
梦帮科技2 小时前
【3.0修订版】创世与节点工程:genesis.json 与 node.toml 解剖
数据结构·算法·json·去中心化·区块链·智能合约·信任链