1. 数组(Array)
数组是一种基础的数据结构,用来存储固定大小的同类型元素的序列。在内存中,数组中的元素被分配在连续的内存空间上,每个元素都可以通过数组索引来访问,索引通常从 0 开始。 当您需要快速访问固定数量的数据项时,数组是一个很好的选择。例如,在数学运算、数据缓冲区、查找表等场景中常用。
- 固定大小:创建时需要指定数组的大小,之后无法更改。
- 随机访问:可以直接通过索引快速访问任何元素,时间复杂度为 O(1)。
- 高效的内存利用:由于连续的内存分配,数组在内存利用和访问速度方面非常高效。
- 插入/删除操作耗时:在数组中间插入或删除元素通常需要移动其他元素,因此这些操作的时间复杂度较高。
- 数组的限制:数组的大小在声明时必须确定,且不能改变,数组不提供检查越界的机制,访问无效索引可能导致未定义行为。
int arr[10]; // 声明一个整型数组,包含10个整数
// 初始化数组:在声明时赋初值
int arr[5] = {1, 2, 3, 4, 5}; // 初始化数组,包含5个整数
// 如果初始化时未填满,剩余的元素会被自动初始化为0
int arr[5] = {1, 2}; // 后三个元素自动初始化为0
// 可以省略数组长度,让编译器自动计算
int arr[] = {1, 2, 3, 4, 5}; // 编译器计算数组长度为5
// 访问数组元素
int value = arr[2]; // 访问第三个元素
arr[4] = 10; // 修改第五个元素的值
// 数组遍历
for(int i = 0; i < 5; ++i) {
std::cout << arr[i] << std::endl;
}
C++ 标准库中的数组
// C++11 引入了 std::array,提供了固定大小的数组
#include <array>
std::array<int, 5> arr = {1, 2, 3, 4, 5};
// std::array 提供了 .size() 方法获取大小,以及 .begin() 和 .end() 方法用于范围遍历:
for(auto it = arr.begin(); it != arr.end(); ++it) {
std::cout << *it << std::endl;
}
2. 向量(Vector)
向量是数组的一个变体,通常在编程语言的标准库中实现。与数组不同,向量可以动态地调整其大小。当需要一个大小可变的数组时,向量是一个很好的选择。在不确定数据项数量的情况下收集数据、实现动态数据集合等场景中常用。
- 动态大小:向量可以在运行时根据需要扩展或缩减其大小。
- 随机访问:与数组一样,向量支持通过索引的快速随机访问。
- 自动管理内存:向量在内部管理内存,自动扩展和缩减存储空间。
- 可能的重新分配开销:当向量扩展到超过当前分配的内存时,它可能需要重新分配整个内存块来存储元素。
在 C++ 中,std::vector 是一个封装了动态大小数组的序列容器。它可以存储任何类型的元素,并且可以在运行时动态地更改其大小。std::vector 提供了灵活的动态数组功能,包括动态扩容和方便的元素添加/删除操作,但由于动态扩容,向量的内存使用可能不如静态数组高效, 在频繁扩容的情况下,性能可能受到影响。
// 在使用 std::vector 之前,需要包含头文件 <vector>
#include <vector>
// 声明并初始化空向量, 声明一个 int 类型的向量并初始化为空向量
std::vector<int> vec;
// 使用初始化列表,初始化一个包含 5 个元素的向量
std::vector<int> vec = {1, 2, 3, 4, 5};
// 指定大小和初始值,包含10个初始化为0的元素
std::vector<int> vec(10, 0);
// 添加元素
vec.push_back(6); // 在向量末尾添加一个元素
// 访问元素
int value = vec[2]; // 访问第三个元素
int value = vec.at(2); // 访问第三个元素,带有边界检查
// 遍历向量
for(int num : vec) {
std::cout << num << std::endl;
}
// 使用 [] 运算符或 .at() 的方式修改元素
vec[2] = 100; // 修改第三个元素的值
// 获取向量大小
size_t size = vec.size(); // 获取向量中的元素数量
// 检查向量是否为空
bool isEmpty = vec.empty(); // 如果向量为空,返回 true
// 删除元素
vec.pop_back(); // 删除向量末尾的元素
// 删除指定位置的元素
vec.erase(myVector.begin() + 2); // 删除第三个元素
// 清空向量
vec.clear(); // 删除向量中的所有元素
3. 链表(LinkedList)
链表是一种由节点组成的数据结构,每个节点包含数据和指向下一个节点的指针,链表中的元素不必在内存中连续存储,这种情况下频繁插入和删除时,链表会更加高效,但是不支持随机访问,访问特定索引的元素需要从头开始遍历,效率较低。
- 动态大小:链表的大小可以根据需要动态变化。
- 高效的插入和删除:可以在任何位置快速地插入或删除节点,不需要移动其他元素。
- 无随机访问:访问链表中的元素需要从头开始遍历,时间复杂度为 O(n)。
- 额外的内存开销:每个节点需要额外的存储空间来存储指针。
C++ 标准库提供了两种链表类型:std::list:双向链表 和 std::forward_list:单向链表
// 头文件
#include <list> // 对于 std::list
// 声明和初始化链表
std::list<int> myList = {1, 2, 3, 4, 5}; // 双向链表
std::forward_list<int> myForwardList = {1, 2, 3, 4, 5}; // 单向链表
// 添加元素
myList.push_back(6); // 在链表末尾添加元素
myList.push_front(0); // 在链表头部添加元素
// 删除元素
myList.pop_back(); // 删除链表末尾元素
myList.pop_front(); // 删除链表头部元素
// 遍历元素
for (int num : myList) {
std::cout << num << std::endl;
}
std::forward_list 提供了单向链表的实现,适合于需要频繁在头部插入或删除元素的场景。由于其单向特性,它在内存使用上比 std::list 更高效,但也牺牲了一些灵活性,如无法直接访问前一个元素。
#include <forward_list>
std::forward_list<int> myList; // 空的单向链表
std::forward_list<int> myList = {1, 2, 3, 4, 5}; // 初始化列表
// 在链表头部添加元素
myFowardList.push_front(0);
// 删除头部元素
myFowardList.pop_front();
// 使用迭代器遍历元素
for (auto it = myFowardList.begin(); it != myFowardList.end(); ++it) {
std::cout << *it << std::endl;
}
// 使用迭代器删除元素
auto it = myFowardList.before_begin(); // 获取头部之前的位置
myFowardList.insert_after(it, 10); // 在头部插入10
myFowardList.erase_after(it); // 删除头部元素
4. 总结
4.1 各结构优缺点及使用场景
- 数组(Array)随机访问快,时间复杂度为 O(1),插入/删除操作慢,特别是在数组的中间,因为可能需要移动元素,内存静态分配,大小在编译时确定,无法改变。 适用场景:需要频繁访问元素,且元素数量固定不变的场景,在进行大量数学计算,需要高效访问连续内存的场景。
- 向量(Vector,如 std::vector)随机访问快,时间复杂度为 O(1),插入/删除操作慢,在末尾快,但在中间或开始较慢,内存动态分配,可以根据需要改变大小,但可能涉及复制整个数组到新的内存位置。适用场景:当需要动态数组,即数组大小可以改变的场景,需要随机访问,但也会有在末尾添加或删除元素的操作场景。
- 链表(LinkedList,如 std::list 和 std::forward_list) 随机访问慢,需要遍历链表,时间复杂度为 O(n),插入/删除操作快,特别是在已知节点的情况下,时间复杂度为 O(1),内存动态分配:每个新元素通常都会进行一次内存分配。适用场景: 当元素的插入和删除操作频繁,且不需要快速随机访问的场景;在实现某些数据结构如队列和栈时,链表提供了自然的优势。
4.2 各特点对比
- 内存使用:数组使用连续内存,而向量和链表使用非连续内存,链表为每个元素分配额外内存(存储指针)
- 访问速度:数组和向量提供快速随机访问,链表则不支持高效的随机访问。
- 插入/删除效率:链表在插入和删除操作上最为高效。数组在这方面最不高效,向量则在末尾操作时较高效。
- 灵活性:向量和链表在大小上更为灵活,数组在声明后大小固定。
欢迎来公众号交流,共同学习,共同进步,在这里有 C++、Golang、数据结构等面试考点,持续更新!