
🫧个人主页:小年糕是糕手
💫个人专栏:《C++》《Linux》《数据结构》《Blender 修行笔记》
🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来!
目录
[六、The Blocks Problem](#六、The Blocks Problem)
一、询问学号
cpp
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main()
{
int n, m;
//n表示学生个数,m表示询问次数
cin >> n >> m;
vector<int>a(n);//表示学生学号
vector<int>b(m);//表示询问次数
for (int i = 0; i < n; i++)
{
//依次输入学生学号
cin >> a[i];
}
for (int j = 0; j < m; j++)
{
cin >> b[j];
auto it = find(a.begin(), a.end(), b[j]);
if (it != a.end())
cout << *it << endl;
else
cout << -1 << endl; // 没找到输出-1最安全
}
return 0;
}
我们先来看这段代码,思路都是对的,而且非常清晰,也使用了我们刚学的vector,但是这段代码的问题是超时了,相信大部分初学者都会这样写,因为思路清晰而且一步到位中间没有任何报错,但是竞赛中是非常讲究效率的,这里的代码问题就出现在find:
- 你的
find是从头遍历到尾查找- 数据一大,就会跑得特别慢
- 题目数据量很大时,就会 超时(TLE)
下面我们对这段代码进行改良:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
//加速输入输出,应对2e6级别的大数据量
//大家应对大数据处理99%的题目可以无脑写上去
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<int> a(n);
// 按顺序读入n个学生的学号
for (int i = 0; i < n; ++i)
{
cin >> a[i];
}
// 处理m次询问
for (int j = 0; j < m; ++j)
{
int pos;
cin >> pos;
// 第pos个同学 → 下标是 pos-1(因为vector从0开始计数)
cout << a[pos - 1] << endl;
}
return 0;
}
二、寄包柜
cpp
#include<iostream>
#include<vector>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
//n表示寄包柜的个数,q表示询问次数
int n, q;
cin >> n >> q;
int m = 100000;//m表示寄包柜的格子
//创建了一个二维数组表示寄包柜第n个柜子,m个格子
//下标从1开始
vector<vector<int>>a(n + 1, vector<int>(m + 1, 0));
int k = 0;//表示存的物品
while (q--)
{
int Store_or_Search;//表示要存储还是查询
cin >> Store_or_Search;
if (Store_or_Search == 1)
{
//存储
int i, j;//第i个柜子第j个格子
cin >> i >> j;
cin >> k;
a[i][j] = k;
}
if (Store_or_Search == 2)
{
//查询
int i, j;//第i个柜子第j个格子
//输入我要查询的
cin >> i >> j;
cout << a[i][j] << endl;
}
}
return 0;
}
我们上述的思路没有什么问题但是任意造成内存爆炸,也就是如果用二维数组来模拟,需要开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)
{
// 扩容
//vector下标从0开始但是我们从1开始存
a[i].resize(j + 1);
}
a[i][j] = k;
}
else // 查询
{
cout << a[i][j] << endl;
}
}
return 0;
}
三、移动零
我们的第一想法是我们通过遍历整个数组,遇到0的元素我们就和后面一个元素交换即可,这样的思路是最简单的也是最可行的,但是这样的效率显然是不高的
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n = nums.size();
for (int k = 0; k < n; k++) {
for (auto i = nums.begin(); i != nums.end() - 1; i++) {
if (*i == 0) {
swap(*i, *(i + 1));
}
}
}
}
};
我们这里可以用到我们以前学过的一种方法叫做双指针法,没学过的同学可以从这题入手来学习一下什么是双指针法:
双指针法 = 用两个指针(索引),一次遍历就把数组里的非零元素移到前面,剩下位置补 0,时间复杂度 O (n),空间复杂度 O (1),原地操作,是这道题的最优解。
核心原理
我们用两个指针:
- 快指针(fast) :负责遍历整个数组,找非零元素
- 慢指针(slow):负责标记「下一个非零元素应该放的位置」
执行步骤
- 快指针从头遍历数组:
- 遇到非零元素 → 把它赋值给慢指针的位置,然后慢指针 + 1
- 遇到 0 → 直接跳过,不操作
- 遍历完后,慢指针后面的所有位置,全部补 0(这些位置本来就是要放 0 的)
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++) {
if (nums[fast] != 0) {
// 直接交换非零元素到慢指针位置
swap(nums[slow], nums[fast]);
slow++;
}
}
}
};
四、颜色分类
这道题目也是对双指针法的一个练习,希望大家独立去试试看,下面给出答案:
cpp
class Solution {
public:
void sortColors(vector<int>& nums) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++)
{
if (nums[fast] != 0)
{
swap(nums[fast], nums[slow]);
slow++;
}
}
slow = 0;
for (int fast = 0; fast < nums.size(); fast++)
{
if (nums[fast] != 1)
{
swap(nums[fast], nums[slow]);
slow++;
}
}
slow = 0;
for (int fast = 0; fast < nums.size(); fast++)
{
if (nums[fast] != 2)
{
swap(nums[fast], nums[slow]);
slow++;
}
}
}
};
这段代码也符合要求,也可以通过题目但是代码过于冗余了其实我们可以对他进行如下的优化:
cpp
class Solution
{
public:
void sortColors(vector<int>& nums)
{
int n = nums.size();
int left = -1;
int right = n;
int i = 0;
while (i < right)
{
if (nums[i] == 0)
{
left++;
swap(nums[left], nums[i]);
i++;
}
else if (nums[i] == 1)
{
i++;
}
else // nums[i] == 2
{
right--;
swap(nums[right], nums[i]);
}
}
}
};
下面我们对上述的代码来进行分析:
算法思路
left:指向已排好序的0区域的最后一个位置(初始为-1)。
right:指向已排好序的2区域的第一个位置(初始为n)。
i:当前遍历指针,从0开始。循环条件:
i < right,确保未处理的元素在[i, right-1]区间内。处理逻辑:
若
nums[i] == 0:将
left右移,交换nums[left]和nums[i],然后i++。(此时
nums[left]是0,left指向0区域的末尾,i继续向右)若
nums[i] == 1:直接
i++,因为1应放在中间区域。若
nums[i] == 2:将
right左移,交换nums[right]和nums[i],不移动i。(因为交换过来的数可能是
0或1,需要在下一次循环中重新判断)示例演示
以
nums = [2,0,2,1,1,0]为例:
初始:
left=-1, right=6, i=0
nums[0]=2→right=5,交换nums[5]和nums[0]→[0,0,2,1,1,2]
i=0,nums[0]=0→left=0,交换nums[0]和nums[0]→[0,0,2,1,1,2],i=1
nums[1]=0→left=1,交换nums[1]和nums[1]→[0,0,2,1,1,2],i=2
nums[2]=2→right=4,交换nums[4]和nums[2]→[0,0,1,1,2,2],i=2
nums[2]=1→i=3
nums[3]=1→i=4此时
i=4, right=4,循环结束,得到[0,0,1,1,2,2]。复杂度
时间复杂度:O(n),每个元素最多被交换一次。
空间复杂度:O(1),仅使用常数个额外变量。
五、合并两个有序数组
我们首先想到的一种想法应该是我们去利用辅助数组,也就是我们创建一个第三个数组作为一个中间变量来帮助我们实现操作,我们可以定义两个变量指向nums1和nums2的初始位置然后我们通过遍历比较这两个数组的元素大小并且按照顺序放入第三个数组中,当某一个数组遍历完另一个数组直接放到第三个数组中即可:
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];
cur++;
cur1++;
}
else
{
tmp[cur] = nums2[cur2];
cur++;
cur2++;
}
}
//当循环结束之后有一个数组已经遍历完了,我们给剩下的一个数组剩余的直接插入即可
while (cur1 < m)
{
tmp[cur] = nums1[cur1];
cur++;
cur1++;
}
while (cur2 < n)
{
tmp[cur] = nums2[cur2];
cur++;
cur2++;
}
for (int i = 0; i < m + n; i++)
{
nums1[i] = tmp[i];
}
}
};
我们这里由于第⼀个数组的空间本来就是m+n个,所以我们可以直接把最终结果放在nums1中。为了不覆盖未遍历到的元素,定义两个指针指向两个数组的末尾,从后往前扫描。每次拿出较大的元素也是从后往前放在nums1的后面,直到把所有元素全部放在nums1中:
cpp
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
//cur1指向第一个数组最后一个元素位置
//cur2指向第二个数组最后一个元素位置
//cur指向第一个数组这整个的末尾(大小是m+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];
cur--;
cur1--;
}
else
{
nums1[cur] = nums2[cur2];
cur--;
cur2--;
}
}
while (cur2 >= 0)
{
nums1[cur] = nums2[cur2];
cur--;
cur2--;
}
}
};
六、The Blocks Problem
这个题目放到后面一部分的内容进行讲解,大家可以自己先去自行尝试解决
七、排队顺序
cpp
#include<iostream>
#include<vector>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;//表示学生人数
cin >> n;
int h;//第一个小朋友的编号
vector<int>ne(n + 1);//学生后面一个人的编号
for (int i = 1; i <= n; i++)
{
//给小朋友后面的一个人的编号确定下来
cin >> ne[i];
}
cin >> h;
cout << h << " ";
int j;
for (j = ne[h]; ne[j] != 0; j = ne[j])
{
cout << j << " ";
}
cout << j << endl;//最后一个遗漏的小朋友
return 0;
}
我们来分析一下我给的这段代码:我们首先是提升了代码的输入和输出效率,定义了n来表示学生的人数,h表示我们第一个小朋友(排头)的编号,定义了一个动态数组用来存储每个学生后面的一个学生的编号,下面我们用for去给每一个小朋友后面给一个编号,此时的ne数组就应该变成如图所示:
下面我们cin>>h表示排在第一个的小朋友的编号,同时可以直接打印出来(因为最后我们也是按照编号顺序输出小朋友的顺序,他肯定是第一个),下面我们定义了一个整型变量j表示学生,for循环让j先等于第一个小朋友在ne数组中的对应值,也就是我们找到了第一个小朋友的后面一个小朋友,直接打印出j即可,因为他就代表小朋友,然后只要j不为0即他不是最后一个小朋友我们就继续遍历,但是要注意的是我们这段代码遇到j==0就停止了,也就是无法打印出最后一个小朋友所以我们需要额外打印一下,这点要注意,如果是采用while循环就没有这样的问题,但是这里为了便于大家理解我还是用for循环来解释的,这段代码的时间复杂度是O(N),一般来说O(N)都是符合题目要求的
八、单向链表
cpp
#include<iostream>
#include<list>
#include<algorithm>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;//q表示操作次数
cin >> q;
list<int>arr;//表示存储元素的链表
int ret;//表示操作数
int x, y;//表示后续操作中的x和y
while(q--)
{
cin >> ret;//表示下面该如何操作
if (ret == 1)
{
cin >> x >> y;
if (arr.empty())
{
arr.push_back(x);
arr.push_back(y);
}
else
{
auto it = find(arr.begin(), arr.end(), x);
arr.insert(next(it), y);//在x的后面插入y
}
}
else if (ret == 2)
{
cin >> x;
auto it = find(arr.begin(), arr.end(), x);
auto nx = next(it);
if (nx == arr.end())
cout << 0 << '\n';
else
cout << *nx << '\n';
}
else if (ret == 3)
{
cin >> x;
auto it = find(arr.begin(), arr.end(), x);
auto nx = next(it);
if (nx != arr.end())
{
//删除
arr.erase(nx);
}
}
}
return 0;
}
我们对这段代码来进行分析,这段代码下意识看是没有什么问题的,但是这段代码只能拿80分,
std::find是 O (n) 时间复杂度的全局算法 ,每次操作都要从头遍历整个链表。当数据量很大(比如 1e5 次操作)时,总时间复杂度会变成 O(n²),直接超时(这里写的时候其实有一点多余的部分是判断这个链表是否为空,题目告诉我们了链表最初有一个1):这样写起来是方便但是很明显没办法让我们拿到满分,有些竞赛中大家为了节约时间这样写确实可以,不会其他方法也拿到80%的分数,这里是练习,我们这样正确的做法应该是用数组来模拟单向链表,下面给出可以拿满分的代码:
cpp
#include <iostream>
using namespace std;
const int MAXN = 1e6 + 5;
int nxt[MAXN]; // nxt[i] 表示 i 后面的元素,0 表示无
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
// 初始表只有 1
nxt[1] = 0;
while (q--) {
int op, x, y;
cin >> op;
if (op == 1) {
cin >> x >> y;
// 将 y 插入到 x 后面
nxt[y] = nxt[x];
nxt[x] = y;
}
else if (op == 2) {
cin >> x;
cout << nxt[x] << '\n';
}
else if (op == 3) {
cin >> x;
if (nxt[x] != 0) {
int del = nxt[x]; // 要删除的节点
nxt[x] = nxt[del]; // 跳过 del
// 不需要额外处理 del,因为之后不会再使用
}
}
}
return 0;
}
用
nxt[i]存储编号i的下一个元素编号,0 表示末尾。初始时只有 1,
nxt[1] = 0。插入 :
nxt[y] = nxt[x]; nxt[x] = y;即 y 接上 x 原来的后继,x 的后继改为 y。查询 :直接输出
nxt[x]。删除 :若
nxt[x] != 0,令del = nxt[x],然后nxt[x] = nxt[del]跳过 del。时间复杂度 O(q),空间 O(MAXN),完美通过。
nxt [x] = 谁 → 就表示:x 后面的人是谁
- 如果
nxt[x] = 5→ x 后面是 5- 如果
nxt[x] = 0→ x 后面没人(最后一个)为了便于大家理解,下面给出手写的解释(希望可以帮助大家理解):
九、队列安排
这里我们先尝试用list+迭代器来解题(直观、便于理解):
cpp
#include<iostream>
#include<list>
#include<algorithm>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;//表示同学人数
cin >> N;
list<int>arr;
arr.push_back(1);//先给第一个同学放进去
int p, k;
//q表示左边还是右边
//k表示插入谁的旁边
for (int i = 2; i <= N; i++)
{
cin >> k >> p;
//p为0表示插入k的左边 -- 在指定位置前插入
if (p == 0)
{
auto it1 = find(arr.begin(), arr.end(), k);
arr.insert(it1, i);
}
//p为1表示插入k的右边 -- 在指定位置后插入
if (p == 1)
{
auto it2 = find(arr.begin(), arr.end(), k);
arr.insert(next(it2), i);
}
}
int M;//M表示的是下面我要删除的次数
cin >> M;
int ret;//表示要删除谁
while (M--)
{
cin >> ret;
//查找 ret
auto it = find(arr.begin(), arr.end(), ret);
//关键:只有找到了,才删除!
//如果find没有找到,it就等于arr.end()!!!
if (it != arr.end())
{
arr.erase(it);
}
//没找到就什么都不做,直接跳过
}
//最后输出即可
for (auto e : arr)
{
cout << e << " ";
}
return 0;
}
C++
std::list里只有一个插入函数:
insert(迭代器位置, 元素)含义:在【迭代器指向的位置 前面】插入元素
那怎么实现「插前面」和「插后面」?
- 在 k 的前面 插入
cppli.insert(迭代器_k, 元素);
- 在 k 的后面 插入
cppli.insert(next(迭代器_k), 元素);
超级大白话总结(必背)
insert(it, val)→ 插在 it 前面insert(next(it), val)→ 插在 it 后面list 没有专门的 insert_before /insert_after! 就这一个 insert,靠迭代器位置区分前后!
因为每个学生的编号是 1~N,且插入删除频繁,用数组
pre[]和nxt[]记录每个编号的前驱和后继,可以在 O(1) 时间内完成查找(通过编号直接索引)
cpp
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 100005; // 根据实际数据范围可调整,如 1000005
int pre[MAXN], nxt[MAXN];
bool exist[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
// 初始化 1 号同学
exist[1] = true;
pre[1] = nxt[1] = 0;
// 依次插入 2 ~ N 号同学
for (int i = 2; i <= N; ++i) {
int k, p;
cin >> k >> p;
exist[i] = true;
if (p == 0) { // 插入到 k 左边
int left = pre[k];
pre[i] = left;
nxt[i] = k;
if (left != 0) nxt[left] = i;
pre[k] = i;
} else { // 插入到 k 右边
int right = nxt[k];
pre[i] = k;
nxt[i] = right;
if (right != 0) pre[right] = i;
nxt[k] = i;
}
}
// 删除 M 个同学
int M;
cin >> M;
while (M--) {
int x;
cin >> x;
if (!exist[x]) continue; // 已删除则忽略
int left = pre[x], right = nxt[x];
if (left != 0) nxt[left] = right;
if (right != 0) pre[right] = left;
exist[x] = false;
// 可选:清空 x 的指针
pre[x] = nxt[x] = 0;
}
// 寻找队首(存在且前驱为 0 的结点)
int head = 1;
for (int i = 1; i <= N; ++i) {
if (exist[i] && pre[i] == 0) {
head = i;
break;
}
}
// 输出队列
for (int cur = head; cur != 0; cur = nxt[cur]) {
cout << cur << " ";
}
cout << endl;
return 0;
}
十、约瑟夫问题
大家看这个题目肯定第一眼就会想到循环链表去解决这个问题,下面我们来进行尝试:
cpp
#include<iostream>
#include<list>
#include<algorithm>
using namespace std;
int main()
{
int n;//围圈人数
cin >> n;
list<int>arr1;//循环链表
int m;//表示出圈的序号
cin >> m;
// 放入 1~n
for (int i = 1; i <= n; i++)
{
arr1.push_back(i);
}
int count = 1;//表示一个序号
auto it = arr1.begin();
while (!arr1.empty())//这个循环链表不为空
{
// 循环链表:走到 end 立刻回头
if (it == arr1.end())
{
it = arr1.begin();
}
// 数到 m,删除
if (count == m)
{
cout << *it << " ";
it = arr1.erase(it);
count = 1; // 重新计数
}
else
{
++it;
count++;
}
}
return 0;
}
虽然大家在竞赛中确实要以效率为主(静态实现方式),但是以后项目中我们都是要会用这些函数的,效率是优化出来的,没有人可以一遍写出完美的代码,下面给出不用这些成员函数解决:
cpp
#include <iostream>
using namespace std;
const int N = 110;
int n, m;
int ne[N];
int main()
{
cin >> n >> m;
// 创建循环链表
for (int i = 1; i < n; i++) ne[i] = i + 1;
ne[n] = 1;
// 模拟约瑟夫游戏的过程
int t = n;
for (int i = 1; i <= n; i++) // 执⾏ n 次出圈操作
{
for (int j = 1; j < m; j++) // 让 t 向后移动 m - 1 次
{
t = ne[t];
}
cout << ne[t] << " ";
ne[t] = ne[ne[t]];
}
return 0;
}



