1.vector<T>
1.1 什么是 vector
vector 是 C++ 标准库中提供的一个动态数组容器,它可以自动管理内存,根据需要动态扩容。与普通数组相比,vector 具有以下优势:
-
动态大小:不需要预先指定大小,可以随时添加或删除元素
-
自动内存管理:不需要手动 new/delete,避免内存泄漏
-
丰富的成员函数:提供了大量便捷的操作方法
-
连续内存存储:与数组一样,可以通过下标随机访问
1.2 vector 的基本使用
头文件与命名空间
cpp
#include <vector> // 必须包含头文件
using namespace std; // 可以省略 std::
创建 vector 对象
cpp
// 方式1:创建空的 vector
vector<int> v1;
// 方式2:创建包含 n 个元素的 vector,默认初始化为0
vector<int> v2(10); // 10个int元素,值为0
// 方式3:创建包含 n 个指定值的 vector
vector<int> v3(5, 100); // 5个int元素,值都是100
// 方式4:拷贝构造
vector<int> v4(v3); // v4 是 v3 的副本
// 方式5:C++11 列表初始化
vector<int> v5 = {1, 2, 3, 4, 5};
常用成员函数
| 函数 | 功能说明 |
|---|---|
v.push_back(x) |
在尾部添加元素 x |
v.pop_back() |
删除尾部元素 |
v.size() |
返回元素个数 |
v.empty() |
判断是否为空 |
v.clear() |
清空所有元素 |
v.resize(n) |
改变大小为 n,新增元素默认初始化 |
v.resize(n, val) |
改变大小为 n,新增元素值为 val |
v.front() |
返回第一个元素的引用 |
v.back() |
返回最后一个元素的引用 |
v[i] |
下标访问(不检查越界) |
v.at(i) |
下标访问(越界抛异常) |
遍历 vector
cpp
vector<int> v = {1, 2, 3, 4, 5};
// 方式1:下标遍历
for(int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
// 方式2:迭代器遍历
for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
// 方式3:范围for(C++11)
for(auto x : v) {
cout << x << " ";
}
1.3 vector 的内存扩容机制
**重要概念:**vector 有两个重要的大小概念:
-
size:实际存储的元素个数
-
capacity:当前容量(能容纳的最大元素数)
当 size == capacity 时继续添加元素,vector 会自动扩容:通常扩容为原来的 1.5 倍或 2 倍(取决于编译器实现)。
cpp
vector<int> v;
cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl;
for(int i = 0; i < 10; i++) {
v.push_back(i);
cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl;
}
1.4 二维 vector
cpp
// 创建 3 行 4 列的二维 vector,初始值为0
vector<vector<int>> v(3, vector<int>(4, 0));
// 访问元素
v[0][0] = 100;
// 创建 vector 数组(N个vector)
const int N = 105;
vector<int> a[N]; // 每个 a[i] 都是独立的 vector
// 常用于图的邻接表、链式存储等
1.5 vector 常用算法
cpp
#include <algorithm>
vector<int> v = {3, 1, 4, 1, 5, 9};
// 排序
sort(v.begin(), v.end()); // 升序排序
// 翻转
reverse(v.begin(), v.end());
// 查找
auto it = find(v.begin(), v.end(), 5);
if(it != v.end()) {
cout << "找到了,下标是: " << it - v.begin() << endl;
}
2. 算法题
2.1 询问学号
**题目来源:**洛谷
题目链接: P3156 【深基15.例1】询问学号
难度系数:★
【解法】
直接用 vector 或者数组模拟即可。
【参考代码】
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 2e6 + 10;
int n, m;
vector<int> a(N);
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i];
while(m--)
{
int x; cin >> x;
cout << a[x] << endl;
}
return 0;
}
2.2 寄包柜
**题目来源:**洛谷
题目链接: P3613 【深基15.例2】寄包柜
难度系数:★
【解法】
如果用二维数组来模拟,需要开 10^5 × 10^5 大小的数组,空间会超。但是格子的总数量是 10^7,用数组模拟是完全够用的。因此可以用动态扩容的数组,创建 10^5个 vector 来模拟。
【参考代码】
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
int n, q;
vector<int> a[N]; // 创建 N 个柜子
int main()
{
cin >> n >> q;
while(q--)
{
int op, i, j, k;
cin >> op >> i >> j;
if(op == 1) // 存
{
cin >> k;
if(a[i].size() <= j)
{
// 扩容
a[i].resize(j + 1);
}
a[i][j] = k;
}
else // 查询
{
cout << a[i][j] << endl;
}
}
return 0;
}
2.3 移动零
**题目来源:**力扣
题目链接: 283. 移动零
难度系数:★
【解法】
在本题中,我们可以用一个 cur 指针来扫描整个数组,另一个 dest 指针用来记录非零数序列的最后一个位置。根据在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。
在 cur 遍历期间,使 0, dest 的元素全部都是非零元素,dest + 1, cur − 1 的元素全是零。
【参考代码】
cpp
class Solution
{
public:
void moveZeroes(vector<int>& nums)
{
for(int i = 0, cur = -1; i < nums.size(); i++)
{
if(nums[i]) // 非0元素
{
swap(nums[++cur], nums[i]);
}
}
}
};
2.4 颜色分类
**题目来源:**力扣
题目链接: 75. 颜色分类
难度系数:★
【解法】
类比数组分两块的算法思想,这里是将数组分成三块,那么我们可以再添加一个指针,实现数组分三块。
设数组大小为 n,定义三个指针 left, cur, right:
-
left:用来标记 0 序列的末尾,因此初始化为 −1;
-
cur:用来扫描数组,初始化为 0;
-
right:用来标记 2 序列的起始位置,因此初始化为 n。
在 cur 往后扫描的过程中,保证:
-
0, left 内的元素都是 0;
-
left + 1, cur − 1 内的元素都是 1;
-
cur, right − 1 内的元素是待定元素;
-
right, n 内的元素都是 2。
【参考代码】
cpp
class Solution
{
public:
void sortColors(vector<int>& nums)
{
int n = nums.size();
int left = -1, right = n, i = 0;
while(i < right)
{
if(nums[i] == 0) swap(nums[++left], nums[i++]);
else if(nums[i] == 1) i++;
else swap(nums[--right], nums[i]);
}
}
};
2.5 合并两个有序数组
**题目来源:**力扣
题目链接: 合并两个有序数组
难度系数:★
【解法】
解法一:利用辅助数组(需要学会,归并排序的核心步骤)
可以创建一个辅助数组,然后用两个指针分别指向两个数组。每次拿出一个较小的元素放在辅助数组中,直到把所有元素全部放在辅助数组中。最后把辅助数组的结果覆盖到 nums1 中。
解法二:原地修改(本题的最优解)
与解法一的核心思想是一样的。
由于第一个数组的空间本来就是 n + m 个,所以我们可以直接把最终结果放在 nums1 中。为了不覆盖未遍历到的元素,定义两个指针指向两个数组的末尾,从后往前扫描。每次拿出较大的元素也是从后往前放在 nums1 的后面,直到把所有元素全部放在 nums1 中。
通过这道题想告诉大家,在我们的算法竞赛中,只要你的空间不超,想用多少辅助数组就用多少辅助数组,怎么方便怎么来。但是在面试中,还是需要注意挖掘最优解。
【参考代码】
法一:利用辅助数组
cpp
class Solution
{
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n)
{
// 解法一:利用辅助数组
vector<int> tmp(m + n);
int cur = 0, cur1 = 0, cur2 = 0;
while(cur1 < m && cur2 < n)
{
if(nums1[cur1] <= nums2[cur2]) tmp[cur++] = nums1[cur1++];
else tmp[cur++] = nums2[cur2++];
}
while(cur1 < m) tmp[cur++] = nums1[cur1++];
while(cur2 < n) tmp[cur++] = nums2[cur2++];
for(int i = 0; i < n + m; i++) nums1[i] = tmp[i];
}
};
解法⼆:原地修改(本题的最优解)
cpp
class Solution
{
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n)
{
// 解法二:原地合并
int cur1 = m - 1, cur2 = n - 1, cur = m + n - 1;
while(cur1 >= 0 && cur2 >= 0)
{
if(nums1[cur1] >= nums2[cur2]) nums1[cur--] = nums1[cur1--];
else nums1[cur--] = nums2[cur2--];
}
while(cur2 >= 0) nums1[cur--] = nums2[cur2--];
}
};
补充知识:pair
pair 是 C++ 标准库中的一个模板类,用于将两个值组合成一个单一对象,通常用于存储键值对或返回多个值。它有两个公有成员 first 和 second,分别表示第一个值和第二个值。
我们可以把 pair 理解成 C++ 为我们提供一个结构体,里面有两个变量:
cpp
struct pair
{
type first;
type second;
};
使用的时候,可以指定 first 和 second 为我们想要的任意类型。
指定的方式为 pair<第一个关键字的类型, 第二个关键字的类型>,比如:
cpp
pair<int, int> p1; // 第一个 int,第二个 int
pair<long long, int> p2; // 第一个 long long,第二个 int
pair<string, int> p3; // 第一个 string,第二个 int
不过,一般使用 pair 的时候,上述方式要写很多代码,我们一般会 typedef 一下:
cpp
typedef pair<int, int> PII;
PII p1;
typedef pair<long long, long long> PLL;
PLL p2;
2.6 The Blocks Problem
**题目来源:**洛谷
题目链接: The Blocks Problem
难度系数:★★
【题目描述】
初始时从左到右有 n 个木块,编号为 0...n − 1,要求实现下列四种操作:
-
move a onto b:把 a 和 b 上方的木块归位,然后把 a 放到 b 上面。
-
move a over b:把 a 上方的木块归位,然后把 a 放在 b 所在木块堆的最上方。
-
pile a onto b:把 b 上方的木块归位,然后把 a 及以上的木块坨到 b 上面。
-
pile a over b:把 a 及以上的木块坨到 b 的上面。
一组数据的结束标志为 quit,如果有非法指令(如 a 与 b 在同一堆),无需处理。
输出:所有操作输入完毕后,从左到右,从下到上输出每个位置的木块编号。
【解法】
本质是一个模拟题,可以用 vector 来模拟,注意细节问题。
【参考代码】
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 30;
typedef pair<int, int> PII;
int n;
vector<int> p[N]; // 创建 n 个放木块的槽
PII find(int x)
{
for(int i = 0; i < n; i++)
{
for(int j = 0; j < p[i].size(); j++)
{
if(p[i][j] == x)
{
return {i, j};
}
}
}
}
void clean(int x, int y)
{
// 把 [x, y] 以上的木块归位
for(int j = y + 1; j < p[x].size(); j++)
{
int t = p[x][j];
p[t].push_back(t);
}
p[x].resize(y + 1);
}
void move(int x1, int y1, int x2)
{
// 把 [x1, y1] 及其以上的木块放在 x2 上面
for(int j = y1; j < p[x1].size(); j++)
{
p[x2].push_back(p[x1][j]);
}
p[x1].resize(y1);
}
int main()
{
cin >> n;
// 初始化
for(int i = 0; i < n; i++)
{
p[i].push_back(i);
}
string op1, op2;
int a, b;
while(cin >> op1 >> a >> op2 >> b)
{
// 查找 a 和 b 的位置
PII pa = find(a);
int x1 = pa.first, y1 = pa.second;
PII pb = find(b);
int x2 = pb.first, y2 = pb.second;
if(x1 == x2) continue; // 处理不合法的操作
if(op1 == "move") // 把 a 上方归位
{
clean(x1, y1);
}
if(op2 == "onto") // 把 b 上方归位
{
clean(x2, y2);
}
move(x1, y1, x2);
}
// 打印
for(int i = 0; i < n; i++)
{
cout << i << ":";
for(int j = 0; j < p[i].size(); j++)
{
cout << " " << p[i][j];
}
cout << endl;
}
return 0;
}
3. 拓展:ACM 模式 vs 核心代码模式
3.1 ACM 模式
ACM 模式一般是竞赛和笔试面试常用的模式,就是只给你一个题目描述,外加输入样例和输出样例,不会给你任何的代码。此时,选手或者应聘者需要根据题目要求,自己完成如下任务:
-
头文件的包含
-
main 函数的设计
-
自己定义程序所需的变量和容器(数组、链表、哈希表、字符串等等)
-
数据的输入(根据题目叙述控制输入数据的格式)
-
数据的处理(各种函数接口的设计)
-
数据的输出(根据题目叙述控制返回数据的格式)
总而言之,ACM 模式相当于给你一个空白的代码框,让你自己设计程序来解决问题。
例如:牛客网上一道简单的 ACM 模式的题:牛客学加法
题目描述:
给你两个整数,要求输出这两个整数的和
示例:
输入 1 2
输出 3
此时右边的代码框内一片空白,因此我们需要自己设计程序来解决问题。
ACM 模式代码:
cpp#include <stdio.h> // 自己写头文件 // 自己设计函数接口 int add(int a, int b) { return a + b; } int main() // 自己写主函数 { int a = 0, b = 0; // 自己定义程序所需的变量或者容器(数组) scanf("%d %d", &a, &b); // 自己处理数据的输入 int c = add(a, b); // 自己设计数据的处理逻辑,以及函数的接口 //(这里为了方便演示,因此用了函数,其实我们大可不必使用函数) printf("%d\n", c); // 自己处理数据的打印 return 0; }