C++ std::list 深度解析:迭代器、splice 核心接口与排序效率全解

目录

一丶list是什么

[二、list 的迭代器](#二、list 的迭代器)

[2.1 迭代器类别](#2.1 迭代器类别)

[三、简述 emplace_back 接口](#三、简述 emplace_back 接口)

[四、list 特色接口使用细节](#四、list 特色接口使用细节)

[4.1 简单接口概述](#4.1 简单接口概述)

[4.2 细讲 splice 接口](#4.2 细讲 splice 接口)

[五、list 的排序效率问题](#五、list 的排序效率问题)


一丶list是什么

list 是定义于<list>头文件下的 STL 序列式容器,以类模板的形式实现。从底层结构来看,list 的本质是带头结点的双向循环链表

list支持在常数时间O(1) 内完成任意位置的插入与删除操作 ,执行效率极高。但受链表内存空间不连续的底层结构限制,list原生不支持operator[]随机访问。若手动封装实现operator[]接口,其时间复杂度会从O(1)退化为O(N),因为必须遍历链表节点才能定位到目标元素。

二、list 的迭代器

2.1 迭代器类别

STL 迭代器分为五大类别,不同类别支持的运算操作不同,list迭代器的核心特性可通过下表清晰区分:

迭代器类别 ++ -- + -
Input(输入迭代器)
Output(输出迭代器)
Forward(单向迭代器)
Bidirectional(双向迭代器)
Random Access(随机访问迭代器)

list底层为双向链表,其迭代器属于Bidirectional(双向迭代器),仅支持前后单向遍历(++/--),不支持随机访问运算(+/-)。

迭代器分为多种类别,上文已完整列举。vector的迭代器属于随机访问迭代器 ,支持 ++、--、+、- 等多种随机访问操作。与vector、string 不同,list底层内存空间不连续 ,其迭代器并非简单的原生指针封装。因此,作为双向链表实现的list,迭代器为双向迭代器,仅支持 ++ 和 -- 遍历,不支持 +、- 这类随机访问运算。

因为list用的是双向迭代器,不支持随机跳转,所以它没法像vector那样,直接把迭代器挪到中间去插入数据。

cpp 复制代码
auto it = lt.begin();
int k = 3;
while (k--)
    ++it;
lt.insert(it, 30);

迭代器的类型,其实会直接限制容器能调用哪些接口。我们拿两个典型例子来说。首先是标准库的 sort 排序算法,它底层实现需要频繁用operator[]这类随机访问操作,所以只支持随机访问迭代器。

我们看看sort的底层代码:如你所见,使用了 - 操作符,自然不支持。

第二个例子就是reverse逆序算法 。它的底层实现需要用到**--**操作,所以只要求迭代器能够双向移动,也就只需要支持双向迭代器就够了。

这里还要分清直接支持和间接支持的区别:

  • 直接支持:reverse本身就是直接面向双向迭代器设计的;
  • 间接支持:随机访问迭代器本身也完全支持 -- 操作,所以reverse也同样可以兼容随机迭代器。

一个迭代器能不能被某个接口正常使用,说到底,还是看接口底层到底需要哪些迭代器操作。

其实我们可以把它们理解成一层包含的关系:双向迭代器本身就是一种特殊的单向迭代器,而随机访问迭代器,又是一种特殊的双向迭代器、单向迭代器

这和面向对象里的继承逻辑很像,子类本身就是一种特殊的父类。虽然标准库没有用类继承明确定义迭代器类型,但一个迭代器完全可以传递给要求它上层任意 "父类" 迭代器的接口使用。

三、简述 emplace_back 接口

emplace_back是C++11推出的新接口,我们这里先简单提一下:它的用法和 push_back 完全一样,日常使用中绝大多数场景下效率也基本一致。接下来我们重点说说两者的核心区别:

cpp 复制代码
list<A> lt;
A aa1(1, 1);
lt.push_back(aa1);
lt.push_back(A(2,2));
lt.push_back(3, 3);
 
lt.emplace_back(aa1);
lt.emplace_back(A(2,2));
lt.emplace_back(3, 3);

这里假设A是一个提前定义好、包含两个成员变量的类。向容器中插入元素时,push_back有两种常用写法:传入已定义的有名对象,或是传入临时匿名对象 。而emplace_back还多出第三种用法:直接传入参数,在容器原地构造对象

emplace_back在前两种插入方式下,效率和push_back基本持平;但在第三种方式中,它会比push_back更高效,而push_back本身并不支持这种用法。原因很简单:emplace_back(3, 3)是直接在容器空间内构造出A对象,而push_back的两种写法都需要先构造对象,再通过拷贝构造(或移动)将其放入容器,多了一步额外开销。

四、list 特色接口使用细节

4.1 简单接口概述
  1. swap:为规避std::swap拷贝数据带来的低效问题,list重载了 swap接口,底层仅交换两个链表的头指针,无任何数据拷贝,效率极高;
  2. find:list 未实现专属的查找成员函数,查找元素需调用标准库通用算法std::find;
  3. merge:用于合并两个有序链表,底层采用归并排序的双指针遍历逻辑,依次选取较小元素插入目标链表,合并完成后,被合并的源链表会变为空链表;
  4. unique:链表去重接口,必须要求链表有序,因底层仅会判断相邻节点是否重复;
  5. splice:名义上是拼接接口,本质为节点的剪切转移,是list的特色核心接口,我们会展开详细讲解;
  6. reverse:用于对链表进行原地翻转操作;
  7. remove:根据元素的具体值,直接删除容器中匹配的节点;
  8. remove_if:根据自定义的条件规则,删除满足条件的节点。

代码示例

cpp 复制代码
#include <iostream>
#include <list>
#include <algorithm> // 包含 std::find
using namespace std;

template <class T>
void print_list(const list<T>& lt, const string& name) {
    cout << name << ": ";
    for (const auto& x : lt) cout << x << " ";
    cout << endl;
}

int main() {
    cout << "swap" << endl;
    list<int> lt1 = {1, 2, 3};
    list<int> lt2 = {10, 20, 30};
    print_list(lt1, "交换前 lt1");
    print_list(lt2, "交换前 lt2");
    
    lt1.swap(lt2); // 仅交换头指针,无数据拷贝
    print_list(lt1, "交换后 lt1");
    print_list(lt2, "交换后 lt2");
    cout << endl;

    cout << "std::find" << endl;
    list<int> lt_find = {10, 20, 30, 40};
    auto it = find(lt_find.begin(), lt_find.end(), 30);
    if (it != lt_find.end()) {
        cout << "找到元素: " << *it << endl;
    } else {
        cout << "未找到元素" << endl;
    }
    cout << endl;

    cout << "merge" << endl;
    list<int> lt_m1 = {1, 3, 5}; // 必须有序
    list<int> lt_m2 = {2, 4, 6}; // 必须有序
    print_list(lt_m1, "合并前 lt_m1");
    print_list(lt_m2, "合并前 lt_m2");
    
    lt_m1.merge(lt_m2); // 归并合并,lt_m2 变为空
    print_list(lt_m1, "合并后 lt_m1");
    print_list(lt_m2, "合并后 lt_m2");
    cout << endl;

    cout << "unique" << endl;
    list<int> lt_u = {1, 1, 2, 2, 2, 3, 3}; // 必须有序
    print_list(lt_u, "去重前 lt_u");
    
    lt_u.unique(); // 仅去除相邻重复元素
    print_list(lt_u, "去重后 lt_u");
    cout << endl;

    cout << "splice" << endl;
    list<int> lt_s1 = {1, 2, 3};
    list<int> lt_s2 = {10, 20, 30};
    print_list(lt_s1, "拼接前 lt_s1");
    print_list(lt_s2, "拼接前 lt_s2");

    //拼接整个 lt_s2 到 lt_s1 末尾
    lt_s1.splice(lt_s1.end(), lt_s2);
    print_list(lt_s1, "拼接整个 lt_s2 后 lt_s1");
    print_list(lt_s2, "拼接整个 lt_s2 后 lt_s2 (已空)");

    //重新创建 lt_s2,拼接单个节点
    lt_s2 = {100, 200};
    auto it_s = lt_s2.begin();
    lt_s1.splice(lt_s1.begin(), lt_s2, it_s);
    print_list(lt_s1, "拼接单个节点后 lt_s1");
    print_list(lt_s2, "拼接单个节点后 lt_s2");
    cout << endl;

    cout << "reverse" << endl;
    list<int> lt_r = {1, 2, 3, 4, 5};
    print_list(lt_r, "翻转前 lt_r");
    
    lt_r.reverse(); // 原地翻转
    print_list(lt_r, "翻转后 lt_r");
    cout << endl;


    cout << "remove" << endl;
    list<int> lt_rm = {1, 2, 3, 2, 4, 2};
    print_list(lt_rm, "按值删除前 lt_rm");
    
    lt_rm.remove(2); // 删除所有值为 2 的节点
    print_list(lt_rm, "按值删除后 lt_rm");
    cout << endl;

    cout << "remove_if" << endl;
    list<int> lt_rmif = {1, 2, 3, 4, 5, 6};
    print_list(lt_rmif, "条件删除前 lt_rmif");
    
    // 删除所有大于 3 的元素
    lt_rmif.remove_if([](int val) { return val > 3; });
    print_list(lt_rmif, "条件删除后 lt_rmif");

    return 0;
}
4.2 细讲 splice 接口

splice是list独有的核心特色接口,它的本质是节点的剪切与转移 ,全程不发生任何数据拷贝、构造和析构 ,仅通过修改链表指针完成操作,时间复杂度为 O (1),效率极高。该接口主要支持两种使用方式:

  1. 跨链表操作:将一个list中的节点,剪切到另一个list的指定位置前方;
  2. 自身内部操作:将当前list自身的节点,剪切到自身的指定位置前方。
cpp 复制代码
int main ()
{
  std::list<int> mylist1, mylist2;
  std::list<int>::iterator it;
 
  // set some initial values:
  for (int i=1; i<=4; ++i)
     mylist1.push_back(i);      // mylist1: 1 2 3 4
 
  for (int i=1; i<=3; ++i)
     mylist2.push_back(i*10);   // mylist2: 10 20 30
 
  it = mylist1.begin();
  ++it;                         // points to 2
 
  mylist1.splice (it, mylist2); // mylist1: 1 10 20 30 2 3 4
                                // mylist2 (empty)
                                // "it" still points to 2 (the 5th element

五、list 的排序效率问题

测试list的排序效率时,必须在Release模式下进行测试 ,千万不要用Debug版本。因为Debug模式会附带大量调试信息、关闭编译器优化,会严重干扰测试结果,无法体现容器真实的运行效率。

在Release版本下,两者的效率表现会截然不同!即便使用相同的排序算法,vector的排序速度也会远超list。

核心原因就是缓存利用率

  • list 的内存空间不连续,CPU缓存命中率极低,每次只能读取一个节点到缓存中,后续访问需要反复重新加载,效率大打折扣;
  • 而vector内存连续,缓存可以一次性加载大量数据,充分利用高速缓存,性能优势非常明显。

最优的解决办法也很简单:先把list的数据拷贝到vector中完成排序,排序结束后再拷贝回 list。听起来是两次数据拷贝会有开销,但实际测试中,整体效率反而能直接提升两倍以上。

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
#include <ctime>
using namespace std;

const int DATA_SIZE = 1800000;

int main()
{
    srand((unsigned int)time(NULL));
    vector<int> origin_data;

    origin_data.reserve(DATA_SIZE);

    for (int i = 0; i < DATA_SIZE; ++i)
    {
        origin_data.push_back(rand());
    }


    // vector排序测试
    vector<int> v(origin_data.begin(), origin_data.end());
    clock_t start = clock();
    sort(v.begin(), v.end());
    clock_t end = clock();
    int vec_time = (int)((double)(end - start) / CLOCKS_PER_SEC * 1000);
    cout << "vector sort:" << vec_time << " debug" << endl;
    // Release模式 cout << "vector sort:" << vec_time << " release" << endl;

    // list排序测试
    list<int> lt(origin_data.begin(), origin_data.end());
    start = clock();
    lt.sort();
    end = clock();
    int list_time = (int)((double)(end - start) / CLOCKS_PER_SEC * 1000);
    cout << "list sort:" << list_time << endl;

    list<int> lt_opt(origin_data.begin(), origin_data.end());
    start = clock();
    vector<int> tmp(lt_opt.begin(), lt_opt.end());
    sort(tmp.begin(), tmp.end());
    lt_opt.assign(tmp.begin(), tmp.end());
    end = clock();
    int opt_time = (int)((double)(end - start) / CLOCKS_PER_SEC * 1000);
    cout << "list copy vector sort copy list sort:" << opt_time << endl;

    list<int> lt2(origin_data.begin(), origin_data.end());
    start = clock();
    lt2.sort();
    end = clock();
    int list_time2 = (int)((double)(end - start) / CLOCKS_PER_SEC * 1000);
    cout << "list sort:" << list_time2 << endl;

    return 0;
}
相关推荐
艾莉丝努力练剑2 小时前
【Linux网络】计算机网络入门:从背景到协议,理解网络通信基础
linux·运维·服务器·c++·学习·计算机网络
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(十):线程安全和重入、死锁相关话题
java·linux·运维·服务器·c++·学习·安全
没有天赋那就反复2 小时前
C++里面引用参数和实参的区别
开发语言·c++·算法
MinterFusion2 小时前
如何使用Qt5在窗口中显示矩形(v0.1.3)(下)
开发语言·qt·编程·明德融创·窗口中绘制矩形
ximu_polaris2 小时前
设计模式(C++)-创造型模式-建造者模式
c++·设计模式·建造者模式
Mr.E52 小时前
odoo18 关闭搜索框点击自动弹出下拉框
开发语言·前端·javascript·odoo·owl·odoo18
xyq20242 小时前
MongoDB GridFS
开发语言
TIEM_692 小时前
C++string接口(下)|修改器、字符串操作、成员常量、非成员函数重载
开发语言·c++
AbandonForce2 小时前
C++ 多态(多态定义 多态应用 多态底层||final override关键字||抽象类)
java·开发语言·c++