C++中list容器使用详解

1.list容器

文章目录

1.基本概念

C++的list容器是标准模板库(STL)中的一种顺序容器,它基于双向链表实现。

  1. 双向链表结构:每个元素(节点)都包含数据部分和两个指针,分别指向前一个元素和后一个元素,这使得在链表的任何位置进行插入和删除操作都非常高效,时间复杂度为O(1)。

  2. 动态大小list容器的大小可以在运行时动态改变,即可以在程序运行过程中添加或移除元素。

  3. 随机访问 :不同于vectorarray等连续内存的容器,list不支持随机访问迭代器,不能直接通过索引获取元素,而需要通过迭代器遍历。

  4. 迭代器稳定性 :在list中插入或删除元素不会导致其他迭代器失效,除了指向被删除元素的迭代器。这是因为它通过调整相邻节点的指针来维护链表结构,而不需要移动元素或重新分配内存。

  5. 空间开销 :每个元素都有额外的存储开销用于存储指针(前驱和后继节点的地址),因此相比于数组或vectorlist在存储同样数量元素时通常占用更多的内存。

  6. 常用操作list容器提供了丰富的成员函数,如push_back()push_front()在链表尾部或头部添加元素,pop_back()pop_front()从链表尾部或头部移除元素,以及在指定位置插入或删除元素的函数等。

  7. 适用场景list特别适合需要频繁进行插入和删除操作,而对随机访问需求不高的场景。例如,实现队列、堆栈、最近最少使用(LRU)缓存等数据结构时,list是一个很好的选择。

2.基本操作
1. 构造与初始化
  • 默认构造list<T> lst; 创建一个空的list,其中T是存储元素的数据类型。
  • 拷贝构造list<T> lst2(lst1); 从另一个list (lst1)拷贝构造一个新的list (lst2)。
  • 区间构造list<T> lst(start, end); 从迭代器startend范围内的元素构造list
  • 初始化列表构造 :C++11起,可以直接用初始化列表创建list,如list<int> lst = {1, 2, 3};
2. 迭代器
  • 正向迭代器反向迭代器list提供iteratorconst_iterator用于正向遍历,以及reverse_iteratorconst_reverse_iterator用于反向遍历。由于链表的特性,这些迭代器都是双向迭代器,但不是随机访问迭代器。
3. 元素访问
  • front()back():分别返回第一个元素和最后一个元素的引用。
  • 访问特定元素 :由于缺乏随机访问迭代器,访问list中的特定元素需要从头或尾开始迭代,或者维护额外的迭代器/指针。
4. 修改操作
  • 插入操作

    • push_back(val):在链表末尾添加元素。
    • push_front(val):在链表开头添加元素。
    • insert(pos, val)insert(pos, n, val)insert(pos, iter, iterEnd):在迭代器pos之前插入单个元素、n个相同元素或[iter, iterEnd)区间的所有元素。
  • 删除操作

    • pop_back():删除链表最后一个元素。
    • pop_front():删除链表第一个元素。
    • erase(pos)erase(start, end):删除单个元素或区间内的所有元素。
5. 大小管理
  • size():返回链表中元素的数量。
  • empty():检查链表是否为空。
6. 合并与分割
  • merge(list& other) :将另一个无序的list合并到当前list中,要求当前list已排序。
  • splice(pos, list& other [, iter]) :将另一个list的所有元素移到当前list的指定位置,可选地也可以只移动一个特定元素。
  • remove(val) :删除所有值为val的元素。
  • remove_if(pred) :删除满足谓词函数pred的元素。
7. 排序与查找

虽然链表不适合于高效的随机访问,但list依然支持一些基本的排序和查找操作:

  • sort():对链表进行原地排序,要求元素类型支持比较操作。
  • unique():删除相邻的重复元素。
  • find(val):使用迭代器遍历查找指定值的元素。
8. 异常安全

list的操作通常提供较强的异常安全性,意味着即使在操作过程中发生异常,容器的状态也是良好的,不会泄露资源或破坏数据结构的完整性。

综上所述,list容器因其高效的插入和删除性能,在需要频繁变动的序列操作中非常有用,尽管牺牲了随机访问的效率。理解和掌握这些特性和操作,能帮助开发者更有效地利用list解决特定问题。

3.list的构造函数

C++ list 容器提供了多种构造函数来初始化容器。

1. 默认构造函数
cpp 复制代码
list<T> lst;

这个构造函数创建一个空的 list,其中 T 是容器内元素的数据类型。

2. 拷贝构造函数
cpp 复制代码
list<T> lst1(anotherList);

这个构造函数用于拷贝一个已存在的 list (anotherList),创建一个内容完全相同的新的 list (lst1)。

3. 列表初始化构造函数

C++11 起,可以使用初始化列表来直接初始化 list

cpp 复制代码
list<int> lst = {1, 2, 3, 4, 5};

这个例子创建了一个包含整数 1 到 5 的 list

4. 区间构造函数
cpp 复制代码
list<T> lst(startIterator, endIterator);

这个构造函数根据两个迭代器所指定的区间 [startIterator, endIterator) 来创建 list,复制该区间内所有元素到新创建的 list 中。这里的迭代器可以来自任何能够提供适当类型的兼容序列,比如另一个 listvector 等。

5. 使用 allocator 构造

虽然较少直接使用,list 还可以通过指定自定义的分配器(allocator)来构造:

cpp 复制代码
list<T, Allocator> lst(customAllocator);

这里的 Allocator 是一个符合 C++ 标准分配器要求的类,用于管理元素的内存分配和释放。如果不指定,默认使用 std::allocator<T>

示例
cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,将被打印出来。
 */
void printList(const list<int>&L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表L,并添加一些元素
    list<int>L;
    L.push_back(30);
    L.push_back(40);
    L.push_back(50);
    L.push_back(10);
    L.push_back(20);

    // 打印列表L
    printList(L);

    // 通过迭代器初始化一个新的列表L1,包含与L相同的元素
    list<int>L1(L.begin(), L.end());
    printList(L1);

    // 初始化一个包含10个值为100的元素的列表L2
    list<int>L2(10,100);
    printList(L2);

    // 初始化一个常量引用L3,指向列表L2
    const list<int>&L3(L2);
    printList(L3);

    return 0;
}
4.list赋值和交换

在C++中,list容器提供了赋值操作符和交换函数来改变容器的内容。以下是关于list容器赋值和交换的详细说明:

1.赋值操作符
复制赋值
cpp 复制代码
list<T>& operator=(const list<T>& rhs);

这个操作符将rhs(右侧操作数,另一个list对象)的内容复制给左侧的list对象。执行此操作后,左侧的list将拥有与rhs相同的内容,而原先的内容会被覆盖。此操作返回对左侧list的引用,以便可以链接其他操作。

移动赋值
cpp 复制代码
list<T>& operator=(list<T>&& rhs) noexcept(see below);

自C++11起,引入了移动赋值,它将rhs的资源"移动"(而不是复制)给左侧的list对象,如果rhs的资源不再需要,则可能避免了复制的成本。这对于临时对象或即将销毁的对象尤其有用,可以提高效率并减少资源消耗。此操作也是返回左侧list的引用。

2.交换操作
cpp 复制代码
void swap(list<T>& other) noexcept(see below);

swap函数允许交换两个list容器的内容,而无需创建任何副本。它是一种高效的操作,特别是在不需要保留原容器内容的情况下。这个操作是noexcept的,意味着它承诺不抛出异常(除非容器的元素类型的操作可能抛出异常)。

3.示例
cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,将被打印。
 */
void printList(const list<int>&L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表L,并打印其内容
    list<int>L = {1,3,2,4,6};
    printList(L);
    
    // 创建一个空列表L1,并将其内容设置为列表L的副本,然后打印L1的内容
    list<int>L1;
    L1 = L;
    printList(L1);
    
    // 将列表L的内容移动到列表L1中,并打印L1的新内容
    L1 = std::move(L);
    printList(L1);
    
    // 为列表L2分配L1的内容,并打印L2的内容
    list<int>L2;
    L2.assign(L1.begin(), L1.end());
    printList(L2);
    
    // 为列表L2分配10个值为100的元素,并打印L2的内容
    L2.assign(10,100);
    printList(L2);
    
    // 交换列表L2和L1的内容,并分别打印它们的内容
    L2.swap(L1);
    printList(L2);
    printList(L1);

    return 0;
}

注意:在上述示例中,对于list容器,移动赋值和复制赋值的实际效果是相同的,因为list中的元素是以拷贝的方式存储的,不涉及内部指针的转移。但对于具有动态分配的大型或复杂对象的容器,移动赋值可以显著提升性能。

5.list的大小操作

C++ list 容器提供了几种与大小操作相关的成员函数,用于查询和修改容器的大小。以下是主要的大小操作函数:

1.查询大小
  1. size()

    • 函数原型:size_type size() const;

    • 功能:返回容器中元素的数量。

    • 用法示例:

      cpp 复制代码
      std::list<int> myList = {1, 2, 3, 4, 5};
      std::cout << "List size: " << myList.size() << std::endl;
  2. empty()

    • 函数原型:bool empty() const;

    • 功能:检查容器是否为空。如果容器没有元素,返回true;否则,返回false

    • 用法示例:

      cpp 复制代码
      if (myList.empty()) {
          std::cout << "List is empty." << std::endl;
      } else {
          std::cout << "List is not empty." << std::endl;
      }
2.修改大小
  1. resize(num)

    • 函数原型:void resize(size_type count);

    • 功能:重新指定容器的长度为count。如果容器需要变长,会以默认构造的元素值填充新增的位置;如果容器需要变短,则会移除多出的元素。

    • 用法示例:

      cpp 复制代码
      myList.resize(3); // 如果myList原来有5个元素,现在将只剩下前3个
  2. resize(num, elem)

    • 函数原型:void resize(size_type count, const T& value);

    • 功能:与上面类似,但当容器需要增长时,会用value来初始化新增的元素。

    • 用法示例:

      cpp 复制代码
      myList.resize(7, 0); // 将myList扩展到7个元素,新增的位置用0填充

这些操作允许动态地调整list容器的大小,无论是查询当前状态还是根据需要调整元素数量,都非常直接且方便。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,将被打印出来。
 */
void printList(const list<int>& L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表
    list<int> L = {1,2,5,6,4};
    // 打印列表的所有元素
    printList(L);
    // 输出列表的大小
    cout << L.size() << endl;
    // 如果列表不为空,则再次打印列表的所有元素
    if(!L.empty()){
        printList(L);
    }
    // 将列表的大小调整为10,可能删除或添加元素
    L.resize(10);
    // 打印调整大小后的列表的所有元素
    printList(L);
    // 将列表的大小调整为15,如果需要,用3填充新增的部分
    L.resize(15,3);
    // 打印再次调整大小后的列表的所有元素
    printList(L);

    return 0;
}
6.list的插入和删除

C++ list 容器提供了丰富的成员函数来进行元素的插入和删除操作,这些操作基于双向链表的特性,能够高效地在容器的任何位置插入或删除元素,时间复杂度通常为 O(1)。

1.插入操作
  1. push_back(value)

    • 在链表的尾部插入一个元素。

    • 示例:

      cpp 复制代码
      lst.push_back(42);
  2. push_front(value)

    • 在链表的头部插入一个元素。

    • 示例:

      cpp 复制代码
      lst.push_front(13);
  3. insert(iterator pos, value)

    • 在迭代器 pos 指定的位置之前插入一个元素。

    • 示例:

      cpp 复制代码
      auto it = lst.begin();
      ++it; // 移动到第二个元素之前
      lst.insert(it, 99);
  4. insert(iterator pos, size_t count, value)

    • 在迭代器 pos 指定的位置之前插入 countvalue 复制。

    • 示例:

      cpp 复制代码
      lst.insert(lst.begin(), 3, 7);
  5. insert(iterator pos, InputIt first, InputIt last)

    • 在迭代器 pos 指定的位置之前插入由 [first, last) 迭代器指定的范围内所有元素的副本。

    • 示例:

      cpp 复制代码
      std::list<int> anotherList = {1, 2};
      lst.insert(lst.end(), anotherList.begin(), anotherList.end());
2.删除操作
  1. pop_back()

    • 删除链表的最后一个元素。

    • 示例:

      cpp 复制代码
      lst.pop_back();
  2. pop_front()

    • 删除链表的第一个元素。

    • 示例:

      cpp 复制代码
      lst.pop_front();
  3. erase(iterator pos)

    • 删除迭代器 pos 指向的元素。

    • 示例:

      cpp 复制代码
      auto it = lst.begin();
      ++it; // 删除第二个元素
      lst.erase(it);
  4. erase(iterator first, iterator last)

    • 删除由 [first, last) 迭代器指定范围内的所有元素。

    • 示例:

      cpp 复制代码
      lst.erase(lst.begin(), lst.begin()++); // 删除前两个元素  不支持lst.begin()+2的操作 
3.注意事项
  • 插入和删除操作不会导致其他未被直接删除的迭代器失效。
  • 在进行插入或删除操作后,先前获取的迭代器和引用可能会变得无效,特别是当它们指向被删除或插入位置附近的元素时。
  • list容器的插入和删除操作相比vector等连续存储容器具有更高的效率,特别是在中间位置进行操作时,因为它们不需要移动后续的元素。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印整数列表的所有元素。
 * @param L 一个整数列表,其元素将被打印。
 */
/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,将被打印出来。
 */
void printList(const list<int>& L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表
    // 初始化一个整数列表
    list<int> L = {1,2,5,6,4};
    
    // 打印列表的所有元素
    // 打印列表的所有元素
    printList(L);
    
    // 向列表末尾添加一个元素
    // 输出列表的大小
    L.push_back(10);
    // 打印修改后的列表
    printList(L);
    
    // 向列表前端添加一个元素
    L.push_front(20);
    // 打印修改后的列表
    printList(L);
    
    // 移除列表前端的元素
    L.pop_front();
    // 打印修改后的列表
    printList(L);
    
    // 移除列表末尾的元素
    L.pop_back();
    // 打印修改后的列表
    printList(L);
    
    // 在列表中插入一个元素
    auto it = L.begin();
    ++it;
    L.insert(it,99);
    // 打印修改后的列表
    printList(L);
    
    // 在列表中插入多个相同元素
    L.insert(it , 10,100);
    // 打印修改后的列表
    printList(L);
    
    // 从另一个列表复制元素到当前列表
    list<int>L1;
    L1.insert(L1.begin(),L.begin(), L.end());
    // 打印修改后的列表
    printList(L1);
    
    // 移除列表的第一个元素
    L1.erase(L1.begin());
    // 打印修改后的列表
    printList(L1);
    
    // 移除列表的第一个元素到第二个元素之间的所有元素
    L1.erase(L1.begin(),++L1.begin());
    // 打印修改后的列表
    printList(L1);
    
    // 移除列表中所有值为100的元素
    L1.remove(100);
    // 打印修改后的列表
    printList(L1);
    
    // 清空列表
    L1.clear();
    // 打印修改后的列表
    printList(L1);
    
    return 0;
}
7.list的数据存取

在C++的list容器中,由于它基于双向链表实现,不支持随机访问迭代器,因此数据存取主要通过迭代器进行顺序访问。以下是list容器进行数据存取的主要方法:

1.访问元素
  1. front() 和 back()
    • T& front();
    • const T& front() const;
    • T& back();
    • const T& back() const;
    • 这些函数分别返回对链表首元素和尾元素的引用。如果链表为空,调用这些函数会未定义行为。
2.遍历元素

由于list不支持随机访问迭代器,遍历元素通常需要使用正向迭代器或反向迭代器:

cpp 复制代码
std::list<int> myList = {1, 2, 3, 4, 5};

// 正向遍历
for(std::list<int>::iterator it = myList.begin(); it != myList.end(); ++it) {
    std::cout << *it << " ";
}

// 或者使用C++11范围for循环
for(int val : myList) {
    std::cout << val << " ";
}

// 反向遍历
for(std::list<int>::reverse_iterator rit = myList.rbegin(); rit != myList.rend(); ++rit) {
    std::cout << *rit << " ";
}

list容器的数据存取主要依赖于迭代器的顺序遍历,适合于需要高效插入和删除但不频繁随机访问的场景。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,用于打印其所有元素。
 */
void printList(const list<int>& L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表L,包含一组数字
    list<int>L = {1,3,4,3,2,5};
    
    // 调用函数打印列表的所有元素
    printList(L);
    
    // 输出列表的第一个元素
    cout << L.front() << endl;
    
    // 输出列表的最后一个元素
    cout << L.back() << endl;
    
    // 获取列表的开始迭代器,并移动到第二个元素
    auto it = L.begin();
    cout << *(++it) << endl;
    
    return 0;
}
8.list的反转和排序

C++ list 容器提供了内置的方法来实现反转和排序操作。以下是这两个操作的详细介绍:

1.反转

list容器提供了成员函数reverse()来反转容器中元素的顺序。调用此函数后,容器中的元素会按照相反的顺序排列。

cpp 复制代码
std::list<int> myList = {1, 2, 3, 4, 5};
myList.reverse();

在这段代码中,myList的内容将会变为5, 4, 3, 2, 1

2.排序

排序操作稍微复杂一点,因为STL的list容器没有直接提供像vector那样的sort()成员函数。不过,你可以使用标准算法std::sort(),但需要注意的是,std::sort()要求容器提供随机访问迭代器,而list只提供双向迭代器,因此不能直接使用std::sort()

对于list,应该使用std::list特有的成员函数sort(),它需要元素之间定义了比较操作。如果元素类型自身定义了<运算符(即实现了std::less比较),那么可以直接调用:

cpp 复制代码
std::list<int> myList = {5, 3, 1, 4, 2};
myList.sort(); // 对于int类型,默认升序排序

如果需要自定义排序规则,可以传递一个比较函数对象:

cpp 复制代码
struct DescendingComparator {
    bool operator()(const int& a, const int& b) const {
        return a > b; // 降序
    }
};

std::list<int> myList = {1, 3, 5, 2, 4};
myList.sort(DescendingComparator()); // 降序排序

或者使用C++11以后的lambda表达式简化代码:

cpp 复制代码
std::list<int> myList = {1, 3, 5, 2, 4};
myList.sort([](const int& a, const int& b) { return a > b; }); // 降序排序

请注意,list::sort()函数是稳定的排序,意味着相等的元素的相对顺序不会改变,并且这个操作在平均情况下具有O(n log n)的时间复杂度。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,用于打印其所有元素。
 */
void printList(const list<int>& L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表L
    list<int>L = {1,3,2,5,6};
    // 打印列表L的原始顺序
    printList(L);
    // 反转列表L中的元素顺序
    L.reverse();
    // 打印反转后的列表L
    printList(L);
    // 对列表L进行排序(升序)
    L.sort();
    // 打印排序后的列表L
    printList(L);
    // 对列表L进行排序(降序)
    L.sort([](const int &a,const int &b){return a > b;});
    // 打印降序排序后的列表L
    printList(L);
    return 0;
}
9.list排序案例

将学生类按年龄降序排列,年龄相同则按身高排列。

cpp 复制代码
#include <iostream>
#include <list>
#include <utility>
using namespace std;

/**
 * 学生类,用于存储学生的信息。
 * @param name 学生的姓名。
 * @param age 学生的年龄。
 * @param len 学生的身高。
 */
class Student{
public:
    string name;
    int len;
    int age;
    /**
     * 构造函数,初始化学生对象。
     * @param name 学生的姓名。
     * @param age 学生的年龄。
     * @param len 学生的身高。
     */
    Student(string name,int age,int len) : name(std::move(name)),age(age),len(len){

    }
};

/**
 * 打印学生列表。
 * @param L 学生列表。
 */
void printList(const list<Student>&L){
    for(const Student& it : L){
        cout << it.name << " " << it.age << " " << it.len << endl;
    }
}

int main(){
    // 创建并添加学生对象到列表
    Student stu("张三",18,158);
    list<Student>L;
    L.push_back(stu);
    Student stu2("李四",16,155);
    L.push_back(stu2);
    Student stu3("王五",17,166);
    L.push_back(stu3);
    Student stu4("赵六",19,165);
    L.push_back(stu4);
    Student stu5("孙七",16,185);
    L.push_back(stu5);
    // 打印学生列表
    printList(L);
    cout << "--------------------" << endl;
    // 对学生列表按年龄和身高进行排序
    L.sort([](const Student &stu,const Student &stu1){
        if(stu.age != stu1.age) return stu.age > stu1.age;
        else return stu.len > stu1.len;
    });
    // 打印排序后的学生列表
    printList(L);
    return 0;
}

Lambda表达式是C++11引入的一种匿名函数对象,它允许快速定义并创建小型的、一次性的功能函数。Lambda表达式提供了一种简洁、灵活的方式来编写内联代码块,特别适用于传递给算法或作为回调函数。Lambda的基本语法结构如下:

cpp 复制代码
[capture-list] (parameters) -> return-type { function-body }

这里每个部分的含义如下:

  • capture-list (捕获列表): 定义了lambda函数体内部可以访问的外部变量。可以捕获外部变量的值(值捕获)或引用(引用捕获),也可以默认捕获整个作用域。例如,[x, &y]表示捕获x的值和y的引用。

  • parameters(参数列表): 类似于常规函数的参数列表,定义了lambda接受的参数。

  • -> return-type(返回类型声明): 可选部分,显式指定lambda表达式的返回类型。如果不写,默认根据函数体进行推导。

  • function-body(函数体): 包含了实际执行的代码。

Lambda表达式提高了代码的灵活性和可读性,使得可以在不定义独立函数的情况下直接在代码中创建小型函数。

相关推荐
小悟空GK20 分钟前
Http介绍
开发语言
502胶水20530 分钟前
腾讯地图异步调用
开发语言·ios·swift
森龙安39 分钟前
指针 || 引用 || const || 智能指针 || 动态内存
c++
SwBack40 分钟前
【pearcmd】通过pearcmd.php 进行GetShell
android·开发语言·php
Lingoesforstudy41 分钟前
c#中的超时终止
开发语言·笔记·c#
**K1 小时前
C++ 智能指针使用不当导致内存泄漏问题
开发语言·c++·算法
u0104058361 小时前
如何利用Java Stream API简化集合操作?
java·开发语言
湫兮之风1 小时前
C++:.front()函数作用
开发语言·c++
小老鼠不吃猫1 小时前
力学笃行(四)Qt 线程与信号槽
c++·qt·信息可视化
小羊子说1 小时前
Android 开发中 C++ 和Java 日志调试
android·java·c++