
往期《算法基础》回顾:算法基础_基础算法【快速排序 + 归并排序 + 二分查找】 算法基础_基础算法【高精度 + 前缀和 + 差分 + 双指针】 算法基础_基础算法【位运算 + 离散化 + 区间合并】
往期《算法精讲》回顾:算法精讲【整数二分】(实战教学)

#include <iostream>
using namespace std;
const int N = 100010;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将x插到头结点
void add_to_head(int x)
{
e[idx] = x;
ne[idx] = head;
head = idx;
idx++;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}
// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int m;
cin >> m;
init();
while (m--)
{
int k, x;
char op;
cin >> op;
if (op == 'H')
{
cin >> x;
add_to_head(x);
}
else if (op == 'D')
{
cin >> k;
if (!k) head = ne[head];//k=0时,表示删除头节点
remove(k - 1);
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
return 0;
}if (!k) head = ne[head];//k=0时,表示删除头节点在这段代码中,
if (!k) head = ne[head];的作用是:处理删除头节点的特殊情况
链表的表示:
head:表示头节点的下标,初始值为 -1e[N]:存储节点的值ne[N]:存储节点的 next 指针idx:表示当前可用的节点下标 疑问:为什么需要单独处理 k = 0 的情况?
next 指针实现的。remove(k) 直接删除,需要单独处理 k = 0 的情况。head: head = ne[head];,直接将 head 指向下一个节点,实现头节点的删除。这里要区分几种操作:
//头插当中的核心两步
ne[idx] = head; //idx对应的节点连接上head对应的节点
head = idx; //head指向idx节点
//任意插当中的核心两步
ne[idx] = ne[k]; //idx对应的节点连接上k对应的节点的下一个节点
ne[k] = idx;第一条:ne数组写在等号前面的情况:在图像上的表示就是从索引代指的节点出发的一条线
第二条:ne数组写在等号后面的情况:在图像上的表示就是索引代指的节点的下一个节点的指针
第三条:等号前面不是ne数组的情况:在图像上的表示就是该值的节点的指针
第四条:等号后面不是ne数组的情况:在图像上的表示就是该值的节点的指针
如果你理解上面的四条定则之后:可以尝试分析一下下面的代码
void add_to_head_before(int x)
{
e[idx] = x; // 将值 x 存储到新节点
ne[idx] = head; // 新节点的 next 指向当前的头节点
head = idx; // 更新头节点为新节点
idx++; // 移动到下一个可用位置
}
void add_to_head_after(int x)
{
e[idx] = x; // 将值 x 存储到新节点
ne[idx] = ne[head]; // 新节点的 next 指向头节点的下一个节点
ne[head] = idx; // 头节点的 next 指向新节点
idx++; // 移动到下一个可用位置
}
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a;
r[idx] = r[a];
l[r[a]] = idx;
r[a] = idx;
idx++;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
int main()
{
cin >> m;
// 0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
while (m--)
{
string op;
cin >> op;
int k, x;
if (op == "L")
{
cin >> x;
insert(0, x);
}
else if (op == "R")
{
cin >> x;
insert(l[1], x);
}
else if (op == "D")
{
cin >> k;
remove(k + 1);
}
else if (op == "IL")
{
cin >> k >> x;
insert(l[k + 1], x);
}
else
{
cin >> k >> x;
insert(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
return 0;
}// 0是左端点,1是右端点
r[0] = 1, l[1] = 0;在这段代码中,
l[N]和r[N]是用于实现双向链表的数组。l[N]存储每个节点的左指针(前驱节点),r[N]存储每个节点的右指针(后继节点)
双向链表的表示
e[N]:存储每个节点的值l[N]:存储每个节点的左指针(前驱节点)r[N]:存储每个节点的右指针(后继节点)idx:表示当前可用的节点下标 r[0] = 1, l[1] = 0; 的作用
0 和 1 是两个虚拟节点,分别表示链表的左边界和右边界。0 是左边界节点,1 是右边界节点。r[0] = 1:表示左边界节点的右指针指向右边界节点。l[1] = 0:表示右边界节点的左指针指向左边界节点。0 <-> 1else if (op == "R")
{
cin >> x;
insert(l[1], x);
}
else if (op == "IL")
{
cin >> k >> x;
insert(l[k + 1], x);
} 疑问:为什么上面的代码中insert函数的位置参数为什么都需要
l[]insert(l[1], x);:原因是因为双链表的左右端点和单链表的头节点的性质是一样的。 它们只是为了方便我们操作单/双链表而被设计出来,辅助实现插入删除操作。 所以:这里要插入的节点的位置是右端点左侧节点的右侧insert(l[k + 1], x);:原因是我们的插入函数是在第K个节点的右侧插入一个节点,所以想在第K节点左侧插入一个节点就要传入第K个节点左侧的节点。
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';这行代码的作用是:遍历双向链表并输出链表中的所有元素
双向链表的表示:
r[N]:存储每个节点的右指针(后继节点)e[N]:存储每个节点的值0 是左边界节点,1 是右边界节点。 代码的逻辑:
1. r[0]
r[0] 是左边界节点 0 的右指针,指向链表的第一个实际节点。 0 <-> 3 <-> 2 <-> 1,则 r[0] = 3(指向第一个实际节点 3) 2. i != 1
1 是右边界节点,表示链表的结束。i = 1 时,表示已经遍历到链表的末尾。 3. i = r[i]
r[i] 是节点 i 的右指针,指向下一个节点。i = r[i],可以移动到链表的下一个节点。 4. cout << e[i] << ' ';
i 的值 e[i]输入:
5
L 10
R 20
IL 1 15
IR 1 18
D 1初始化
0 <-> 1(0 是左边界节点,1 是右边界节点)idx: 2(因为 0 和 1 已经被占用)1. 操作 L 10
10insert(0, 10)0 的右边插入 1020 <-> 10 <-> 1e = [0, 0, 10]l = [0, 0, 0]r = [2, 1, 1]idx = 32. 操作 R 20
20insert(l[1], 20)l[1] = 2(右边界节点的前驱节点是节点 2)2 的右边插入 2030 <-> 10 <-> 20 <-> 1e = [0, 0, 10, 20]l = [0, 0, 0, 2]r = [2, 1, 3, 1]idx = 43. 操作 IL 1 15
1 个插入的节点的左边插入 151 个插入的节点是节点 2(值为 10)insert(l[2], 15)l[2] = 0(节点 2 的前驱节点是左边界节点 0)0 的右边插入 1540 <-> 15 <-> 10 <-> 20 <-> 1e = [0, 0, 10, 20, 15]l = [0, 0, 4, 2, 0]r = [4, 1, 3, 1, 2]idx = 54. 操作 IR 1 18
1 个插入的节点的右边插入 181 个插入的节点是节点 2(值为 10)insert(2, 18)2 的右边插入 1850 <-> 15 <-> 10 <-> 18 <-> 20 <-> 1e = [0, 0, 10, 20, 15, 18]l = [0, 0, 4, 2, 0, 2]r = [4, 1, 5, 1, 2, 3]idx = 65. 操作 D 1
1 个插入的节点(即节点 2,值为 10)remove(2)20 <-> 15 <-> 18 <-> 20 <-> 1e = [0, 0, 10, 20, 15, 18]l = [0, 0, 4, 5, 0, 4]r = [4, 1, 5, 1, 5, 3]idx = 6最终链表
0 <-> 15 <-> 18 <-> 20 <-> 115, 18, 20输出
15 18 20
#include <iostream>
#include <vector>
using namespace std;
int n, x;
string str;
//模拟栈 ----> 一个一维数组 + 一个int变量
const int N = 1e5 + 10;
vector<int> stk(N);
int tt = 0;
int main()
{
cin >> n;
while (n--)
{
cin >> str;
if (str == "push")
{
//入栈操作
cin >> x;
stk[++tt] = x;
}
else if (str == "pop")
{
//出栈操作
tt--;
}
else if (str == "empty")
{
//栈判空
if (tt > 0) cout << "NO" << endl;
else cout << "YES" << endl;
}
else if (str == "query")
{
//显示栈顶元素
cout << stk[tt] << endl;
}
}
return 0;
}数组模拟栈 ----> 一个**
一维数组 + 一个int变量**:vector<int> stk(N); int tt = 0;入栈操作 :stk[++tt] = x出栈操作:tt--栈判空:if (tt > 0)显示栈顶元素:stk[tt]

#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <unordered_map>
using namespace std;
stack<int> num;
stack<char> opt;
void eval()
{
auto b = num.top(); num.pop(); //先出op栈的数字是后一个运算数
auto a = num.top(); num.pop(); //后出op栈的数字是前一个运算数
auto c = opt.top(); opt.pop();
int x;
if (c == '+') x = a + b;
else if (c == '-') x = a - b;
else if (c == '*') x = a * b;
else x = a / b;
num.push(x);
}
int main()
{
unordered_map<char, int> pr{ {'+', 1}, {'-', 1}, {'*', 2}, {'/', 2} };
string str;
cin >> str;
for (int i = 0; i < str.size(); i++)
{
auto c = str[i];
if (isdigit(c)) //提取到的字符是数字
{
int x = 0, j = i;
while (j < str.size() && isdigit(str[j]))
{
x = x * 10 + str[j] - '0';
j++;
}
i = j - 1; //j现在指向的位置不是数字,该位置的字符应该重新判定应该执行那个操作了,所以i应该=j
//但是for循环里有i++操作,所以这里让i先等于j-1
num.push(x);
}
else if (c == '(') opt.push(c); //提取到的字符是(
else if (c == ')') //提取到的字符是)
{
while (opt.top() != '(') eval();
opt.pop();//将'('出栈
}
else//提取到的字符是:'+','-','*','/'
{
while (opt.size() && opt.top() != '(' && pr[opt.top()] >= pr[c]) eval();
opt.push(c);//将新遍历到的操作符入栈
}
}
while (opt.size()) eval();
cout << num.top() << endl;
return 0;
}
if (isdigit(c))
函数的介绍:isdigit():用于检查一个字符是否是数字字符(即0到9之间的字符)
函数的原型:
int isdigit(int c);c:是待检查的字符。 int 类型的参数,这是因为该函数可以处理 EOF(文件结束符)char 类型的变量,因为 char 类型会自动转换为 int 类型
函数的返回值:
c 是十进制数字字符(即:字符 '0' 到 '9'),函数返回一个非零值(通常为 true)c 不是十进制数字字符,函数返回 0(即 false)疑问:为什么不将
str[j] - '0'改为str[j] + '0'?
str[j] - '0':是将字符转换为对应的数字值。
'5' 的 ASCII 值是 53,字符 '0' 的 ASCII 值是 48,所以 '5' - '0' 的结果是 5str[j] + '0':会将字符的 ASCII 值加上 '0' 的 ASCII 值,结果是一个完全不同的数值。
'5' 的 ASCII 值是 53,加上 '0' 的 ASCII 值 48,结果是 101,这显然不是我们想要的数字值while (j < str.size() && isdigit(str[j]))
{
x = x * 10 + str[j] - '0';
j++;
}这段代码的作用是提取表达式中的多位数,并将其转换为整数存储到 num 栈中
代码的作用:
12、345 等),而 str[i] 只能提取单个字符。x = x * 10 + str[j] - '0';,将字符形式的数字转换为整数。示例:
输入:12+341:
isdigit('1') 为真,进入循环x = 0 * 10 + 1 = 1j++,j = 12:
isdigit('2') 为真,继续循环x = 1 * 10 + 2 = 12j++,j = 2+:
isdigit('+') 为假,退出循环i = j - 1 = 1x = 12 压入 num 栈3:
isdigit('3') 为真,进入循环x = 0 * 10 + 3 = 3j++,j = 44:
isdigit('4') 为真,继续循环x = 3 * 10 + 4 = 34j++,j = 5j = 5,超出字符串范围,退出循环i = j - 1 = 4x = 34 压入 num 栈while (op.top() != '(') eval();疑问:c == ')'时为什么要这样写while (op.top() != ‘(’) eval(); 在表达式求值的代码中,当遇到右括号
)时,需要计算括号内的表达式。
op 栈中弹出操作符并计算,直到遇到左括号 ((2+(3*4))),这段代码也能正确处理。while (op.size() && op.top() != '(' && pr[op.top()] >= pr[c]) eval();
疑问:当提取到的字符是:'+','-','*','/'是为什么要这么写while (op.size() && op.top() != '(' && pr[op.top()] >= pr[c]) eval();
op.size():
op 栈不为空op.top() != '(':
(,可以开始进行计算(,表示还不能进行计算,应当先将操作符入栈 ')'时再进行计算pr[op.top()] >= pr[c]:
eval():
op 栈中弹出一个操作符,从 num 栈中弹出两个操作数,进行计算,并将结果压入 num 栈
#include <iostream>
#include <vector>
using namespace std;
//获取数据:
int n, x;
string str;
//使用数组模拟队列 ---> 一个原生的数组 + 两个int变量
const int N = 1e5 + 10;
int arr[N];
int hh, tt = -1;
int main()
{
cin >> n;
//处理数据:
//1.队尾插入一个整数x 2.队头出对一个元素 3.判断队列是否为空 4.查询队头的元素
while (n--)
{
cin >> str;
if (str == "push")
{
cin >> x;
arr[++tt] = x;
}
else if (str == "pop")
{
hh++;
}
else if (str == "empty")
{
if (hh <= tt) cout << "NO" << endl;
else cout << "YES" << endl;
}
else
{
cout << arr[hh] << endl;
}
}
return 0;
}
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main()
{
int n;
cin >> n;
while (n--)
{
int x;
scanf("%d", &x);
while (tt && stk[tt] >= x) tt--; //出栈操作
if (!tt) printf("-1 "); //栈的判空操作
else printf("%d ", stk[tt]); //显示栈顶元素
stk[++tt] = x; //入栈操作
}
return 0;
}单调栈的思路步骤: 第一步:输入一个整数 第二步:使用while循环判断是否需要出栈 第三步:使用if语句判断当前的栈是否为空
第六步:将整数入栈

#include <iostream>
using namespace std;
const int N = 1000010;
int arr[N], que[N];
int hh = 0, tt = -1;
int main()
{
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i++) scanf("%d", &arr[i]);
for (int i = 0; i < n; i++)
{
if (hh <= tt && i - k + 1 > que[hh]) hh++; // 如果队头元素已经不在当前窗口范围内,则出队
while (hh <= tt && arr[que[tt]] >= arr[i]) tt--; // 如果队尾元素大于等于当前元素,则出队
que[++tt] = i;//入队操作
if (i >= k - 1) printf("%d ", arr[que[hh]]);// 当窗口大小达到 k 时,输出队头元素(当前窗口的最小值)
}
puts("");
hh = 0, tt = -1;
for (int i = 0; i < n; i++)
{
if (hh <= tt && i - k + 1 > que[hh]) hh++;
while (hh <= tt && arr[que[tt]] <= arr[i]) tt--;
que[++tt] = i;
if (i >= k - 1) printf("%d ", arr[que[hh]]);
}
puts("");
return 0;
}
单调队列的思路步骤: 第一步:使用for循环遍历整个数组中的元素 第二步:使用if语句判断队头元素是否已经不在当前窗口范围内,如果是则将队头元素出队 第三步:使用while语句判断队尾元素是否大于等于当前元素,如果是则将队尾元素出队 第四步:将当前的元素入队 第五步:当窗口大小达到 k 时,输出队头元素