C++中的list(1)

C++中的list(1)

同理,本期只讲使用,不深究。

先整体的介绍一遍list

你可以把 std::list 想象成一个 双向链表 。想象一下手拉手(或者更确切地说,是手拉手 并且 背靠背)站成一排的人:

复制代码
[人A] <--> [人B] <--> [人C] <--> ... <--> [人Z]
  • 每个人(节点): 代表链表中的一个元素。它包含三部分:
    • 实际的数据: 你想存储的值(比如整数、字符串、对象等)。
    • 指向下一个人的指针(Next): 告诉我下一个元素是谁。
    • 指向上一个人的指针(Prev): 告诉我上一个元素是谁。(这就是"双向"的关键!)
  • 手拉手(指针): 这些指针就像他们互相拉着的手,把所有人连接起来,形成一个序列。
  • 队伍的头尾: 队伍有明确的第一个(front())和最后一个(back())人。

核心特性(为什么选它?)

  1. 插入/删除快到飞起(O(1)时间复杂度 - 常数时间):

    • 场景: 你需要在队伍中间(比如人B和人C之间)插入一个新成员(人X),或者把中间的某个人(比如人M)踢出去。
    • 传统数组/vector的痛: 如果用数组或std::vector,插入/删除中间元素,需要把后面所有人依次往后挪或往前挪,人越多,挪动越耗时(O(n)时间复杂度 - 线性时间)。想象搬砖,队伍越长越累。
    • list的绝活: list只需要修改指针!找到人B和人C(或者找到人M),让B松开C的手,拉上X的手,同时让X拉上C的手(插入)。或者让M前面的人松开M的手,直接拉住M后面人的手,再把M请走(删除)。无论队伍多长(100人还是100万人),这个"改指针"的操作代价几乎不变! 这是list最大的优势。
  2. 不连续的内存(非连续存储):

    • 场景: 队伍里的人不需要紧挨着站在一块连续的空地上。他们可以分散在操场各处,只要互相记得谁在前后就行。
    • 好处: 插入删除时不需要大块连续内存移动,效率高(如上所述)。
    • 代价:
      • 不能随机访问(Random Access): 你不能直接说"我要第5个人!"。队伍里的人只知道邻居是谁,不知道自己是第几个。想找第5个人?你得从第1个开始,一个接一个地数(顺着指针找),直到第5个(O(n)时间)。vector像一排编号的储物柜,可以直接走到第5号(O(1)时间)。
      • 内存开销稍大: 每个人除了要带自己的行李(数据),还要带两张写着邻居地址的小纸条(前后指针),所以占用的总内存通常比vector只带行李要大一点。
      • 可能的内存碎片: 人分散站着,时间长了操场(内存)上可能留下很多小块的空隙,虽然现代内存管理很聪明,但这在极端情况下可能是个小问题。
  3. 迭代器稳定性(超级重要!):

    • 场景: 你正在挨个检查队伍里的人(使用迭代器遍历)。这时候,有人插入或删除了,只要不是删除你当前正在看的这个人,指向其他人的迭代器仍然有效!
    • 对比vector的痛:vector中间插入或删除元素,可能导致后面所有元素内存地址改变(挪位置了),之前获取的所有迭代器、指针、引用都可能瞬间失效! 继续用它们会引发崩溃或错误(悬垂指针/迭代器)。
    • list的妙处: 因为list只改指针,元素本身待在原地不动(内存地址不变),所以只要你不删除当前元素,指向其他元素的迭代器、指针、引用稳如泰山。这在需要长时间持有迭代器或在遍历中修改结构(除了当前元素)的场景下非常关键。

基本操作(怎么用?)

cpp 复制代码
#include 

int main() {
    // 1. 创建一个空队伍
    std::list myList;

    // 2. 在队伍末尾加人 (入队尾)
    myList.push_back(10);
    myList.push_back(20); // 队伍: 10 <--> 20

    // 3. 在队伍开头加人 (插队头)
    myList.push_front(5); // 队伍: 5 <--> 10 <--> 20

    // 4. 在中间插人 (需要先找到位置 - 用迭代器)
    // 假设我们想在10后面插入15
    auto it = std::find(myList.begin(), myList.end(), 10); // 找到值为10的"人"的位置
    if (it != myList.end()) {
        myList.insert(++it, 15); // 在10后面(也就是++it指向的位置)插入15
        // 现在队伍: 5 <--> 10 <--> 15 <--> 20
    }

    // 5. 删除队伍开头的人 (出队头)
    myList.pop_front(); // 删除5, 队伍: 10 <--> 15 <--> 20

    // 6. 删除队伍末尾的人 (出队尾)
    myList.pop_back(); // 删除20, 队伍: 10 <--> 15

    // 7. 删除中间特定的人 (用迭代器指定位置)
    it = std::find(myList.begin(), myList.end(), 15);
    if (it != myList.end()) {
        myList.erase(it); // 删除15, 队伍只剩: 10
    }

    // 8. 获取队伍头和尾的人 (查看但不删除)
    int first = myList.front(); // first = 10
    int last = myList.back();   // last = 10 (因为只剩一个)

    // 9. 遍历队伍 (必须用迭代器,不能直接用下标[])
    std::cout << "队伍成员: ";
    for (auto iter = myList.begin(); iter != myList.end(); ++iter) {
        std::cout << *iter << " "; // 解引用迭代器获取数据
    }
    std::cout << std::endl; // 输出: 队伍成员: 10

    // 10. 检查队伍是否为空
    if (!myList.empty()) {
        std::cout << "队伍不是空的,有 " << myList.size() << " 个人。" << std::endl;
    }

    // 11. 清空队伍
    myList.clear(); // 队伍解散了

    return 0;
}

关键成员函数(工具包)

  • 构造/赋值:
    • list(): 创建空链表
    • list(n, value): 创建有 n 个元素,每个都是 value 的链表
    • list(begin_iter, end_iter): 用另一个容器的迭代器范围拷贝构造
    • list(other_list): 拷贝构造函数
    • operator=: 赋值
    • assign(): 替换内容
  • 元素访问:
    • front(): 返回第一个元素的引用 (不能用于空链表)
    • back(): 返回最后一个元素的引用 (不能用于空链表)
    • 注意: 没有 operator[]at()!因为不能随机访问。
  • 迭代器:
    • begin() / cbegin(): 指向第一个元素的迭代器 (const)
    • end() / cend(): 指向最后一个元素下一个位置的迭代器 (const)
    • rbegin() / crbegin(): 指向最后一个元素的反向迭代器 (const)
    • rend() / crend(): 指向第一个元素前一个位置反向迭代器 (const)
  • 容量:
    • empty(): 检查是否为空
    • size(): 返回元素个数 (注意: 某些实现中 size() 可能是 O(n) 操作!C++11 标准要求是 O(1),主流现代编译器都遵守)
  • 修改器 (核心优势区):
    • push_front(value): 在头部插入元素
    • pop_front(): 删除头部元素 (不能用于空链表)
    • push_back(value): 在尾部插入元素
    • pop_back(): 删除尾部元素 (不能用于空链表)
    • insert(iterator pos, value): 在迭代器 pos 指向的位置之前 插入 value,返回指向新元素的迭代器 (O(1))
    • insert(iterator pos, n, value): 插入 nvalue
    • insert(iterator pos, begin_iter, end_iter): 插入另一个容器的迭代器范围
    • erase(iterator pos): 删除迭代器 pos 指向的元素,返回指向下一个元素的迭代器 (O(1))
    • erase(iterator first, iterator last): 删除 [first, last) 范围的元素
    • clear(): 清空链表
    • swap(other_list): 交换两个链表的内容 (非常快,只交换内部指针)
  • 操作 (链表特有):
    • splice(iterator pos, other_list): 把 other_list所有元素 移动到 pos 指向的位置之前,other_list 变空。(O(1) 或 O(n),取决于是否同一个链表)
    • splice(iterator pos, other_list, iterator it): 把 other_listit 指向的单个元素 移动到 pos 指向的位置之前。(O(1))
    • splice(iterator pos, other_list, iterator first, iterator last): 把 other_list[first, last) 范围的元素移动到 pos 指向的位置之前。(O(1))
    • remove(const T& value): 删除所有值等于 value 的元素。(O(n))
    • remove_if(Predicate pred): 删除所有满足谓词 pred 的元素。(O(n))
    • unique(): 删除连续重复的元素(通常需要先 sort)。(O(n))
    • unique(BinaryPredicate pred): 用自定义比较删除连续"重复"元素。
    • merge(other_list): 合并两个已排序 链表。other_list 的内容被合并到 *this 中并变空。合并后 *this 保持有序。(O(n))
    • merge(other_list, Compare comp): 用自定义比较合并。
    • sort(): 对链表进行排序(通常用归并排序)。(O(n log n))
    • sort(Compare comp): 用自定义比较排序。
    • reverse(): 反转链表中元素的顺序。(O(n))

何时使用 std::list?(选型建议 - 后端视角)

  1. 频繁在序列中间进行插入或删除操作: 这是 list 的王牌场景。比如:

    • 实现 LRU (Least Recently Used) 缓存淘汰算法(移动热门项到链表头/删除尾)。
    • 维护一个有序列表(但插入位置不确定),需要不断插入新项到正确位置。
    • 需要高效地拼接(splice)或分割链表。
    • 需要稳定迭代器,在遍历过程中插入/删除其他元素(比如事件处理循环)。
  2. 对迭代器/指针/引用的稳定性要求极高: 当元素插入删除后,你还需要保证之前获取到的(指向未删除元素的)迭代器/指针/引用仍然有效时,list 是首选。

  3. 不需要随机访问(即不需要按索引快速访问): 如果你大部分操作是按顺序访问或者只关心头尾,list 是合适的。

何时避免使用 std::list?(选型建议 - 后端视角)

  1. 需要频繁随机访问元素(按索引访问):vectordequelist 的 O(n) 随机访问太慢了。
  2. 内存非常紧张: list 每个元素都有额外开销(两个指针)。如果存储的是小对象(比如 int, char),这个开销比例会很大。vector 的内存利用率通常更高(连续存储,只有少量额外管理开销)。
  3. 缓存友好性(Cache Locality)至关重要: vector 的元素在内存中连续存放,CPU 缓存预取效率极高,访问速度快。list 的元素分散存放,缓存命中率低,遍历速度可能显著慢于 vector,即使时间复杂度一样是 O(n)。
  4. 元素很小且操作简单: vector 的插入删除成本在数据量不大时可能可以接受,而其随机访问和遍历速度的优势更明显。

list vs vector vs deque 速查表 (后端选型参考)

特性 std::list (双向链表) std::vector (动态数组) std::deque (双端队列)
底层结构 非连续,双向链表 连续存储 分段连续 (多块数组+索引)
随机访问 (O(n)) ❌ 极快 (O(1)) ✅ (O(1)) ✅
头部插入/删除 极快 (O(1)) ✅ (O(n)) ❌ (O(1)) ✅
尾部插入/删除 极快 (O(1)) ✅ (O(1) 摊还) ✅ (O(1)) ✅
中间插入/删除 (O(1) 找到位置后) ✅ (O(n)) ❌ (O(n)) ❌
迭代器失效 高稳定 (只删当前元素失效) 低稳定 (插入删除常失效) 中稳定 (中间插入删除失效)
内存开销 较高 (每个元素+2指针) 较低 (少量额外管理开销) 中 (管理索引开销)
内存连续性 碎片化 高度连续 分段连续
缓存友好性 (遍历慢) 极好 (遍历极快) 较好 (分段连续)
特殊操作 高效 splice, merge, sort - -
典型后端用例 LRU缓存、需稳定迭代器的列表 主容器、需随机访问/遍历的集合 队列(FIFO)、栈(LIFO)

性能建议

  1. 优先用 vector 默认首选。除非有明确证据(性能分析)表明 listdeque 在特定操作上显著更好。
  2. 善用 reserve() (vector/deque): 如果能预估大小,提前预留空间,避免频繁扩容复制。
  3. 理解 list::size() 确认你的编译器/标准库实现它是 O(1) 还是 O(n)(C++11 后都是 O(1),但老代码/特定平台需留意)。如果需要频繁调用且是 O(n),考虑自己维护计数器。
  4. 考虑 deque 做队列/栈: 如果主要操作在头尾,deque 通常比 list 更优(内存更紧凑,缓存更好,同样 O(1) 操作)。

总结

std::list 是一个强大的双向链表 容器。它在序列中间频繁插入和删除操作上效率无敌(O(1)) ,并且提供了卓越的迭代器稳定性 。它还支持高效的链表特有操作如 splice, merge, sort

但是,它不擅长随机访问(O(n))内存开销相对较大 ,并且缓存不友好

作为后端工程师,选择它时要非常明确:你的场景是否极度依赖中间的高效插入删除?是否需要超强的迭代器稳定性?如果答案是肯定的,并且可以接受随机访问慢和内存稍大的代价,那么 list 就是你的神兵利器。否则,vectordeque 通常是更通用、性能更均衡的选择。 理解每种容器的底层实现和性能特征是做出明智选择的关键。

下面我将详细讲解 std::list 中的各个函数,包括使用示例和注意事项。


1. 构造函数

cpp 复制代码
// 默认构造
std::list<int> list1;  // 空list

// 填充构造 (n个相同元素)
std::list<int> list2(5, 100);  // {100, 100, 100, 100, 100}

// 范围构造 (用迭代器范围)
std::vector<int> vec = {1, 2, 3};
std::list<int> list3(vec.begin(), vec.end());  // {1, 2, 3}

// 拷贝构造
std::list<int> list4(list3);  // {1, 2, 3}

注意事项

  • 默认构造创建空链表,内存开销最小
  • 填充构造的 val 参数可省略(使用类型默认值)
  • 范围构造兼容任何迭代器(数组、vector等)

2. 容量相关

(1) empty()
cpp 复制代码
std::list<int> list;
if (list.empty()) {  // 返回 true
    std::cout << "List is empty!\n";
}
(2) size()
cpp 复制代码
std::list<int> list = {1, 2, 3};
std::cout << "Size: " << list.size();  // 输出 3
(3) max_size()
cpp 复制代码
std::list<int> list;
std::cout << "Max possible size: " << list.max_size();
// 输出理论最大容量(通常极大)

注意事项

  • empty()size()==0 更高效
  • size() 在C++11后保证是O(1)时间复杂度
  • max_size() 是理论值,实际受内存限制

3. 元素访问

(1) front()
cpp 复制代码
std::list<std::string> words = {"apple", "banana"};
std::cout << words.front();  // 输出 "apple"
words.front() = "orange";    // 修改第一个元素
(2) back()
cpp 复制代码
std::list<int> nums = {10, 20, 30};
std::cout << nums.back();  // 输出 30
nums.back() += 5;          // 修改为35

注意事项

  • 在空list上调用会导致未定义行为
  • 返回引用,可直接修改元素
  • 比迭代器访问更简洁

4. 修改器

(1) assign() - 重新赋值
cpp 复制代码
std::list<char> letters;
// 方法1:n个相同值
letters.assign(3, 'A');  // {'A','A','A'}

// 方法2:迭代器范围
std::string word = "XYZ";
letters.assign(word.begin(), word.end());  // {'X','Y','Z'}
(2) emplace_front() - 头部构造插入(C++11)
cpp 复制代码
struct Point { int x, y; Point(int a, int b) : x(a), y(b) {} };
std::list<Point> points;
points.emplace_front(1, 2);  // 直接构造,避免拷贝
(3) push_front() - 头部插入
cpp 复制代码
std::list<int> nums;
nums.push_front(3);  // {3}
nums.push_front(1);  // {1, 3}
(4) pop_front() - 头部删除
cpp 复制代码
std::list<int> nums = {1, 2, 3};
nums.pop_front();  // {2, 3}
(5) emplace_back() - 尾部构造插入(C++11)
cpp 复制代码
std::list<std::pair<int, int>> pairs;
pairs.emplace_back(4, 5);  // 直接构造 pair(4,5)
(6) push_back() - 尾部插入
cpp 复制代码
std::list<std::string> names;
names.push_back("Alice");
names.push_back("Bob");  // {"Alice", "Bob"}
(7) pop_back() - 尾部删除
cpp 复制代码
std::list<int> nums = {1, 2, 3};
nums.pop_back();  // {1, 2}
(8) emplace() - 指定位置构造插入(C++11)
cpp 复制代码
std::list<int> nums = {10, 30};
auto it = nums.begin();
++it;  // 指向30
nums.emplace(it, 20);  // 在30前插入20 → {10,20,30}
(9) insert() - 指定位置插入
cpp 复制代码
std::list<int> nums = {1, 4};
// 插入单个元素
auto it = nums.begin();
++it;
it = nums.insert(it, 2);  // {1,2,4},返回指向2的迭代器

// 插入多个相同元素
nums.insert(it, 2, 3);  // {1,3,3,2,4}

// 插入迭代器范围
std::vector<int> vec = {5,6};
nums.insert(nums.end(), vec.begin(), vec.end());  // 尾部追加
(10) erase() - 删除元素
cpp 复制代码
std::list<int> nums = {10, 20, 30, 40};
auto it = nums.begin();
++it;  // 指向20

// 删除单个元素
it = nums.erase(it);  // 删除20,it现在指向30

// 删除范围
auto first = nums.begin();
auto last = nums.end();
--last;
nums.erase(first, last);  // 删除[第一个, 最后一个) → 只剩40
(11) swap() - 交换内容
cpp 复制代码
std::list<int> listA = {1, 2};
std::list<int> listB = {3, 4, 5};
listA.swap(listB); 
// listA = {3,4,5}, listB = {1,2}
(12) resize() - 调整大小
cpp 复制代码
std::list<int> nums = {1, 2, 3};
nums.resize(5);      // {1,2,3,0,0} 默认填充0
nums.resize(2);      // {1,2} 删除多余元素
nums.resize(4, 99);  // {1,2,99,99} 指定填充值
(13) clear() - 清空内容
cpp 复制代码
std::list<int> nums = {1, 2, 3};
nums.clear();  // 变为空list

修改器注意事项

  1. 所有插入/删除操作均保持迭代器有效性(被删除元素除外)

  2. emplace 系列函数效率高于 push(避免拷贝)

  3. erase 返回被删除元素的下一个有效迭代器

  4. resize 缩小尺寸会销毁多余元素

  5. clear 不会释放内存(capacity不变),可用 swap 技巧释放内存:

    cpp 复制代码
    std::list<int>().swap(nums);  // 释放所有内存

5. 特殊操作

(1) splice() - 链表拼接
cpp 复制代码
std::list<int> listA = {1, 2};
std::list<int> listB = {3, 4};

// 1. 移动整个list
listA.splice(listA.end(), listB); 
// listA={1,2,3,4}, listB为空

// 2. 移动单个元素
listB.splice(listB.begin(), listA, listA.begin());
// listB={1}, listA={2,3,4}

// 3. 移动元素范围
auto it = listA.begin();
++it;  // 指向3
listB.splice(listB.end(), listA, it, listA.end());
// listB={1,3,4}, listA={2}
(2) remove() - 按值删除
cpp 复制代码
std::list<int> nums = {1, 2, 3, 2, 4};
nums.remove(2);  // 删除所有2 → {1,3,4}
(3) remove_if() - 条件删除
cpp 复制代码
std::list<int> nums = {1, 2, 3, 4, 5};
nums.remove_if([](int n) { 
    return n % 2 == 0;  // 删除所有偶数
});  // {1,3,5}
(4) unique() - 去重
cpp 复制代码
std::list<int> nums = {1, 2, 2, 3, 3, 3};
nums.unique();  // 删除连续重复 → {1,2,3}

// 自定义去重规则(相邻元素差<=1)
nums = {1, 2, 4, 5, 7};
nums.unique([](int a, int b) {
    return std::abs(a - b) <= 1;
});  // 结果:{1,4,7}(2,5被删除)
(5) merge() - 合并有序列表
cpp 复制代码
std::list<int> listA = {1, 3, 5};
std::list<int> listB = {2, 4, 6};

// 前提:两个list必须已排序!
listA.merge(listB); 
// listA={1,2,3,4,5,6}, listB为空

// 自定义排序规则(降序)
listA = {5, 3, 1};
listB = {6, 4, 2};
listA.merge(listB, std::greater<int>());
// listA={6,5,4,3,2,1}
(6) sort() - 排序
cpp 复制代码
std::list<int> nums = {3, 1, 4, 2};

// 默认升序
nums.sort();  // {1,2,3,4}

// 自定义排序(降序)
nums.sort(std::greater<int>());  // {4,3,2,1}
(7) reverse() - 反转
cpp 复制代码
std::list<int> nums = {1, 2, 3};
nums.reverse();  // {3,2,1}

特殊操作注意事项

  1. splice

    • 最强大的链表操作,时间复杂度O(1)
    • 不复制元素,只修改指针
    • 源链表会被修改
  2. merge

    • 必须保证两个链表已排序
    • 使用与排序时相同的比较规则
    • 合并后源链表被清空
  3. sort

    • 链表专用排序(通常为归并排序)
    • std::sort 更高效(因 std::sort 需要随机访问)
  4. unique

    • 只删除连续重复元素
    • 通常先 sortunique 实现完全去重

关键总结

操作类型 推荐函数 使用场景
头部操作 emplace_front/push_front 实现队列(FIFO)时使用
尾部操作 emplace_back/push_back 实现栈(LIFO)时使用
位置插入 emplace/insert 需要中间插入时使用
高效转移 splice 合并链表无需拷贝
条件删除 remove_if 根据复杂条件删除元素
排序去重 sort + unique 准备数据时使用

最佳实践

  1. 需要频繁中间插入/删除 → 首选 list

  2. 操作元素前检查 empty() 避免未定义行为

  3. 尽量用 emplace 替代 push 提升性能

  4. 合并有序列表时优先用 merge 而非 insert

  5. 清空内存技巧:

    cpp 复制代码
    std::list<int> temp;
    nums.swap(temp);  // 彻底释放内存

通过结合具体场景选择合适的操作,可以充分发挥链表优势,实现高效的数据处理。

相关推荐
GISer_Jing4 小时前
JavaScript 中Object、Array 和 String的常用方法
开发语言·javascript·ecmascript
耳总是一颗苹果6 小时前
C语言---动态内存管理
c语言·开发语言
手眼通天王水水6 小时前
【Linux】3. Shell语言
linux·运维·服务器·开发语言
仟濹6 小时前
【数据结构】「队列」(顺序队列、链式队列、双端队列)
c语言·数据结构·c++
小蜗牛狂飙记6 小时前
在github上传python项目,然后在另外一台电脑下载下来后如何保障成功运行
开发语言·python·github
小苏兮6 小时前
【C语言】字符串与字符函数详解(上)
c语言·开发语言·算法
chilavert3186 小时前
技术演进中的开发沉思-40 MFC系列:多线程协作
c++·windows·mfc
程序员JerrySUN7 小时前
Valgrind Memcheck 全解析教程:6个程序说明基础内存错误
android·java·linux·运维·开发语言·学习
apocelipes7 小时前
使用uint64_t批量比较短字符串
c语言·数据结构·c++·算法·性能优化·golang
菜一头包7 小时前
C++ STL中迭代器学习笔记
c++·笔记·学习