链表:
概念:每一个结点只知道旁边的点,通过传递的方式遍历链表。
单向链表(用数组模拟,用结构体模拟的话new比较慢,数组虽然会导致一点空间浪费,但很快)
const int s = 1e6 + 10;
int e[s], ne[s]; //e数组存值,ne数组存指针(即这个下标对应的结点指向哪)
int n, ind, hp;
void init() //创建链表,hp是头指针,ind是标记目前数组用到哪里
{
hp = -1;
ind = 0;
}
void add_to_head(int x) //在链表头插入结点
{
e[ind] = x;
ne[ind] = hp; //注意,用新结点指第一个结点,再用头指针指新结点
hp = ind;
ind++; //ind永远指向下一个空的位置
}
void add(int k, int x) //在物理存储(即数组)位置为k的右边插入元素,即k+1个插入的元素右边
{
if (k == -1) //特判在链表头插入
{
e[ind] = x;
ne[ind] = hp;
hp = ind;
ind++;
}
else
{
e[ind] = x;
ne[ind] = ne[k];
ne[k] = ind;
ind++;
}
}
void remove(int k) //移除元素
{
if (k == -1) //特判移除链表头元素
{
hp = ne[hp];
}
else
{
ne[k] = ne[ne[k]]; //原理就是直接移去指向它的指针(这样就找不到它了,相当于删除,实际上还在)
}
}
int main()
{
ios::sync_with_stdio(false);
cin >> n;
init();
while (n--)
{
int k, x;
char c;
cin >> c;
switch (c)
{
case 'H':cin >> x; add_to_head(x); break;
case 'D':cin >> k; remove(k - 1); break;
case 'I':cin >> k >> x; add(k - 1, x);
}
}
for (int i = hp; i != -1; i = ne[i]) //从头指针开始传递遍历
{
cout << e[i] << ';a
}
}
注意点:分清物理存储结构(即用数组存储)和逻辑存储结构(即链表),链表是结果,数组存储是实现方式
升级为双向链表(都一样的,只是一个结点变得可以指向左右两结点)
模版:
const int s = 1e6 + 10;
int e[s], l[s], r[s]; //e数组存值,l数组存左指针,r数组存右指针(下标对应结点)
int m, ind;
void init() //创建链表,以0为左指针,1为右指针(他们都没有值)
{
ind = 2;
r[0] = 1;
l[1] = 0;
}
void add(int k, int x) //在物理存储结构为k(即第k个插入的元素右边)右边插入元素
{
e[ind] = x;
l[ind] = k;
r[ind] = r[k];
l[r[k]] = ind; //这里一定要注意顺序
r[k] = ind;
ind++;
}
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k]; //除去物理存储结构位置为k的元素(即第k个插入的元素),去除指向它的指针
}
int main()
{
ios::sync_with_stdio(false);
cin >> m;
init();
while (m--)
{
int k, x; //这里的k是第几个元素
string c;
cin >> c;
if (c == "L")
{
cin >> x; add(0, x);
}
else if (c == "R")
{
cin >> x; add(l[1], x);
}
else if (c == "D")
{
cin >> k; remove(k + 1);
} //加一是因为占用0和1
else if (c == "IR")
{
cin >> k >> x;
add(k + 1, x);
}
else
{
cin >> k >> x;
add(l[k + 1], x); //加在k的左边相当于加在k左边的右边(这里有个转换)
}
}
for (int i = r[0]; i != 1; i = r[i])
{
cout << e[i] << ' ';
}
}
栈:
概念:可以想象成一个罐子,先进后出。
用数组模拟:
const int s = 1e6 + 10;
int sta[s];
int top = -1; //初始化头指针为-1,使其永远指向栈顶元素
int m;
void push(int x)
{
sta[++top] = x;
}
void pop()
{
top--; //要删除栈顶元素只要下调栈顶指针,之后入栈就会覆盖旧的数据
}
bool empty()
{
if (top < 0)return true;
else return false;
}
int query() //查询栈顶元素
{
return sta[top];
}
int main()
{
cin >> m;
while (m--)
{
int x;
string c;
cin >> c;
if (c == "push") { cin >> x; push(x); }
else if (c == "pop") { pop(); }
else if (c == "empty") { if (empty())cout << "YES" << endl; else cout << "NO" << endl; }
else { cout << query() << endl; }
}
}
应用:用栈来求表达式的值
给定一个表达式,其中运算符仅包含 +,-,*,/
(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。
如
(2+2)*(1+1)
stack<int>num;
stack<char>op; //两个栈,一个存数字,一个存操作符
string a;
unordered_map<char, int>pi{ {'+',1},{'-',1},{'*',2},{'/',2} }; //操作符绑定优先级
void eval() //取一个操作符和两操作数计算
{
char c = op.top(); op.pop();
int x, y, ans;
y = num.top(); num.pop();
x = num.top(); num.pop(); //注意先取的在右边,后去的在前面
if (c == '+') ans = x + y;
else if (c == '-') ans = x - y;
else if (c == '*') ans = x * y;
else ans = x / y;
num.push(ans); //把结果放回栈
}
int main()
{
ios::sync_with_stdio(false);
cin >> a;
for (int i = 0; i < a.size(); i++)
{
char c = a[i];
if (isdigit(c)) //判断c是否为数字
{
int x = 0;
x = c - '0';
while (i < a.size() && isdigit(a[++i])) x = x * 10 + a[i] - '0'; //模拟位数
i--;
num.push(x); //是数字直接进数字栈
}
else if (c == '(') op.push(c); //遇到(直接进操作符栈
else if (c == ')')
{
while (op.top() != '(') eval(); //如果遇到)一直计算,知道操作栈中(为栈首
op.pop(); //去(
}
else
{
while (op.size() && op.top() != '(' && pi[op.top()] >= pi[c]) eval(); //遇到其他操作符,规则就是
op.push(c); //一直进行计算,知道操作栈顶的元素优先级小于要放进的操作符
}
}
while (!op.empty()) eval(); //全输入后一直计算达到结果
cout << num.top();
}
原理就是将重载符和重载数组合在一起按优先级写成一颗二叉树
如
(2+2)*(1+1)
就画成
再如 55*9+55/45+4可画成
可以通过后序遍历,先求所有子树,最后求值
但我们可以通过栈来模拟(核心就是模拟优先级)
队列:
想象成中通纸筒,先进先出。
数组模拟:
const int s = 1e6 + 10;
int q[s];
int l = 0, r = -1; //l为队头指针,r为队尾指针(其初始为-1的原因与栈相同)
int m;
void push(int x)
{
q[++r] = x;
}
void pop() //弹出元素就移动队头指针来实现
{
l++;
}
bool empty()
{
return l > r;
}
int query()
{
return q[l];
}
int main()
{
string c; int x;
cin >> m;
while (m--)
{
cin >> c;
if (c == "push") { cin >> x; push(x); }
else if (c == "empty") { if (empty()) cout << "YES" << endl; else cout << "NO" << endl; }
else if (c == "pop") pop();
else cout << query() << endl;
}
}
单调栈(比较抽象)
概念:就是使栈内的元素成单调增减的关系
应用的场景:求一个数列中在某个元素左边离他最近的、小于它的数
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
模版:
const int s = 1e6 + 10;
stack<int>sta;
int a[s];
int n; //n是数组长度
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n;
while (n--)
{
int x;
cin >> x;
while (!sta.empty() && sta.top() >= x) sta.pop(); //把大于讨论元素的栈值从上到下去掉
if (sta.empty()) cout << "-1" << ' ';
else cout << sta.top() << ' ';
sta.push(x);
}
}
原理:举个例子 1 5 3 6 7 9 10 2,当讨论2后面的数时,5 3 6 7 9 10 都没有机会是答案,因为是从讨论数往前找,找也是找2,所以说5 3 6 7 9 10 都可以在讨论2时都删了。
变形:
1.如果找左边离最近的大于的数,就直接把sta.top() >= x改成<=就OK。
2.如果找右边的就把数组翻转
单调队列:
和单调栈一样,都是维持队列中的单调性
应用场景:给定一个移动区间,求区间中的最小、大值
给定一个大小为 n≤106�≤106 的数组。
有一个大小为 k� 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k� 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7]
,k� 为 33。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
模版:
const int s = 1e6 + 10;
int j[s], k[s]; //j数组存题目给的数组,k数组是队列数组(存的是原数组的下标)
int n, m;
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> j[i];
int h = 0, t = -1; //h头指针,t尾指针
for (int i = 0; i < n; i++)
{
if (i - k[h] + 1 > m) h++; //判断队列头元素是否出了区间
while (h <= t && j[k[t]] >= j[i]) t--; //和讨论元素进行比较,特别注意,这里元素是通过队尾出队列的,保证队列头元素一直为队列中作为原数组最大元素的下标,也维持单调性
k[++t] = i; //插入讨论元素
if (i >= m - 1) cout << j[k[h]] << ' '; //一直输出队头元素就行(因为它就是最大的)判断是因为区间限制,区间一开始就要占m个位
}
cout << endl;
h = 0, t = -1;
for (int i = 0; i < n; i++)
{
if (i - k[h] + 1 > m) h++;
while (h <= t && j[k[t]] <= j[i]) t--; //求最大时就这里符号有变
k[++t] = i;
if (i >= m - 1) cout << j[k[h]] << ' ';
}
}
原理:
队头元素永远是最大、小的
KMP算法
用途:给定一个字符序列,在一个字符串中寻找这个序列的出现位置
给定一个字符串 S(被匹配的串,长),以及一个模式串 P(匹配串,短),所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P 在字符串 S 中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
输入样例:
3
aba
5
ababa
输出样例:
0 2
算法说明:
但我们细想一下,因为我们在不匹配的情况出现之前,已经匹配了一段,为什么不利用呢?
由此可见,我们如果知道匹配串每个字符到首字符的字符串中,前缀和后缀最大的相等字母数 j 。当一个字母的后一个字母开始不匹配时,直接将那个字母的位值用对应j位置的匹配字符串开始就行;
举个例子
基本思路就这样。
我们来看看代码模版:
const int s = 1e6 + 10, ss = 1e6 + 10;
char da[s], xiao[ss]; //da数组是字符串,xiao数组是模式串
int ne[ss]; //ne数组存前后缀最大相等长度
int n, m;
int main()
{
cin >> n >> xiao + 1 >> m >> da + 1;
for (int i = 2, j = 0; i <= n; i++) //这一步是找各个字符的j值
{
while (j && xiao[j + 1] != xiao[i]) j = ne[j]; //和下面匹配的思路是完全一样的,用到的ne都是已经算好的
if (xiao[j + 1] == xiao[i]) j++; //如果符合,j++。
ne[i] = j;
}
for (int i = 1, j = 0; i <= m; i++)
{
while (j && xiao[j + 1] != da[i]) j = ne[j]; //匹配不成功就移到最大相等前缀尾
if (xiao[j + 1] == da[i]) j++;
if (j == n)
{
cout << i - n << ' ';
j = ne[j];
}
}
}
有什么想法或单纯想喷疑问的都欢迎评论~~~
持续更新中~~~