在C++中,容器的概念是指一种对象类型,它可以持有其他对象或指向其他对象的指针。这种对象类型 在数据存储上提供了一种有效的方式来管理一组元素。容器在C++中通常是模板类的形式。
一般来说,容器内的元素都是相同类型的。即如果该容器内存放的是int类型的数据,那么其他所有的数 据都是int类型的。如果你想存储多种类型的数据,建议使用结构体。
联系:数组在广义上可以被视为一种容器,因为它用于存储一组相同类型的元素。然而,在 C++标准库的语境中,当我们提到“容器”,通常不包括数组。
区别:
创建和大小调整:
数组:在创建数组时,必须指定其大小,且一旦创建,数组的大小就是固定的,不能动态改变。
容器:容器的创建不需要预先指定大小,且其大小可以动态改变,根据需要添加或删除元素。
存储方式:
数组:数组在内存空间上是连续存储的,这意味着数组元素在内存中的位置是连续的,可以 通过下标直接访问。
容器:不同的容器有不同的存储方式。例如, std::vector 和 std::deque 是连续存储的, 而 std::list 则是链式存储的。
访问效率:
数组:通过数组下标可以直接访问对应位置的元素,因此访问效率非常高。
容器:连续存储的容器(如 std::vector 和 std::deque )也支持通过下标访问元素,但链 式存储的容器(如 std::list )则不支持,需要遍历链表来查找元素。
元素操作:
数组:数组的元素操作通常包括赋值、读取等,但由于其连续,在数组中间位置插入或删除 元素比较麻烦,需要移动其他元素。
容器:容器提供了更丰富的元素操作。例如, std::list 支持在容器中间位置插入或删除元 素,而无需移动其他元素。
而C++中为什么会提出容器的概念?其实从上述问题中我们就可以看到了
动态内存管理: 容器类负责动态分配和释放内存,简化了内存管理的复杂性。使用容器时,我们无需担心数组越界、内 存泄漏等问题,因为容器类内部已经实现了这些内存管理的细节。
自动扩展和收缩:一些容器类(如 std::vector )能够自动扩展和收缩,以适应存储元素数量的变化。这意味着我们可以 将元素添加到容器中,而无需预先分配固定大小的内存空间。
代码复用和可维护性: 使用容器类可以提高代码的复用性和可维护性。由于容器类提供了通用的数据结构和操作接口,我们可 以将更多的精力放在实现业务逻辑上,而不是在数据结构和内存管理上。此外,使用容器类还可以减少 代码中的重复部分,提高代码的可读性和可维护性。
基于容器内部数据的组织方式、访问方式以及操作特性,容器大致可分为两大类:顺序(序列)容器和 关联容器。
顺序容器是将单一类型的元素聚集起来成为容器,并根据元素的插入位置来存储和访问这些元素。顺序 容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。
1. 构造函数:用于创建容器对象,可以指定初始大小、元素值等。
2. 赋值操作:可以使用赋值运算符( = )将一个容器的内容赋值给另一个容器。
3. 元素访问:通过下标运算符( [] )或迭代器( iterator )来访问容器中的元素。对于支持随机 访问的容器(如 vector 和 array ),可以直接使用下标访问;对于只支持顺序访问的容器(如 list ),则需要使用迭代器进行遍历。
4. 容器大小:可以使用 size() 成员函数来获取容器中元素的数量。
5. 添加元素:对于支持在尾部添加元素的容器(如 vector 和 deque ),可以使用 push_back() 方 法;对于支持在头部添加元素的容器(如 list 和 deque ),可以使用 push_front() 方法。
6. 删除元素:可以使用 erase() 成员函数来删除容器中的指定元素或元素范围。此外,对于支持在尾 部删除元素的容器,还可以使用 pop_back() 方法;对于支持在头部删除元素的容器,可以使用 pop_front() 方法。
7. 修改元素:直接通过下标或迭代器访问并修改容器中的元素值。
8. 关系运算符:如 == 、 != 、 < 、 和 >= ,用于比较两个容器是否相等或判断一个容器是否小 于、大于另一个容器。
9. 获取迭代器:使用 begin() 和 end() 成员函数来获取指向容器第一个元素和最后一个元素之后位 置的迭代器,用于遍历容器中的元素。
迭代器:容器(集合)应该提供一种方法。顺序的访问一个集合对象中的各个元素,而又不暴露该 对象的内部表示。
在C++标准模板库(STL)中, array 是一个顺序容器,它封装了固定大小的数组,并提供了与自定义数 组类似的接口,但增加了类型安全和一些额外的功能。 array 容器允许程序员以更安全、更便捷的方式 处理固定大小的数组。头文件: #include
array 的主要特点如下:
固定大小:与自定义数组一样, array 在创建时指定大小,之后其大小不能再改变。
随机访问: array 支持随机访问其元素,可以通过下标运算符 [] 直接访问任意位置的元素。
类型安全:使用 array 时,编译器会检查类型,确保只有正确类型的元素被添加到容器中。
空间连续:使用一组连续的内存空间来存储具有相同类型的数据。
常用方法:
构造函数和赋值
std::array arr1; // 默认构造函数,所有元素都初始化为零(对于基本类型)
std::array arr2 = {1, 2, 3, 4, 5}; // 列表初始化
std::array arr3(arr2); // 复制构造函数
std::array arr4 = std::move(arr3); // 移动构造函数
std::array arr5 = {std::begin(arr2), std::end(arr2)}; // 从迭代器范围初始化
arr1 = arr2; // 赋值操作
容量和大小
size_t size() const; // 返回数组的大小
访问元素
reference operator[](size_t index); // 返回位于index位置的元素的引用
const_reference operator[](size_t index) const; // 返回位于index位置的元素的常量引用
reference at(size_t index); // 返回位于index位置的元素的引用,越界访问时抛出异常
// 返回位于index位置的元素的常量引用,越界访问时抛出异常
const_reference at(size_t index) const;
reference front(); // 返回数组第一个元素的引用
const_reference front() const; // 返回数组第一个元素的常量引用
reference back(); // 返回数组最后一个元素的引用
const_reference back() const; // 返回数组最后一个元素的常量引用
迭代器
iterator begin(); // 返回指向数组第一个元素的迭代器
const_iterator begin() const; // 返回指向数组第一个元素的常量迭代器
iterator end(); // 返回指向数组末尾之后位置的迭代器
const_iterator end() const; // 返回指向数组末尾之后位置的常量迭代器
reverse_iterator rbegin(); // 返回指向数组最后一个元素的反向迭代器
const_reverse_iterator rbegin() const; // 返回指向数组最后一个元素的常量反向迭代器
reverse_iterator rend(); // 返回指向数组开始之前位置的反向迭代器
const_reverse_iterator rend() const; // 返回指向数组开始之前位置的常量反向迭代器
随机访问迭代器: std::array 的迭代器是随机访问迭代器,这意味着你可以使用迭代器进 行算术运算(如 ++it , --it , it + n , it - n , it[n] 等)。
不失效:与 std::vector 不同, std::array 的大小是固定的,因此在修改数组内容时 (例如赋值操作),迭代器不会失效。
修改元素
void fill(const value_type& value); // 用给定的值填充整个数组
void swap(array& other); // 交换两个数组的内容
使用 std::array ,你可以通过下标操作符 [] 直接访问元素,也可以使用 at() 成员函数(它会在索引 越界时抛出 std::out_of_range 异常)。此外, std::array 也提供了迭代器接口,允许你使用范围 for循环遍历数组元素。
#include
#include
using namespace std;
int main()
{
//构造函数:用于创建array对象并初始化其元素。可以通过列表初始化或逐个赋值来初始化元素。
std::array arr1 = { 1, 2, 3, 4, 5 };
std::array arr2; // 默认构造,内容为随机值
//元素访问:通过下标运算符[]访问array中的元素。下标从0开始。
arr1[2] = 10; // 访问arr1中的第三个元素,并修改
//arr1[5] = 10;//error 越界
//大小查询:使用size()成员函数获取array的大小(元素数量)
// 由于array的大小是固定的,所以size()总是返回在模板参数中指定的大小。
cout << arr1.size() << endl; 获取arr1的大小
//迭代器:array支持迭代器,可以使用begin()和end()成员函数获取指向array第一个元素和尾后
//位置的迭代器。这些迭代器可以用于遍历array中的元素。
for (std::array::iterator it = arr1.begin(); it != arr1.end(); it++)
{
// 使用迭代器it访问元素
cout << *it << " ";
}
cout << endl;
//赋值操作:可以使用赋值运算符=将一个array的内容赋给另一个同类型的array。
std::array arr3;
arr3 = arr1; // 将arr1的内容赋给arr3
//比较操作:可以使用关系运算符(如==、!=)来比较两个array是否相等
if (arr1 == arr2)
{
// arr1和arr2相等
cout << "arr1 == arr2" << endl;
}
else
{
cout << "arr1 != arr2" << endl;
}
return 0;
}
std::array不支持添加或删除元素。这是因为std::array是一个固定大小的容器,其大小在创建时通过模 板参数指定,并且之后不能改变。其跟普通数组十分类似,大小固定且不支持插入和删除数据,那么如 何解决这个问题呢?
在C++中, vector (向量)是一个顺序容器,它封装了动态大小数组的功能,并提供了许多有用的成员 函数和方法来管理这些元素。 vector 在内存中是连续存储的,这意味着它的元素在内存中占用一段连 续的空间。头文件: #include
vector 的主要特性:
1. 动态大小:与固定大小的数组不同, vector 可以根据需要增长或缩小,以适应存储的元素数量。
2. 连续内存: vector 的元素在内存中连续存储,这使得对元素的访问(特别是通过下标访问)非常 高效。
3. 迭代器支持: vector 支持迭代器,允许遍历、访问和修改容器中的元素。
常用方法:
构造函数和赋值
std::vector vec; // 默认构造函数
std::vector vec(10); // 包含 10 个默认初始化元素的 vector
std::vector vec(10, 5); // 包含 10 个值为 5 的元素的 vector
std::vector vec(otherVec); // 复制构造函数
std::vector vec(otherVec.begin(), otherVec.end()); // 从迭代器范围构造
std::vector vec = {1, 2, 3, 4, 5}; // 列表初始化
vec = otherVec; // 赋值操作
容量操作
size_t size() const; // 返回 vector 中的元素个数
bool empty() const; // 如果 vector 为空,则返回 true
size_t capacity() const; // 返回当前分配的存储空间大小
void resize(size_t newSize); // 改变 vector 的大小
void resize(size_t newSize, const value_type& val); // 改变 vector 的大小,并用 val
//填充新元素
void reserve(size_t newCapacity); // 为 vector 预留至少 newCapacity 个元素的存储空间
void shrink_to_fit(); // 减少 vector 的容量以适合其大小
访问元素
reference operator[](size_t pos); // 返回位于 pos 位置的元素的引用
const_reference operator[](size_t pos) const; // 返回位于 pos 位置的元素的常量引用
reference at(size_t pos); // 返回位于 pos 位置的元素的引用,越界时
//抛出异常
const_reference at(size_t pos) const; // 返回位于 pos 位置的元素的常量引用,越界
//时抛出异常
reference front(); // 返回 vector 的第一个元素的引用
const_reference front() const; // 返回 vector 的第一个元素的常量引用
reference back(); // 返回 vector 的最后一个元素的引用
const_reference back() const; // 返回 vector 的最后一个元素的常量引用
修改元素
void push_back(const value_type& val); // 在 vector 的末尾添加一个元素
void push_back(value_type&& val); // 在 vector 的末尾添加一个右值引用的元素
void pop_back(); // 删除 vector 的最后一个元素
iterator insert(const_iterator pos, const value_type& val); // 在 pos 位置插入一个元素
iterator insert(const_iterator pos, size_t count, const value_type& val); // 在pos 位置插入 //count 个元素
iterator insert(const_iterator pos, const_iterator first, const_iterator last);
// 在 pos 位置插入一个迭代器范围
iterator erase(const_iterator pos); // 删除 pos 位置的元素
iterator erase(const_iterator first, const_iterator last); // 删除迭代器范围[first, last) 内//的元素
void clear(); // 删除 vector 中的所有元素
void swap(vector& other); // 交换两个 vector 的内容
迭代器
iterator begin(); // 返回指向 vector 第一个元素的迭代器
const_iterator begin() const; // 返回指向 vector 第一个元素的常量迭代器
iterator end(); // 返回指向 vector 末尾之后位置的迭代器
const_iterator end() const; // 返回指向 vector 末尾之后位置的常量迭代器
reverse_iterator rbegin(); // 返回指向 vector 最后一个元素的反向迭代器
const_reverse_iterator rbegin() const; // 返回指向 vector 最后一个元素的常量反向迭代器
reverse_iterator rend(); // 返回指向 vector 开始之前位置的反向迭代器
const_reverse_iterator rend() const; // 返回指向 vector 开始之前位置的常量反向迭代器
const_iterator cbegin() const; // 返回指向 vector 第一个元素的常量迭代器
const_iterator cend() const; // 返回指向 vector 末尾之后位置的常量迭代器
const_reverse_iterator crbegin() const; // 返回指向 vector 最后一个元素的常量反向迭代器
const_reverse_iterator crend() const; // 返回指向 vector 开始之前位置的常量反向迭代器
随机访问迭代器: std::vector 的迭代器是随机访问迭代器,这意味着你可以使用迭代器进 行算术运算(如 ++it , --it , it + n , it - n , it[n] 等),以及比较操作(如 it1 == it2 )。
迭代器失效:虽然 std::vector 的迭代器在大多数操作下是有效的,但在某些情况下,迭代 器可能会失效。特别是当 std::vector 进行容量调整(如 push_back 导致重新分配内存) 时,所有指向该 vector 的迭代器、引用和指针都可能失效。这是因为重新分配内存可能导致 元素被移动到新的内存位置。
为了避免迭代器失效,可以考虑以下几种策略:
在进行可能导致迭代器失效的操作之前,先使用迭代器完成所需的操作。
使用 std::vector 的成员函数(如 end() )来获取新的结束迭代器,以确保迭代器的有效性。
使用 std::vector 的 reserve 成员函数预先分配足够的容量,以避免在添加元素时发生重新分配。
在尾部插入元素(底层原理)
如果当前容量( capacity )足够,则直接在尾部添加新元素。
如果当前容量不足,则分配一个新的、更大的内存块,将原有元素复制到新块中,然后在新块的尾部添 加新元素,并释放旧块。
在指定位置插入元素(底层原理)
如果当前容量足够,则将指定位置及之后的元素向后移动一个位置,以腾出空间,然后在指定位置插入 新元素。
如果当前容量不足,则同样需要进行内存重新分配和元素复制。
删除尾部元素(底层原理)
直接移除尾部的元素,不需要移动其他元素。
删除指定位置的元素(底层原理)
将指定位置之后的元素向前移动一个位置,以覆盖被删除的元素。
更改大小(底层原理)
如果新的大小小于当前大小,则删除尾部多余的元素。 如果新的大小大于当前大小,则在尾部添加默认构造的元素或给定值的元素。
如果当前容量不足,则需 要进行内存重新分配和可能的元素复制。
#include
#include
int main()
{
// 创建一个空的 std::vector
std::vector myVector;
// 使用 push_back() 添加元素
myVector.push_back(1);
myVector.push_back(2);
myVector.push_back(3);
// 使用 size() 获取 std::vector 的大小
std::cout << "Size of myVector: " << myVector.size() << std::endl;
//capacity():返回vector在不进行内存重新分配的情况下可以容纳的元素数量。
std::cout << "capacity: " << myVector.capacity() << std::endl;
// 使用 at() 访问元素(会进行边界检查)
std::cout << "Element at index 1: " << myVector.at(1) << std::endl;
// 使用 [] 访问元素(不会进行边界检查)
std::cout << "Element at index 2: " << myVector[2] << std::endl;
// 使用 front() 和 back() 访问第一个和最后一个元素
std::cout << "First element: " << myVector.front() << std::endl;
std::cout << "Last element: " << myVector.back() << std::endl;
// 使用 insert() 在指定位置插入元素
myVector.insert(myVector.begin() + 1, 100); // 在索引 1 的位置插入 100
// 使用 erase() 删除指定位置的元素
myVector.erase(myVector.begin() + 2); // 删除索引 2 的元素
// 使用 pop_back() 删除最后一个元素
myVector.pop_back();
// 使用迭代器遍历 std::vector 并打印元素
//这是一个基于范围的 for 循环(range - based for loop),它是 C++11 引入的一种新的循
//环结构,用于简化对容器(如 std::vector、std::array、std::list 等)或数组的遍历。
//const auto & elem:这是循环变量的声明。
//const 表示 elem 是一个常量引用,即我们不能通过这个引用修改容器中的元素。
//auto 表示自动类型推导,编译器会自动根据 myVector 容器中的元素类型推断 elem 的类型。在
//这个例子中,elem 的类型将是 int。
//& 表示 elem 是一个引用,它引用容器中的元素,而不是元素的一个副本。这通常用于提高效率,因
为引用避免了不必要的复制操作。
//: myVector:这是基于范围的 for 循环的关键部分,它指定了循环变量 elem 应该遍历的容器,
//这里是 myVector。for (const auto& elem : myVector)
for (const auto& elem : myVector)
{
std::cout << elem << " ";
}
for (std::vector::iterator it = myVector.begin(); it !=
myVector.end();it++)
{
std::cout << *it << " ";
}
std::cout << std::endl;
std::cout << std::endl;
// 使用 resize() 改变 std::vector 的大小
myVector.resize(5); // 将大小调整为 5,新元素将使用默认值(对于 int 类型是 0)
std::cout << "Size of myVector: " << myVector.size() << std::endl;
// 使用 clear() 移除所有元素
myVector.clear();
// 检查 std::vector 是否为空
if (myVector.empty())
{
std::cout << "myVector is empty" << std::endl;
}
return 0;
}
可以看到vector相对array的插入和删除已经非常好用了,他在尾部插入和删除元素非常方便,直接 push_back() 或者 pop_back() 即可,但是在其他位置需要借助迭代器来实现,而且其不能在头部直接 插入元素。那么如何解决这个问题呢?
std::deque(双端队列)是C++标准库中的一个容器,它允许在序列的两端(即头部和尾部)进行元素的快 速插入和删除操作。
std::deque通常使用多个固定大小的块来存储元素,这些块通过指针或迭代器链接在一起,从而实现了高效 的头部和尾部操作。 头文件:#include
以下是 std::deque 的一些主要特点:
快速的头部和尾部操作:这是因为 std::deque 的内部实现允许它在不移动其他元素的情况下,在序列 的两端添加或删除元素。
中间操作的效率:虽然 std::deque 在中间位置插入或删除元素的操作不如在头部或尾部高效,但它的 性能通常优于 std::vector 。这是因为 std::deque 不需要像 std::vector 那样重新分配整个内存块 并移动所有元素。
内存使用: std::deque 的内存使用可能不如 std::vector 紧凑,因为它使用多个块来存储元素,并且 这些块之间需要额外的空间来存储指针或迭代器。然而,这种内存使用方式使得 std::deque 在头部和 尾部操作时更加高效。
随机访问:与 std::vector 一样, std::deque 也支持随机访问迭代器,这使得可以快速地访问序列中 的任意元素。
构造函数和赋值
std::deque d; // 默认构造函数
std::deque d(n); // 创建一个包含n个默认初始化元素的deque
std::deque d(n, val); // 创建一个包含n个值为val的元素的deque
std::deque d(begin, end); // 使用迭代器范围初始化deque
std::deque d(const std::deque& other); // 复制构造函数
std::deque d(std::deque&& other); // 移动构造函数
d1 = d2;
容量操作
size_t size() const; // 返回deque中的元素个数
bool empty() const; // 如果deque为空,则返回true
size_t max_size() const; // 返回deque可能容纳的最大元素数
void resize(size_t n); // 改变deque的大小为n,可能需要插入或删除元素
void resize(size_t n, const value_type& val); // 改变deque的大小为n,用val填充新元素
访问元素
reference at(size_t pos); // 返回位于pos位置的元素的引用
const_reference at(size_t pos) const; // 返回位于pos位置的元素的常量引用
reference operator[](size_t pos); // 返回位于pos位置的元素的引用
const_reference operator[](size_t pos) const; // 返回位于pos位置的元素的常量引用
reference front(); // 返回deque中第一个元素的引用
const_reference front() const; // 返回deque中第一个元素的常量引用
reference back(); // 返回deque中最后一个元素的引用
const_reference back() const; // 返回deque中最后一个元素的常量引用
修改元素
void push_front(const value_type& val); // 在deque的开始处插入一个元素
void push_front(value_type&& val); // 在deque的开始处插入一个右值引用的元素
void push_back(const value_type& val); // 在deque的末尾插入一个元素
void push_back(value_type&& val); // 在deque的末尾插入一个右值引用的元素
void pop_front(); // 删除deque的第一个元素
void pop_back(); // 删除deque的最后一个元素
iterator insert(const_iterator pos, const value_type& val); // 在pos位置插入一个元素
iterator insert(const_iterator pos, value_type&& val); // 在pos位置插入一个右值引用的元素
iterator insert(const_iterator pos, size_t n, const value_type& val); // 在pos位置插入n个元//素
iterator insert(const_iterator pos, const_iterator first, const_iterator last);
// 在pos位置插入一个迭代器范围的元素
iterator erase(const_iterator pos); // 删除pos位置的元素
iterator erase(const_iterator first, const_iterator last); // 删除一个迭代器范围的元素
迭代器操作
iterator begin(); // 返回指向deque第一个元素的迭代器
const_iterator begin() const; // 返回指向deque第一个元素的常量迭代器
iterator end(); // 返回指向deque末尾之后位置的迭代器
const_iterator end() const; // 返回指向deque末尾之后位置的常量迭代器
reverse_iterator rbegin(); // 返回指向deque最后一个元素的反向迭代器
const_reverse_iterator rbegin() const; // 返回指向deque最后一个元素的常量反向迭代器
reverse_iterator rend(); // 返回指向deque开始之前位置的反向迭代器
const_reverse_iterator rend() const; // 返回指向deque开始之前位置的常量反向迭代器
双向迭代器: std::deque 的迭代器是双向迭代器,这意味着你可以使用 ++it 和 --it 来前进 或后退迭代器,但不可以进行随机访问(如 it + n 或 it[n] )。尽管如此,对于大多数常见 的遍历操作来说,双向迭代器已经足够了。
迭代器稳定性:与 std::vector 不同, std::deque 在修改容器(如插入或删除元素)时不 会使迭代器失效,除非迭代器指向被删除的元素本身。这是因为 std::deque 的设计允许在容 器的开始和结束处高效地添加或删除元素,而不会影响已存在元素的内存位置。
因此,在 std::deque 中,你可以安全地在迭代器指向的元素之前或之后插入或删除元素,而不会影响 迭代器的有效性。但是,如果你删除了迭代器当前指向的元素,那么该迭代器将变为无效。
#include
#include
int main()
{
// 创建一个空的deque
std::deque myDeque;
// 向deque的开始位置插入元素
myDeque.push_front(1);
myDeque.push_front(2);
// 向deque的末尾插入元素
myDeque.push_back(3);
myDeque.push_back(4);
// 输出deque的大小
std::cout << "Size of deque: " << myDeque.size() << std::endl;
// 访问并输出deque的第一个和最后一个元素
std::cout << "First element: " << myDeque.front() << std::endl;
std::cout << "Last element: " << myDeque.back() << std::endl;
// 使用迭代器访问和输出deque中的所有元素
for (std::deque::iterator it = myDeque.begin(); it != myDeque.end();++it)
{
std::cout << *it << " ";
}
std::cout << std::endl;
// 在指定位置插入元素
myDeque.insert(myDeque.begin() + 2, 5); // 在第三个位置插入5
// 删除指定位置的元素
myDeque.erase(myDeque.begin()); // 删除第一个元素
// 使用范围for循环输出deque中的所有元素
for (const auto& elem : myDeque) {std::cout << elem << " ";}
std::cout << std::endl;
// 修改deque中的元素
myDeque[1] = 6; // 修改第二个元素为6
// 再次输出修改后的deque
for (const auto& elem : myDeque)
{
std::cout << elem << " ";
}
std::cout << std::endl;
// 清空deque
myDeque.clear();
// 检查deque是否为空
if (myDeque.empty())
{
std::cout << "Deque is empty." << std::endl;
}
return 0;
}
底层原理:
分段存储: std::deque 内部包含多个固定大小的块(或称为“节点”),每个块通常包含一定数量 的元素和一个指向下一个块的指针。块的大小通常是预先设定的,并且对于给定的 std::deque 实 例是固定的。
头部和尾部指针: std::deque 维护了两个迭代器,一个指向序列的开始(头部),另一个指向序 列的结束(尾部)。这两个迭代器实际上是指向各自块的开始和结束位置的迭代器。
插入和删除操作:
在头部插入或删除:当在头部插入或删除元素时, std::deque 会检查头部块是否有足 够的空间。如果有,则直接在头部块进行操作;如果没有,则可能需要分配一个新的块 并将其添加到头部,或者如果头部有多个块且第一个块为空,则可能会释放它。
在尾部插入或删除:与头部操作类似,但在尾部进行。 std::deque 会检查尾部块是否 有足够的空间,并根据需要分配或释放块。
在中间插入:
定位插入点:首先,通过迭代器或索引找到要插入元素的具体位置。
检查空间:检查包含插入点的块是否有足够的空间来容纳新元素。如果有足够的空 间,直接在块内插入元素,并可能需要移动该位置之后的所有元素以腾出空间。
分配新块:如果目标块没有足够的空间, std::deque 会分配一个新的块来存储新 元素。新块可能插入到现有块的前面或后面,取决于插入点的位置。
移动元素:为了使新元素处于正确的位置,可能需要将插入点之后的所有元素向后 移动一个或多个块。这可能涉及复制或移动元素到新的位置。
更新迭代器:最后,更新任何指向已移动元素的迭代器,以确保它们仍然有效。
在中间删除:
定位删除点:通过迭代器找到要删除元素的位置。
删除元素:从包含删除点的块中移除元素。如果删除后块变为空,可能需要将其从 std::deque 中移除。
合并块:如果删除操作导致相邻的块都变得非常空(例如,只包含少量元素), std::deque 可能会尝试合并这些块以优化内存使用。然而,由于块的大小通常是 固定的,直接合并可能并不总是可行的。
更新迭代器:更新任何指向已删除元素或其后元素的迭代器。
内存分配:由于 std::deque 是由多个块组成的,因此它的内存分配策略通常涉及动态内存分配。 当需要更多空间时,会分配新的块;当空间不再需要时,会释放块。
std::list 是 C++ 标准模板库(STL)中的一个容器,它表示一个双向链表。
与 std::vector 和 std::deque 不同,std::list中的元素不是存储在连续的内存空间中,而是通过指 针或迭代器链接在一起。这种结构使得 std::list 在某些操作上具有不同的性能特点。
头文件:#include
底层原理
std::list 的底层实现通常包括一个节点(node)结构,每个节点包含两个指针(或迭代器):一个 指向下一个节点,另一个指向前一个节点。此外,每个节点还包含一个数据元素。这使得 std::list 可以在任何位置进行插入和删除操作,因为只需要更新相邻节点的指针即可。
主要特性
双向迭代: std::list 支持双向迭代,意味着你可以向前或向后遍历链表。
插入和删除:在链表的任何位置插入或删除元素通常只需要更新相邻节点的指针。
非连续内存: std::list 的元素不是存储在连续的内存中,这避免了在插入或删除元素时可能需要的大 量内存移动。
空间开销:由于每个元素都需要额外的空间来存储指针,因此 std::list 在空间使用上可能不如 std::vector 或 std::deque 高效,特别是当存储小对象时。
迭代器稳定性:与 std::vector 不同, std::list 的插入和删除操作不会使指向序列中其他元素的迭 代器或引用失效,除非它们指向被删除的元素本身。
构造函数
std::list List(); 创建一个空的链表。
std::list List(size_t n, const T& value): 创建一个包含 n 个值为 value 的元素的链表
std::list List(const list& other): 通过拷贝另一个链表来构造新的链表。
std::list List(list&& other) noexcept: 通过移动另一个链表的资源来构造新的链表(移动构造)。
List1 = List2;
容量操作
empty(): 检查链表是否为空。
size(): 返回链表中元素的数量。
max_size(): 返回链表可能包含的最大元素数量。
修改操作
push_back(const T& value): 在链表尾部添加一个元素。
push_front(const T& value): 在链表头部添加一个元素。
pop_back(): 删除链表尾部的元素。
pop_front(): 删除链表头部的元素。
insert(const_iterator position, const T& value): 在指定位置 position 前插入一个元素。
insert(const_iterator position, size_t n, const T& value): 在指定位置 position 前插
//入 n 个值为 value 的元素。
insert(const_iterator position, InputIt first, InputIt last): 在指定位置 position
//前插入一个元素范围 [first, last)。
erase(const_iterator position): 删除指定位置 position 的元素。
erase(const_iterator first, const_iterator last): 删除元素范围 [first, last) 内的所有元素。
clear(): 删除链表中的所有元素。
splice(const_iterator position, list& x): 将链表 x 的所有元素移动到当前链表的 position位置。
splice(const_iterator position, list&& x) noexcept: 移动构造函数版本的 splice。
splice(const_iterator position, list& x, const_iterator i): 将链表 x 中迭代器 i 指向
//的元素移动到当前链表的 position 位置。
splice(const_iterator position, list& x, const_iterator first, const_iterator
last): 将链表 x 中迭代器范围 [first, last) 内的元素移动到当前链表的 position 位置。
remove(const T& value): 删除链表中所有等于 value 的元素。
remove_if(Predicate pred): 删除链表中所有满足谓词 pred 的元素。
unique(): 删除链表中所有连续重复的元素。
unique(BinaryPredicate pred): 删除链表中所有满足谓词 pred 的连续重复元素。
merge(list& x): 合并两个已排序的链表。
sort(): 对链表中的元素进行排序。
reverse(): 反转链表中元素的顺序。
访问元素
front(): 返回链表中第一个元素的引用。
back(): 返回链表中最后一个元素的引用。
迭代器
begin(): 返回指向链表第一个元素的迭代器。
end(): 返回指向链表末尾之后位置的迭代器。
cbegin(): 返回指向链表第一个元素的常量迭代器。
cend(): 返回指向链表末尾之后位置的常量迭代器。
rbegin(): 返回指向链表最后一个元素的反向迭代器。
rend(): 返回指向链表开头之前位置的反向迭代器。
crbegin(): 返回指向链表最后一个元素的常量反向迭代器。
crend(): 返回指向链表开头之前位置的常量反向迭代器
双向迭代器: std::list 的迭代器是双向迭代器,这意味着你可以使用 ++it 和 --it 来在链 表中前进或后退。但是,它们不支持随机访问,如 it + n 或 it[n] 这样的操作。
迭代器稳定性:与 std::vector 不同, std::list 在修改容器(如插入或删除元素)时不会 使除了被删除元素本身的迭代器以外的迭代器失效。这是因为 std::list 是通过链接节点来 管理其元素的,插入和删除操作只影响被操作节点及其相邻节点的链接,而不会影响其他节点 的内存位置或链接关系。
因此,在 std::list 中,你可以安全地在迭代器指向的元素之前或之后插入或删除元素,而不会影响其 他迭代器的有效性。但是,如果你删除了迭代器当前指向的元素,那么该迭代器将变为无效。
#include
#include
int main()
{
// 创建一个空的 int 类型列表
std::list myList;
// 在列表末尾插入元素
myList.push_back(10);
myList.push_back(20);
myList.push_back(30);
// 在列表开头插入元素
myList.push_front(5);
// 输出列表元素
std::cout << "List elements: ";
for (const auto& elem : myList)
{
std::cout << elem << " ";
}
std::cout << std::endl;
// 在指定位置插入元素
auto it = myList.begin();
myList.insert(it, 15);
// 输出修改后的列表元素
std::cout << "List after insertion: ";
for (const auto& elem : myList)
{
std::cout << elem << " ";
}
std::cout << std::endl;
// 移除列表末尾的元素
if (!myList.empty())
{
myList.pop_back();
}
// 移除列表开头的元素
if (!myList.empty())
{
myList.pop_front();
}
// 移除所有值为 10 的元素
myList.remove(10);
// 输出移除元素后的列表
std::cout << "List after removal: ";
for (const auto& elem : myList)
{
std::cout << elem << " ";
}
std::cout << std::endl;
// 获取列表大小
std::cout << "Size of the list: " << myList.size() << std::endl;
// 判断列表是否为空
if (myList.empty())
{
std::cout << "The list is empty." << std::endl;
}
else
{
std::cout << "The list is not empty." << std::endl;
}
// 反转列表
myList.reverse();
std::cout << "Reversed list: ";
for (const auto& elem : myList)
{
std::cout << elem << " ";
}
std::cout << std::endl;
// 排序列表(默认升序)
myList.sort();
std::cout << "Sorted list: ";
for (const auto& elem : myList)
{
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
插入操作(底层原理):
分配新节点:首先,为新元素分配内存空间,并创建一个新节点。这个新节点将包含要插入的 元素以及两个指针,一个指向下一个节点,另一个指向前一个节点。
更新指针:接下来,根据插入的位置更新节点的指针。
如果是在链表头部插入,新节点的下一个指针将指向当前的第一个节点,而当前第一个节点的前一个指 针将指向新节点。同时,更新链表的头指针,使其指向新节点。
如果是在链表尾部插入,新节点的前一个指针将指向当前的最后一个节点,而当前最后一个节点的下一 个指针将指向新节点。同时,更新链表的尾指针,使其指向新节点。
如果是在链表中间插入,需要找到插入位置的前一个节点和后一个节点。然后,更新这些节点的指针, 使新节点正确地插入到链表中。新节点的前一个指针将指向插入位置的前一个节点,后一个指针将指向 插入位置的后一个节点。同时,更新前一个节点和后一个节点的指针,以包含新节点。
维护链表完整性:确保链表的头尾指针以及其他节点的指针都正确指向了相应的节点,以保持 链表的完整性。
删除操作(底层原理):
找到要删除的节点:首先,遍历链表找到要删除的节点。这通常通过从头节点开始,并沿着下 一个指针遍历链表直到找到目标节点。
更新指针:一旦找到要删除的节点,就更新其前一个节点和后一个节点的指针,以跳过该节 点。具体来说,前一个节点的下一个指针将指向要删除节点的后一个节点,而后一个节点的前 一个指针将指向要删除节点的前一个节点。
释放内存:删除节点后,释放该节点所占用的内存空间。
维护链表完整性:确保链表的头尾指针以及其他节点的指针都正确地指向了删除节点前后的节 点,以保持链表的完整性
关联容器是一种支持高效的关键字查找和访问的数据结构,用于存储和管理具有键值对的数据元素。关 联容器中的元素按照键值进行排序,这使得它们在数据检索时比序列式容器效率更高。此外,关联容器 中的键值是唯一的,这保证了每个元素都能被唯一标识。
键(Key)
键是用于唯一标识关联容器中元素的一个值。元素本身就是键,键与值相关联,用于查找和访问对应的 值。键的类型通常是可比较的,以便关联容器能够根据键的值对元素进行排序和查找。
键值对(Key-Value Pair)
键值对是一个包含键和与键相关联的值的数据结构。在关联中,每个元素都是一个键值对。键用于唯一 标识或分类值,而值是与该键相关联的数据。通过键,可以快速查找和访问与之关联的值。
关联容器通常使用平衡查找树(如红黑树)或哈希表来实现高效的查找操作,能在对数时间或常数时间 内完成查找。
以红黑树为例,它是一种平衡二叉树,其特点包括所有左子树结点的值小于等于根节点的值,右子树节 点的值大于根节点的值。
使用关联容器的主要原因:
快速查找:关联容器内部通常使用平衡树(如红黑树)来实现,这意味着它们可以在对数时间内完成查 找、插入和删除操作。这在处理大量数据时非常高效,尤其是当你需要频繁地根据键来查找值时
唯一性保证: std::map 和 std::set 保证它们存储的键是唯一的。如果你尝试插入具有相同键的元素, 这些容器会忽略或替换现有的元素。这在需要确保数据唯一性的场景中非常有用,例如存储用户的唯一 标识符或跟踪不重复的任务。
有序存储:关联容器中的元素默认按照键的升序存储。这使得关联容器不仅可以用作查找表,还可以用 作有序集合。你可以轻松地遍历容器中的元素,按照键的顺序进行访问。
在C++中, std::map 是一个关联容器,它存储的元素都是键值对(key-value pairs),并且根据键的 值自动排序。 std::map 内部通常使用红黑树来实现。头文件: #include
键值唯一性: std::map 中的每个元素都由一个唯一的键(key)和一个与之关联的值(value)组 成。键在 std::map 中是唯一的,即不会有两个或更多的元素拥有相同的键。如果尝试插入一个 已经存在的键,则新的值会覆盖旧的值(如果使用 operator[] 插入),或者插入操作会失败 (如果使用 insert 方法且键已存在)。
自动排序: std::map 内部的元素默认按照键的升序排列。这是因为 std::map 通常基于平衡搜 索树(如红黑树)实现,这种数据结构可以保持元素的排序状态,从而支持高效的查找、插入和删 除操作。如果需要自定义排序规则,可以通过向 std::map 传递一个比较函数或对象来实现。
高效查找:由于 std::map 的内部实现保证了元素的排序性,因此可以实现高效的查找操作。
动态大小: std::map 是一个动态容器,可以在运行时根据需要增长或缩小。当插入新元素时,如 果容器的大小不足以容纳新元素,它会自动重新分配内存以容纳更多的元素。同样,当删除元素 时,如果容器的大小超过了所需的容量,它可能会释放一些内存以节省空间。
提供迭代器: std::map 提供了双向迭代器,使得可以遍历容器中的元素。迭代器允许按顺序访问 容器中的元素,并且提供了对元素的读取和修改能力(通过迭代器访问的元素值可以被修改,但键 是固定的)。
键值类型多样性: std::map 的键和值可以是任何支持比较操作的类型。这意味着你可以使用自定 义类型作为键,只需为该类型定义比较操作符或提供比较函数对象即可。
构造函数和赋值
std::map myMap;//默认构造函数:创建一个空的 map。
//初始化列表构造函数(C++11 起):使用初始化列表直接初始化 map。
std::map myMap1 = {{key, value}, {key, value}};
//拷贝构造函数:从一个已有的 map 创建另一个 map,作为已有 map 的副本
std::map myMap2(myMap1); // myMap2 是 myMap1 的副本
//赋值运算符:将一个 map 的内容赋给另一个 map。
myMap2 = myMap1;
容量操作
size();//获取map容器的大小,即键值对的数量。
empty();//检查map容器是否为空。如果map为空(即不包含任何键值对),则返回true,否则返回false
访问元素
/*可以使用下标运算符[],通过键来访问或修改对应的值。如果键已经存在于map中,它会返回对应的值;如果键不存在,它会插入这个键,并为其关联一个默认构造的值(对于基本数据类型,通常是0或空字符串)。*/
at(key);//at成员函数类似于下标运算符,但它在键不存在时会抛出一个std::out_of_range异常,而不
//是插入新元素。因此,使用at可以更安全地访问已知存在的键
find(key);//在map中查找指定键的元素。如果找到了该键,它返回一个指向该元素的迭代器;如果没找
//到,它返回end()迭代器。
插入和删除元素
std::pair 是一个模板类,用于将两个可能不同类型的数据组合成一个单一的对 象。这种配对经常用于表示键值对(key-value pair)
std::pair myPair(key, value);
std::make_pair() 是一个函数模板,它接受两个参数,并返回一个包含这两个参数的 std::pair 对象。这个函数模板用于简化 std::pair 对象的创建过程,特别是当你不想显式 指定 std::pair 的类型时。编译器会根据传递给 std::make_pair 的参数类型来推断 std::pair 的类型。
std::pair myPair = std::make_pair(key, value);
std::mapmyMap;
// 使用insert成员函数和std::pair
myMap.insert(std::pair(key, value));
// 使用insert成员函数和make_pair辅助函数
myMap.insert(std::make_pair(key, value));
// 使用insert成员函数和值初始化
myMap.insert({key, value});
// 使用insert成员函数和迭代器范围的插入
std::map anotherMap = {{key1, value1}, {key2, value2}};
myMap.insert(anotherMap.begin(), anotherMap.end());
// 使用迭代器删除元素
erase(迭代器);
// 使用键删除元素
size_t count = myMap.erase(key); // 如果键存在,返回1;否则返回0
// 删除一个范围内的元素
erase(myMap.begin(), myMap.end());
erase 成员函数返回一个指向被删除元素之后位置的迭代器。如果传递的是一个键,并且该键不存在于 map 中,那么 erase 不会做任何事情,并返回 end() 迭代器。
在使用 erase 时,要注意不要使用已经被删除元素的迭代器,因为它们会变得无效。
迭代器
std::map::iterator//正向迭代器
std::map::const_iterator//常量正向迭代器
std::map::reverse_iterator//反向迭代器
std::map::const_reverse_iterator//常量反向迭代器
使用迭代器访问键值对
// 通过迭代器访问键
it->first
// 通过迭代器访问值
it->second
双向迭代器: std::map 的迭代器是双向迭代器,这意味着你可以使用 ++it 和 --it 来在容器 中前进或后退。然而,它们不支持随机访问操作,如 it + n 或 it[n] 。
迭代器稳定性:在 std::map 中,插入或删除元素通常不会影响指向其他元素的迭代器的有效 性。这是因为平衡搜索树的结构允许在不影响其他元素的情况下插入或删除节点。然而,如果 你删除了迭代器当前指向的元素,那么该迭代器将变为无效。
重要的是要注意,虽然删除操作不会使指向其他元素的迭代器失效,但它可能会改变元素的顺 序(特别是在 std::map 和 std::set 中,因为它们是有序的)。因此,如果你依赖于元素的 特定顺序,那么在插入或删除元素后,你可能需要重新获取迭代器或更新现有的迭代器。
#include
#include
在C++中, std::set 是一个关联容器,它包含唯一元素的集合。元素在 set 中自动按键的升序进行排 序。 std::set 也是一个基于红黑树的有序关联容器,但它只存储唯一的键,没有与键相关联的值。 (key即value)
唯一性:
std::set 中的元素是唯一的,即集合中不会存在重复的元素。当你尝试插入一个已经存在于集合中的 元素时,该操作会被忽略。因此插入重复的元素将不会增加集合的大小。
自动排序:
std::set 中的元素默认按照升序排列。这是通过比较元素的键来实现的,通常使用 < 运算符。如果你 需要降序排列或其他自定义排序方式,可以通过提供自定义的比较函数或对象来实现。
基于红黑树实现:
std::set 内部使用红黑树数据结构来存储元素。红黑树是一种自平衡的二叉搜索树,它能够在对数时 间内完成插入、删除和查找操作。
快速查找:
由于 std::set 是基于红黑树实现的,所以它能够在对数时间内完成元素的查找操作。这使得 std::set 非常适合用于需要快速检查元素是否存在的场景。
不允许修改元素:
一旦一个元素被插入到 std::set 中,你不能直接修改该元素的值(因为这会破坏集合的唯一性和排序 性质)。如果你需要修改一个元素的值,你必须先删除旧元素,然后插入新元素。
迭代器稳定性:
由于 std::set 是基于红黑树实现的,插入和删除操作可能会导致树的重新平衡。然而, std::set 的迭 代器(指向元素的指针或引用)在插入和删除操作中通常是稳定的,这意味着它们不会因为元素的插入 或删除而失效,除非迭代器本身被删除。
高效的内存使用:
std::set 使用红黑树结构来存储元素,这通常意味着它的内存使用是相对高效的,因为它不需要额外 的空间来存储元素之间的关联信息。
构造函数和赋值
std::set mySet;// 创建一个空的int类型集合
std::set mySet1(mySet); // 创建一个集合,它是另一个集合的副本。
//范围构造函数:创建一个集合,其元素来自迭代器指定的范围。
std::vector vec = {value1, value2, value3};
std::set mySet(vec.begin(), vec.end()); // 创建包含vec中所有元素的集合
std::set mySet = {value1, value2, value3}; // 使用初始化列表创建集合
mySet = mySet1;// 赋值
容量操作
size()://返回集合中元素的数量。
empty()://检查集合是否为空。如果集合中没有元素,则返回true;否则返回false。
max_size()://返回集合可能包含的最大元素数量。这通常是一个非常大的数,代表理论上集合可以容纳的
//元素上限。
clear()://移除集合中的所有元素,使集合变为空。
访问元素
不支持下标访问[]
find(key); //获取指向该元素的迭代器,如果元素不存在于集合中,find将返回end()迭代器。
lower_bound(key)和upper_bound(key)//查找集合中不小于(或大于)给定键的第一个元素的迭代器。
修改元素
insert(key);// 插入单个元素
insert(iter1, iter2); // 插入范围元素,参数是迭代器
erase(key);// 通过键值删除单个元素
erase(iter);// 通过迭代器删除单个元素
迭代器
std::set::iterator//正向迭代器
std::set::const_iterator//常量正向迭代器
std::set::reverse_iterator//反向迭代器
std::set::const_reverse_iterator//常量反向迭代器
双向迭代器:
std::set 的迭代器是双向迭代器,这意味着你可以使用 ++it 和 --it 来在容器 中前进或后退。然而,它们不支持随机访问操作,如 it + n 或 it[n] 。
迭代器稳定性:
在 std::set 中,插入或删除元素通常不会影响指向其他元素的迭代器的有效 性。这是因为 std::set 使用红黑树(或其他平衡搜索树)作为其内部数据结构,这种结构允 许在不影响其他元素的情况下插入或删除节点。但是,如果你删除了迭代器当前指向的元素, 那么该迭代器将变为无效
不可修改元素值:
由于 std::set 中的元素是唯一的,并且按照键的顺序排序,因此你不能通 过迭代器修改元素的值。如果你尝试这样做,编译器会报错。这是为了确保 std::set 中的有 序性和唯一性不被破坏。
#include
#include
int main()
{
// 创建一个空的 std::set
std::set mySet;
// 插入元素
mySet.insert(1);
mySet.insert(2);
mySet.insert(3);
mySet.insert(4);
mySet.insert(4);
// 输出集合中的所有元素
std::cout << "Elements in the set: ";
for (const auto& elem : mySet)
{
std::cout << elem << " ";
}
std::cout << std::endl;
// 查找元素
int elementToFind = 3;
auto it = mySet.find(elementToFind);
if (it != mySet.end())
{
std::cout << "Element " << elementToFind << " found in the set." <
元素类型:
std::set 只存储键,而 std::map 存储键值对。
重复元素:
std::set 不允许有重复的元素,每个元素都是唯一的;而 std::map 的键是唯一的,但不同 的键可以对应相同的值。
用途:
std::set 通常用于需要快速查找唯一元素的场景,如检查某个值是否存在于集合中;而 std::map 则用于存储键值对的数据结构。
性能:
由于 std::set 和 std::map 底层都是基于红黑树实现的,然而,因为 std::map 存储的是键值 对,所以在空间占用上可能会稍大一些
以上内容均为作者的个人见解,如有错误,还请读者斧正