STL精讲:list容器

大家好,这里是彩妙呀~

在 C++ 编程的世界中,容器是强大的工具,它们为我们管理数据提供了便捷的方式。而list作为 C++ 标准模板库(STL)中的一员,具有独特的地位和重要性。

它底层基于带头双向循环链表 实现,这一结构赋予了list许多与众不同的特性。与vector的连续存储结构相比,list在任意位置进行插入和删除操作时,时间复杂度仅为 O (1),这使得它在需要频繁增删元素的场景中表现出色。例如,在实现一个实时任务调度系统时,任务可能随时被添加或取消,list就能高效地处理这些操作。

此外,list支持双向迭代,这意味着我们可以从前往后或从后往前遍历容器中的元素,为数据处理提供了更多的灵活性。

然而,list也并非完美无缺,由于其非连续存储的特性,它不支持随机访问,这使得访问某个特定位置的元素时效率较低,时间复杂度为 O (n)。在实际应用中,了解list的这些特性,能够帮助我们根据具体的需求选择最合适的数据结构,从而提升程序的性能和效率。如果想深入了解list的官方文档,可以参考list - C++ 参考文档,这里面包含了丰富的关于list的信息,从基本概念到详细的接口说明,都能找到答案。

目录

[list 容器的底层本质](#list 容器的底层本质)

[list 底层与成员变量深度剖析](#list 底层与成员变量深度剖析)

[list 类的核心成员变量构成](#list 类的核心成员变量构成)

[list 节点结构解析(模拟实现视角)](#list 节点结构解析(模拟实现视角))

[list 成员函数全分类详解](#list 成员函数全分类详解)

[构造与析构函数:list 对象的创建与销毁](#构造与析构函数:list 对象的创建与销毁)

构造函数参考文档

[默认构造函数(list ())](#默认构造函数(list ()))

[填充构造函数(list (n, val))](#填充构造函数(list (n, val)))

[迭代器区间构造函数(list (first, last))](#迭代器区间构造函数(list (first, last)))

[拷贝构造函数(list (const list& x))](#拷贝构造函数(list (const list& x)))

[析构函数(~list ())](#析构函数(~list ()))

list迭代器

[容量相关函数:判断 list 状态的核心接口](#容量相关函数:判断 list 状态的核心接口)

[判空函数 empty ()](#判空函数 empty ())

[元素个数函数 size ()](#元素个数函数 size ())

[元素访问函数:list 的首尾元素操作](#元素访问函数:list 的首尾元素操作)

[首元素访问 front ()](#首元素访问 front ())

[尾元素访问 back ()](#尾元素访问 back ())

有关list迭代器失效之类的说明与解决

迭代器失效的解决方案

元素的增删查改函数

assign()

[头插/头删 尾插/尾删](#头插/头删 尾插/尾删)

其他函数

[list 特殊操作函数](#list 特殊操作函数)

[list 与 vector 容器对比分析](#list 与 vector 容器对比分析)

list 容器的底层本质

list容器的底层实现是带头双向循环链表,链表中的每个节点包含三个部分:一个用于存储数据的变量,以及分别指向前后两个节点的指针。这种结构使得list在内存中的存储是非连续的 ,每个节点可以在内存的任意位置分配。

list容器的这种带头双向链表结构在插入和删除操作上优势显著,只需调整节点指针即可完成,时间复杂度为O(1),适用于数据频繁变动的场景如联系人管理系统。此外,其双向迭代特性支持灵活的正反向遍历,便于双向处理如文本编辑器的撤销重做功能。

但是,list的迭代器性质导致它不支持随机访问,访问元素需从头或尾遍历,时间复杂度为O(n),因此在需要快速随机访问的场景如游戏开发中,更推荐使用vector等数据结构。

list 底层与成员变量深度剖析

list 类的核心成员变量构成

先给出参考文档的内容:

一眼看确实有点迷茫,下面彩妙会做解释的:

在 C++ 标准库的list类实现中,核心成员变量相对简洁,主要包含一个指向链表头节点的指针,以 GCC 标准库实现为例,list类中可能有如下核心成员变量定义:

cpp 复制代码
template <class T, class Alloc = std::allocator<T>>
class list {
private:
    // 节点类型别名,实际是一个包含数据、前驱和后继指针的结构体
    typedef _List_node<T, Alloc> _Node;
   // 指向头节点的指针,头节点是双向循环链表的关键,不存储有效数据
    _Node* _M_head; 
};

这里的**_M_head** 指针指向的头节点在list 中起着至关重要的作用:头节点是双向循环链表的起始点和结束点,它的存在使得链表在初始化、插入、删除等操作时的逻辑更加统一和简洁。

当链表为空时,头节点的前驱指针和后继指针都指向自身,形成一个空的循环结构;当链表有元素时,头节点的后继指针指向第一个有效数据节点,前驱指针指向最后一个有效数据节点,而最后一个有效数据节点的后继指针又指向头节点,从而构成双向循环链表。

除了必要的头节点指针外,在一些list的实现中,还可能会包含一个记录链表元素个数的成员变量(通常情况下,这里记录成员个数的变量均为_size),比如:

cpp 复制代码
template <class T, class Alloc = std::allocator<T>>
class list {
private:
    typedef _List_node<T, Alloc> _Node;
    _Node* _M_head; 
    size_t _M_size; // 记录链表中元素的个数
};

这个**_M_size** 变量并非 C++ 标准所强制要求,不同的标准库实现可能会有所差异。

它的作用是方便快速获取链表的元素数量,在进行size()操作时,无需遍历链表来统计元素个数,从而提高效率。

然而,维护这个变量也需要额外的开销,在每次插入和删除操作时都需要更新它的值,以保证其准确性。

list 节点结构解析(模拟实现视角)

从模拟实现list的角度来看,节点结构体是构建链表的基础。一个典型的list节点结构体包含三个关键成员:

cpp 复制代码
template <class T, class Alloc = std::allocator<T>>
class list {
private:
    // 节点类型别名,实际是一个包含数据、前驱和后继指针的结构体
    typedef _List_node<T, Alloc> _Node;
   // 指向头节点的指针,头节点是双向循环链表的关键,不存储有效数据
    _Node* _M_head; 
    size_t _M_size; // 记录链表中元素的个数
};

struct list_node {
    T _data;            // 存储节点的数据
    list_node* _prev;   // 指向前驱节点的指针
    list_node* _next;   // 指向后继节点的指针
    // 构造函数,支持数据的初始化
    list_node(const T& x = T()) : _data(x), _prev(nullptr), _next(nullptr) {}
};

从底层角度来看,list实现就是顺序表与链表的实现,但区别在于:这里的节点数据被对象类型 T 所替代,这样就满足了可以传任意类型的数据类型(无论是int 还是自定义类型的类)。

list 成员函数全分类详解

构造与析构函数:list 对象的创建与销毁

构造函数参考文档

cpp 复制代码
default (1)	

explicit list (const allocator_type& alloc = allocator_type());

fill (2)	

explicit list (size_type n, const value_type& val = value_type(),                
const allocator_type& alloc = allocator_type());

range (3)	

template <class InputIterator>  list (InputIterator first, InputIterator last,  
const allocator_type& alloc = allocator_type());

copy (4)	

list (const list& x);

(1) 默认构造函数(空容器构造)

构造一个空容器,其中不包含任何元素。

(2) 填充构造函数

构造一个包含 n 个元素的容器,每个元素均为 val 的副本。

(3) 范围构造函数

根据范围 [first, last) 中的元素构造容器,元素按相同顺序依次构造,并与该范围中的对应元素保持一致。

(4) 拷贝构造函数

构造一个容器,其中包含 x 中每个元素的副本,且保持原有顺序。

容器在其生命周期内内部持有一个分配器(alloc)的副本,用于分配存储空间。

拷贝构造函数(4)所创建的容器会复制并使用 x 的分配器副本。

元素的存储空间均通过该内部分配器进行分配。

--- 来自网易翻译

默认构造函数(list ())

list()默认构造函数用于创建一个空的list对象。在执行该构造函数时,主要操作是初始化链表的哨兵位头节点,使其_prev和_next指针都指向自身,形成一个空的循环链表结构

cpp 复制代码
/*
(1) 默认构造函数(空容器构造)
构造一个空容器,其中不包含任何元素。
*/

default (1)	

explicit list (const allocator_type& alloc = allocator_type());

在我们日常的使用中,当我们需要一个空的list来后续添加元素时,就可以使用默认构造函数,例如:

cpp 复制代码
#include <iostream>
#include <list>
int main() {
    //这里创建的myList为空链表,后续可以通过push_back、push_front等函数向其中添加元素。
    std::list<int> myList; // 使用默认构造函数创建一个空的list
    return 0;
}
填充构造函数(list (n, val))

list(n, val)构造函数的功能是创建一个包含n个值为val的元素的list对象。

在其实现过程中,通过循环调用push_back或类似的尾插操作,将n个val值依次插入到链表中。

cpp 复制代码
/*
(2) 填充构造函数
构造一个包含 n 个元素的容器,每个元素均为 val 的副本。
*/
fill (2)	

explicit list (size_type n, const value_type& val = value_type(),                
const allocator_type& alloc = allocator_type());

//简化版:
list(size_type n, const value_type& val = value_type());


//使用:
#include <iostream>
#include <list>
int main() {
    std::list<int> myList(5, 10); // 创建一个包含5个值为10的元素的list
    for (int num : myList) {
        std::cout << num << " ";
    }
    return 0;
}

其中:

  • size_type是无符号整数类型,用于表示元素个数
  • value_type是list中元素的类型

val参数有默认值value_type(),即元素类型的默认构造值,这使得在调用时如果不传入val,也能创建出包含n个默认值元素的list。

迭代器区间构造函数(list (first, last))

list(first, last) 构造函数通过指定其他容器的迭代器区间[first, last){注意:**左闭右开}**来创建list,它会将该区间内的元素按顺序拷贝到新的list中。

这一特性使得list可以方便地从其他容器获取数据,实现不同容器间的数据共享与转换。

cpp 复制代码
/*
(3) 范围构造函数
根据范围 [first, last) 中的元素构造容器,元素按相同顺序依次构造,并与该范围中的对应元素保持一致。
*/
range (3)	

template <class InputIterator>  
list (InputIterator first, InputIterator last,  const allocator_type& alloc = allocator_type());

//简化版:
template <class InputIterator>
list(InputIterator first, InputIterator last);

//例子
#include <iostream>
#include <list>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::list<int> myList(vec.begin(), vec.end()); // 使用vector迭代器构造list
    for (int num : myList) {
        std::cout << num << " ";
    }

     int arr[] = {6, 7, 8, 9, 10};
    std::list<int> myList(arr, arr + sizeof(arr) / sizeof(arr[0])); // 使用数组迭代器构造list
    for (int num : myList) {
        std::cout << num << " ";
    }
    
    return 0;
}
拷贝构造函数(list (const list& x))

list(const list& x) 拷贝构造函数用于创建一个与已有list对象x完全相同的新list对象,它执行的是深拷贝操作

这意味着新创建的list会为每个元素分配独立的内存空间,形成一个独立的链表结构,而不是简单地共享原list的指针。

cpp 复制代码
/*
(4) 拷贝构造函数
构造一个容器,其中包含 x 中每个元素的副本,且保持原有顺序。
*/
copy (4)	
list (const list& x);

//例如:

#include <iostream>
#include <list>
int main() {
    std::list<int> originalList = {1, 2, 3};
    std::list<int> copiedList(originalList); // 使用拷贝构造函数
    for (int num : copiedList) {
        std::cout << num << " ";
    }
    return 0;
}
析构函数 (~list ())

~list() 析构函数用于销毁list对象,释放其占用的内存资源。

在执行析构函数时,首先会调用clear函数,遍历链表,逐个释放每个有效节点的内存,使链表变为空链表(仅保留哨兵位头节点)。然后,释放哨兵位头节点占用的内存空间,并将指向头节点的指针置为nullptr,以确保内存的正确释放和避免野指针问题。

cpp 复制代码
/*
这将销毁所有容器元素,并释放列表容器通过其分配器分配的所有存储容量。
*/
~list();

#include <iostream>
#include <list>
int main() {
    {
        std::list<int> myList = {1, 2, 3};
        // myList在此处有效,离开这个作用域时,析构函数会被调用
    }
    // myList已被销毁,其占用的内存已释放
    return 0;
}

list迭代器

这里就不过多赘述,感兴趣小伙伴可以关注博主的文章,有一篇专门讲迭代器详细说明的。

容量相关函数:判断 list 状态的核心接口

判空函数 empty ()

empty() 函数用于判断list 是否为空,它通过检测list的头节点的_next指针是否指向自身(或_prev指针是否指向自身,因为双向循环链表这两个条件等价)来实现的。

如果_next指针指向自身,说明链表中没有有效元素,即链表为空,此时empty()返回true;否则返回false。

其时间复杂度为 O (1),因为只需检查一个指针的指向,无需遍历链表。

cpp 复制代码
/*
返回列表容器是否为空(即其大小是否为0)。  
此函数不会以任何方式修改容器。如需清空列表容器的内容,请参阅 list::clear。
*/

bool empty() const;

//例子
#include <iostream>
#include <list>
int main() {
    std::list<int> myList;
    if (myList.empty()) {
        std::cout << "The list is empty." << std::endl;
    } else {
        std::cout << "The list is not empty." << std::endl;
    }
    myList.push_back(1);
    if (myList.empty()) {
        std::cout << "The list is empty." << std::endl;
    } else {
        std::cout << "The list is not empty." << std::endl;
    }
    return 0;
}

元素个数函数size ()

size() 函数返回list 中的有效元素个数。

在一些list的实现中,会维护一个专门记录元素个数的成员变量(如我们前面所提到的_M_size),这种情况下,size()函数只需直接返回该变量的值,时间复杂度为 O (1)。

然而,也有部分实现没有维护这个变量,此时size()函数需要遍历链表,从第一个有效元素开始,逐个计数,直到遇到哨兵位头节点,这种情况下时间复杂度为 O (n),其中n是list中的元素个数。

cpp 复制代码
/*
返回列表容器中元素的数量。
*/

size_type size() const;

//例如
#include <iostream>
#include <list>
int main() {
    std::list<int> myList;
    myList.push_back(1);
    myList.push_back(2);
    myList.push_back(3);
    std::cout << "The size of the list is: " << myList.size() << std::endl;
    return 0;
}

元素访问函数:list 的首尾元素操作

首元素访问front ()

front() 函数返回list中第一个有效元素的引用。通过这个引用,可以直接访问和修改第一个元素。

但需要注意的是,如果在空list上调用front()函数,会导致未定义行为,因为空list中没有第一个有效元素。因此,在调用front()之前,通常需要先使用empty()函数判断list是否为空。

cpp 复制代码
/*
访问第一个元素  
返回列表容器中第一个元素的引用。  
与返回指向同一元素的迭代器的成员函数 list::begin 不同,此函数直接返回引用。  
在空容器上调用此函数将导致未定义行为。

reference是元素的引用类型,const_reference是常量引用类型,用于const修饰的list对象。
*/
reference front();
const_reference front() const;
//例子:
#include <iostream>
#include <list>
int main() {
    std::list<int> myList = {1, 2, 3};
    if (!myList.empty()) {
        std::cout << "The first element is: " << myList.front() << std::endl;
        myList.front() = 10; // 修改第一个元素
        std::cout << "The modified first element is: " << myList.front() << std::endl;
    }
    return 0;
}

尾元素访问back ()

back() 函数返回list 中最后一个有效元素的引用,通过它可以访问和修改最后一个元素。

与front()函数类似,在空list上调用back()也会导致未定义行为,所以调用前同样需要用empty()判空。

cpp 复制代码
/*
访问最后一个元素  
返回列表容器中最后一个元素的引用。  

与成员函数 list::end 不同(它返回指向该元素之后位置的迭代器),此函数直接返回引用。  

在空容器上调用此函数将导致未定义行为。
*/

reference back();
const_reference back() const;

有关list迭代器失效之类的说明与解决

list 中,迭代器本质上是指向链表节点的指针或对指针的封装(底层来看,就是指针)。

迭代器失效的根本原因是其指向的节点被删除,导致指针悬空。而当list进行插入操作时,由于链表结构的特性,新节点的插入不会影响其他节点的内存地址,因此不会导致迭代器失效。

cpp 复制代码
插入操作后,it迭代器依然有效,因为插入操作没有改变其他节点的位置
it仍然指向原来的节点(现在是值为 0 的节点)。

#include <iostream>
#include <list>
int main() {
    std::list<int> myList = {1, 2, 3};
    auto it = myList.begin();
    myList.insert(it, 0); // 在开头插入0
    for (int num : myList) {
       std::cout << num << " "; // 输出 0 1 2 3
    }
    std::cout << std::endl;
    // it仍然有效,指向第一个元素0
    return 0;
}

但是:删除操作完全不一样!当删除list中的某个节点时,指向该节点的迭代器会失效。

这是因为删除操作会释放该节点的内存,使得迭代器指向的内存地址变为无效。

cpp 复制代码
在删除值为 2 的节点后,it迭代器失效
若继续使用it,如*it,会导致未定义行为,因为it指向的内存已经被释放。

#include <iostream>
#include <list>
int main() {
    std::list<int> myList = {1, 2, 3};
    auto it = myList.begin();
    ++it; // it指向2
    myList.erase(it); // 删除2
    // 此时it失效,不能再使用it进行操作
    for (int num : myList) {
        std::cout << num << " "; // 输出 1 3
    }
    std::cout << std::endl;
    return 0;
}

这里不同编译器会有不同的处理结果:
vs会强制检查这里的迭代器
而gcc/g++不会,当然,我们自己模拟实现list也可以加入这些判断

迭代器失效的解决方案

为了避免在list中删除元素时迭代器失效带来的问题,正确的做法是利用erase函数的返回值来更新迭代器

erase函数会返回一个指向被删除节点下一个节点的迭代器,通过捕获这个返回值,我们可以确保迭代器始终指向有效的节点。

cpp 复制代码
#include <iostream>
#include <list>
int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};
    auto it = myList.begin();
    while (it != myList.end()) {
        if (*it % 2 == 0) { // 如果是偶数
            it = myList.erase(it); // 删除当前节点,并更新it
        } else {
            ++it;
        }
    }
    for (int num : myList) {
        std::cout << num << " "; // 输出 1 3 5
    }
    std::cout << std::endl;
    return 0;
}

元素的增删查改函数

因为对于STL来说,这类函数大多数都有相似性,所以这里就简要说明,感兴趣可以看我博客文章:

assign()

将新内容分配给容器:为列表容器分配新内容,替换其当前内容,并相应地修改其大小。

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

int main() {
    std::list<int> lst = {1, 2, 3};
    
    // =============== 用法1:n个相同值替换 ===============
    lst.assign(4, 100);  // 清空原内容,插入4个100
    // lst = {100, 100, 100, 100}
    std::cout << "assign(4,100): ";
    for (int x : lst) std::cout << x << " ";
    std::cout << "\n";
    
    // =============== 用法2:迭代器范围替换(跨容器安全) ===============
    std::vector<int> vec = {10, 20, 30};
    lst.assign(vec.begin(), vec.end());  //  支持vector迭代器
    // lst = {10, 20, 30}
    std::cout << "assign(vec): ";
    for (int x : lst) std::cout << x << " ";
    std::cout << "\n";
    
    // =============== 用法3:C++11 初始化列表 ===============
    lst.assign({7, 8, 9});  // lst = {7, 8, 9}
    std::cout << "assign({7,8,9}): ";
    for (int x : lst) std::cout << x << " ";
    std::cout << "\n";

    
    return 0;
}

头插/头删 尾插/尾删

这里的emplace作为C++11之后的产物,效率有的时候会比正常的插入删除高,但实质上只是优化了构造函数的开销,这里不多说明。

cpp 复制代码
#include <deque>  // 包含 deque 容器的头文件

int main() {
    std::deque<int> dq;  // 创建一个 deque 容器,存储 int 类型的数据

    // push_front: 在 deque 的开头插入元素
    dq.push_front(1);  // 在 dq 的开头插入元素 1
    // 此时 dq 的内容为: [1]

    // push_back: 在 deque 的结尾插入元素
    dq.push_back(2);  // 在 dq 的结尾插入元素 2
    // 此时 dq 的内容为: [1, 2]

    // emplace_back: 在 deque 的结尾构造并插入元素
    dq.emplace_back(3);  // 在 dq 的结尾构造并插入元素 3
    // 此时 dq 的内容为: [1, 2, 3]

    // emplace: 在指定位置构造并插入元素(这里以在开头为例)
    dq.emplace(dq.begin(), 0);  // 在 dq 的开头构造并插入元素 0
    // 此时 dq 的内容为: [0, 1, 2, 3]

    // pop_front: 删除 deque 的第一个元素
    dq.pop_front();  // 删除 dq 的第一个元素(即 0)
    // 此时 dq 的内容为: [1, 2, 3]

    // pop_back: 删除 deque 的最后一个元素
    dq.pop_back();  // 删除 dq 的最后一个元素(即 3)
    // 此时 dq 的内容为: [1, 2]

    // emplace: 在指定位置(这里以在结尾为例)构造并插入元素
    dq.emplace(dq.end(), 4);  // 在 dq 的结尾构造并插入元素 4
    // 此时 dq 的内容为: [1, 2, 4]

    return 0;
}

其他函数

不多说,直接上代码说明:

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

int main() {
    // 创建一个 list 容器,存储 int 类型的数据
    std::list<int> myList;

    // 使用 insert 函数插入元素
    // insert(position, val) 在指定位置 position 插入值为 val 的元素
    myList.insert(myList.begin(), 10);  // 在列表开头插入值为 10 的元素
    myList.insert(myList.end(), 20);    // 在列表结尾插入值为 20 的元素
    // insert 也可以插入多个相同的元素
    myList.insert(myList.begin(), 2, 30);  // 在列表开头插入两个值为 30 的元素

    // 使用 erase 函数删除元素
    // erase(position) 删除指定位置的元素
    myList.erase(myList.begin());  // 删除列表的第一个元素(值为 30 的第一个元素)
    // erase 也可以删除一个范围内的元素
    myList.erase(++myList.begin(), myList.end());  // 删除从第二个元素到结尾的所有元素

    // 使用 swap 函数交换两个 list 的内容
    std::list<int> anotherList = {40, 50, 60};
    myList.swap(anotherList);  // 交换 myList 和 anotherList 的内容

    // 使用 resize 函数改变 list 的大小
    // resize(n) 将列表大小调整为 n,如果 n 较小则删除多余元素,如果 n 较大则用 0 填充
    myList.resize(2);  // 将列表大小调整为 2,多余的元素被删除

    // 使用 clear 函数清空 list
    myList.clear();  // 清空列表,列表大小变为 0

    // 输出 list 的内容以验证操作结果
    for (int value : myList) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    return 0;
}

list 特殊操作函数

list提供了一些特殊的操作函数,如sort、reverse、unique、merge 等,这些函数在数据处理中非常实用。

cpp 复制代码
#include <iostream>
#include <list>
int main() {
    std::list<int> myList = {3, 1, 4, 1, 5, 9, 2, 6};
    std::list<int> anotherList = {0, 7, 8};
    // 排序
    myList.sort();
    std::cout << "排序后的myList: ";
    for (int num : myList) {
        std::cout << num << " "; // 输出 1 1 2 3 4 5 6 9
    }
    std::cout << std::endl;

    // 去重
    myList.unique();
    std::cout << "去重后的myList: ";
    for (int num : myList) {
        std::cout << num << " "; // 输出 1 2 3 4 5 6 9
    }
    std::cout << std::endl;

    // 反转
    myList.reverse();
    std::cout << "反转后的myList: ";
    for (int num : myList) {
        std::cout << num << " "; // 输出 9 6 5 4 3 2 1
    }
    std::cout << std::endl;

    // 合并
    myList.merge(anotherList);
    std::cout << "合并后的myList: ";
    for (int num : myList) {
        std::cout << num << " "; // 输出 0 7 8 9 6 5 4 3 2 1
    }
    std::cout << std::endl;
    return 0;
}

这里有几点要注意:

  1. 排序不可以使用std中的排序(std::sort()),这是因为迭代器的性质不同,导致的传参无法进行相对应的操作(彩妙博客中其他文章会详细说明)。
  2. 去重操作中,链表必须是有序的,即在去重前必须要对链表排序!
  3. 合并本质上是链表的排序拼接,进而会导致被传参的链表在执行完操作后会变为空链表。

list 与 vector 容器对比分析(本质上就是顺序表与链表的对比)

|--------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| 对比维度 | list | vector |
| 底层结构 | 带头双向循环链表 | 连续内存数组 |
| 访问方式 | 不支持随机访问,通过迭代器顺序访问,访问第 n 个元素时间复杂度为 O (n) | 支持随机访问,可通过下标直接访问,时间复杂度为 O (1) |
| 插入删除效率 | 在任意位置插入和删除操作效率高,时间复杂度为 O (1),但前提是已知插入删除位置的迭代器,获取该迭代器可能需要 O (n) 时间 | 在尾部插入和删除操作效率较高,均摊时间复杂度为 O (1);在中间或头部插入删除需要移动大量元素,时间复杂度为 O (n),且插入时若容量不足还需重新分配内存和拷贝元素 |
| 迭代器类型 | 双向迭代器,仅支持 ++ 和 -- 操作 | 随机访问迭代器,支持 ++、--、+、- 等操作 |
| 内存管理 | 按需分配内存,每个节点独立申请内存,内存分配分散,不会造成内存浪费,但可能产生较多内存碎片 | 预分配一定容量内存,当元素个数超过容量时,会重新分配更大的内存,然后将原元素复制到新内存,可能会造成内存浪费,但内存布局紧凑 |
| 适用场景 | 适用于需要频繁插入和删除操作,且对随机访问需求较少的场景,如实时任务调度系统、文本编辑器的撤销重做操作记录等 | 适用于需要频繁随机访问,且插入删除操作较少的场景,如游戏开发中对大量角色位置信息的快速查询、数据统计分析中对数据的频繁读取等 |


本篇到这里就结束了,喜欢文章的小伙伴可以关注一下彩妙,我们下一篇再见~

相关推荐
梵刹古音1 小时前
【C语言】 定义变量
c语言·开发语言·嵌入式
草履虫建模2 小时前
Java 基础到进阶|专栏导航:路线图 + 目录(持续更新)
java·开发语言·spring boot·spring cloud·maven·基础·进阶
Zhu_S W2 小时前
Java多进程监控器技术实现详解
java·开发语言
m0_736919102 小时前
C++中的观察者模式
开发语言·c++·算法
我能坚持多久2 小时前
D19—C语言动态内存管理全解:从malloc到柔性数组
c语言·开发语言·柔性数组
咚为2 小时前
Rust Cell使用与原理
开发语言·网络·rust
青芒.2 小时前
macOS Java 多版本环境配置完全指南
java·开发语言·macos
多打代码2 小时前
2026.1.29 复原ip地址 & 子集 & 子集2
开发语言·python
代码无bug抓狂人2 小时前
C语言之宝石组合(蓝桥杯省B)
c语言·开发语言·蓝桥杯