C++的序列式容器(二)list

std::list 是 C++ 标准库中的双向链表容器,提供了快速的插入和删除操作。与 vector 不同,list 是链式存储结构,因此它不支持随机访问。

1. 概述

std::list 是一个双向链表容器,位于 <list> 头文件中。链表是一种线性表数据结构,其中每个节点都包含一个数据元素和指向前后节点的指针。它适合在任意位置进行频繁插入和删除操作,但不适合随机访问。list 提供了指针稳定性,即插入或删除元素不会影响其他元素的地址,因此适合需要频繁插入和删除操作的场景。

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

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};
    for (int val : myList) {
        std::cout << val << " ";    // 输出 1 2 3 4 5
    }
    return 0;
}

2. 节点

list 中,每个元素都被存储在一个节点中,节点通常包含以下部分:

  • 数据域:存储实际的数据值。
  • 前向指针(prev):指向前一个节点。
  • 后向指针(next):指向后一个节点。

这种链式结构允许 list 快速插入和删除元素(时间复杂度为 ),因为只需要调整相邻节点的指针即可,不需要像 vector 那样进行数据的移动。

3. 迭代器

list 支持双向迭代器,可以通过 begin()end() 方法获取头部和尾部迭代器。此外,list 还支持 rbegin()rend() 迭代器进行反向遍历。

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

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    // 正向迭代
    for (auto it = myList.begin(); it != myList.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 反向迭代
    for (auto it = myList.rbegin(); it != myList.rend(); ++it) {
        std::cout << *it << " ";
    }

    return 0;
}

运行结果

XML 复制代码
1 2 3 4 5 
5 4 3 2 1

vector 不同,list 不支持随机访问,list 只支持双向迭代器 ,而不支持随机访问迭代器 。每次访问某个特定位置的元素都需要一个完整的遍历过程,因此不能使用 [] 运算符或 at() 访问指定位置的元素,必须通过迭代器进行遍历。

示例对比

以下示例展示了 vector 支持下标操作,而 list 不支持:

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

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::list<int> lst = {1, 2, 3, 4, 5};

    // 直接使用下标访问 vector 中的元素
    std::cout << "Element at index 2 in vector: " << vec[2] << std::endl;

    // 使用迭代器访问 list 中的第三个元素
    auto it = lst.begin();
    std::advance(it, 2); // 移动迭代器到第三个位置
    std::cout << "Element at index 2 in list: " << *it << std::endl;

    return 0;
}

运行输出:

XML 复制代码
Element at index 2 in vector: 3
Element at index 2 in list: 3

因为 list 是链表结构而不是连续内存,因此它无法提供高效的随机访问;只能通过迭代器进行线性遍历,时间复杂度为

需要注意的是,list有一个重要性质 :插入操作(insert)和接合操作(splice)都不会造成原有的list迭代器失效。这在vector中是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效。甚至list的元素删除操作(erase),也只有"指向被删除元素"的那个迭代器失效,其他迭代器不受任何影响。

下面是一个示例代码,通过删除 std::list 中的某个元素,展示删除操作只使指向被删除元素的迭代器失效,而其他迭代器保持有效:

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

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    // 获取指向第三个元素的迭代器
    auto it1 = myList.begin();
    std::advance(it1, 2); // 指向元素3

    // 获取指向第四个元素的迭代器
    auto it2 = it1;
    ++it2; // 指向元素4

    // 删除第三个元素(值为3)
    myList.erase(it1);

    // 尝试访问其他迭代器(it2)
    std::cout << "Element after deletion (it2): " << *it2 << std::endl;

    // 遍历并输出剩余元素,验证列表完整性
    std::cout << "Remaining elements: ";
    for (const auto& elem : myList) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}
解释
  1. 创建迭代器it1 指向元素 3it2 指向元素 4
  2. 删除操作 :调用 erase(it1) 删除元素 3,使 it1 失效。
  3. 验证未失效迭代器it2 仍然指向元素 4,未受到影响。
  4. 遍历并输出结果:剩余的元素顺序和内容保持正确,验证了删除操作只影响指向被删元素的迭代器,不会影响其他迭代器的有效性。

运行结果

XML 复制代码
Element after deletion (it2): 4
Remaining elements: 1 2 4 5

4. 数据结构

std::list 的底层数据结构是一个(环状)双向链表。链表的特点是每个节点存储一个数据值和两个指针(prevnext),分别指向前一个和后一个节点。双向链表适合频繁的插入和删除操作,尤其是在中间位置插入或删除元素时,可以避免大量数据移动。

然而,链表占用的内存比数组多,因为每个节点都要额外存储两个指针。同时,由于链表分散存储,不具备局部性,不适合频繁的随机访问。

5. 构造与内存管理

list 提供了多种构造方法和内存管理操作:

  • 默认构造std::list<int> myList;
  • 指定大小构造std::list<int> myList(10); // 创建10个元素,初始值为0
  • 指定大小和初始值std::list<int> myList(10, 5); // 创建10个元素,初始值为5
  • 拷贝构造std::list<int> myList2(myList);
  • 移动构造:高效转移资源,避免拷贝

在内存管理方面,list 没有 vectorcapacityreserve 方法,因为链表的存储不需要预留空间。但 list 提供了 clear() 方法用于清空所有元素,size() 方法获取元素数量。

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

int main() {
    std::list<int> myList(5, 2); // 大小为5,初始值为2

    std::cout << "Size: " << myList.size() << std::endl;

    myList.clear();
    std::cout << "Size after clear: " << myList.size() << std::endl;

    return 0;
}

运行结果

XML 复制代码
Size: 5
Size after clear: 0

6. 元素操作方法

list 提供了一些操作方法,允许在列表任意位置插入和删除元素。

  • 添加

    • push_front(val):在头部插入元素
    • push_back(val):在尾部插入元素
    • emplace_front(args...)emplace_back(args...):原地构造新元素,避免拷贝
    • insert(pos, val):在指定迭代器位置插入元素
    • emplace(pos, args...):在指定位置原地构造元素
    • splice:将一个 list 的元素移动到另一个 list

splice 用于将一个 list 的一部分或全部转移到另一个 list 中。该操作不会产生拷贝,只是修改指针,因此效率非常高。

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

int main() {
    std::list<int> list1 = {1, 2, 3};
    std::list<int> list2 = {4, 5, 6};

    // 将 list2 的所有元素移动到 list1 的末尾
    list1.splice(list1.end(), list2);

    std::cout << "After splice, list1: ";
    for (const auto& elem : list1) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    std::cout << "After splice, list2 (should be empty): ";
    for (const auto& elem : list2) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

运行结果

XML 复制代码
After splice, list1: 1 2 3 4 5 6
After splice, list2 (should be empty):

注意: splice 之后,list2 中的元素被转移到 list1list2 变为空。

  • merge:合并两个排序的 list

merge 用于将两个已经排序的 list 合并成一个排序的 list。它要求两个列表都已经排好序。

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

int main() {
    std::list<int> list1 = {1, 3, 5};
    std::list<int> list2 = {2, 4, 6};

    // 合并 list2 到 list1,并保持排序
    list1.merge(list2);

    std::cout << "After merge, list1: ";
    for (const auto& elem : list1) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    std::cout << "After merge, list2 (should be empty): ";
    for (const auto& elem : list2) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

运行结果

XML 复制代码
After merge, list1: 1 2 3 4 5 6
After merge, list2 (should be empty):

注意:merge 会将 list2 合并到 list1,并保持排序。合并后 list2 变为空。

  • 删除

    • pop_front()pop_back():删除头部或尾部元素
    • erase(pos):删除指定位置的元素
    • clear():清空所有元素
    • remove(val):删除所有等于 val 的元素
    • unique:移除连续重复的元素。

unique 用于删除 list 中连续的重复元素。如果列表中的元素是 [1, 2, 2, 3, 3, 3, 4],调用 unique 后会变成 [1, 2, 3, 4]

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

int main() {
    std::list<int> myList = {1, 2, 2, 3, 3, 3, 4};

    // 移除连续重复的元素
    myList.unique();

    std::cout << "After unique: ";
    for (const auto& elem : myList) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

# 输出:After unique: 1 2 3 4
  • 访问

    • front()back():访问首尾元素
  • 顺序操作

    • reverse():反转 list 中的元素顺序
    • sort():对 list 进行排序

sort 用于对 list 中的元素进行排序。list 不支持随机访问,因此不能使用 std::sort,必须使用 list 自带的 sort 方法。

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

int main() {
    std::list<int> myList = {4, 2, 5, 1, 3};

    // 排序元素
    myList.sort();

    std::cout << "After sort: ";
    for (const auto& elem : myList) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

运行结果:

XML 复制代码
After sort: 1 2 3 4 5

可以使用自定义比较函数对 list 进行降序排序或其他排序方式。

示例:使用 Lambda 表达式进行降序排序

C++11 引入的 Lambda 表达式可以让我们更简洁地定义比较函数,适合进行一次性排序操作。

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

int main() {
    std::list<int> myList = {4, 2, 5, 1, 3};

    // 使用 Lambda 表达式进行降序排序
    myList.sort([](int a, int b) { return a > b; });

    std::cout << "List after sorting in descending order using Lambda: ";
    for (const auto& elem : myList) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

运行结果

XML 复制代码
List after sorting in descending order using Lambda: 5 4 3 2 1

总结

std::list 是一种双向链表容器,适合频繁插入和删除操作。但list 是链式存储结构,因此它不支持随机访问

相关推荐
依旧阳光的老码农3 分钟前
标准C++ 字符串
开发语言·c++
白-胖-子20 分钟前
【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-成绩排序
c++·算法·蓝桥杯·真题·蓝桥等考
极地星光20 分钟前
JSON-RPC-CXX深度解析:C++中的远程调用利器
c++·rpc·json
用屁屁笑32 分钟前
Java:JVM
java·开发语言·jvm
大叔是90后大叔34 分钟前
vue3中查找字典列表中某个元素的值对应的列表索引值
开发语言·前端·javascript
山登绝顶我为峰 3(^v^)31 小时前
刷算法题(C++)
开发语言·c++·算法
No0d1es1 小时前
2024年9月青少年软件编程(C语言/C++)等级考试试卷(九级)
c语言·数据结构·c++·算法·青少年编程·电子学会
bingw01141 小时前
华为机试HJ42 学英语
数据结构·算法·华为
小小靖大王1 小时前
js 基础语法 书写方式
开发语言·前端·javascript
NikitaC1 小时前
ldconfig 和 LD_LIBRARY_PATH 区别
linux·c++