#include <iostream>
using namespace std;
/*
用数组模拟动态链表的原因:每次new节点(结构体)需要一定耗时
1.链表与邻接表
2.栈与队列
3.kmp
*/
826.单链表
实现一个单链表,链表初始为空,支持三种操作:
(1) 向链表头插入一个数;
(2) 删除第k个插入的数后面的数;
(3) 在第k个插入的数后插入一个数
现在要对该链表进行M次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第k个插入的数并不是指当前链表的第k个数。例如操作过程中一共插入了n个数,则按照插入的时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,…第n个插入的数。
输入格式
第一行包含整数M,表示操作次数。
接下来M行,每行包含一个操作命令,操作命令可能为以下几种:
(1) “H x”,表示向链表头插入一个数x。
(2) “D k”,表示删除第k个输入的数后面的数(当k为0时,表示删除头结点)。
(3) “I k x”,表示在第k个输入的数后面插入一个数x(此操作中k均大于0)。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
1≤M≤100000
所有操作保证合法。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
【new node 非常慢! 所以一般竞赛、面试也用数组模拟,而不是用指针】
[ typedef struct node{e , ne ,idx} , node[N];结构体数组模拟结点,但是代码变长,仍不是最优写法 ]
记含义:
算法题:追求速度
head 表示存头结点的下标
e[i] 表示下标节点i的值
ne[i] 表示节点i的next指针,值为指向下一个结点值是多少
idx 存储当前已经用到了哪个点(第k个插入的数idx = k-1) (数组长度)
【head-> 3-> 5 -> 7-> 9-> NULL】
head表示头结点下标,刚开始初始化指向-1 , head(null) 还没有指向 (-1表示空)
e[0] = 3 (第一个节点,下标0,地址存放的值)
ne[0]= ② [顺序为链表的第二个结点,结点下标(地址)]
空集用ne[最后结点的next]=-1表示, e和ne用下标关联
idx存储当前用到的地址(相当于一个指针,从第一个结点开始)
插入:),赋x值,元素x指向head->next ,然后头结点指向新结点(数组模拟的lead的next为ne[0] = idx(下一个结点下标)) ,head(指针)指向此插入元素x(元素的结点对应下标idx,再idx++
①初始化 (头指针)head = -1(空) ; idx = 0(当前无结点,0为第一个插入的结点下标);
②【头插法-算法题80%】e[idx] = x ; ne[idx] = head ; head = idx ; idx++ ;
③将x插到下标是k的点【后面】 (在下标k结点前面插入就传入实参k-1)
e[idx]=x ; ne[idx] = ne[k]; ne[k] = idx ;idx++;
④将下标k后面的节点删除 (删除第k个的结点就传入实参k-1)
ne[k] = ne[ne[k]]; //递归的味道
⑤数组模拟链表循环遍历(i模拟指针从head开始沿着ne[]):
for(int i = head ;i != -1;i = ne[i])
const int N = 100010;
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; //当前节点idx的next指针 存节点(指向头结点)
head = idx; //head的next指向idx位置 (head不存值,所以head本身值代表next位置(存放头结点下标位置))
idx++; //节点用过了,数组中移动到下一个位置
}
//将x插到下标是k的点【后面】 (插入下标k结点前面就传实参k-1即可)
void add(int k,int x) {
e[idx] = x;
ne[idx] = ne[k];//idx的next指向k的下一位
ne[k] = idx;
idx++; //注意:数组中使用过就不再用,移位 【同时代表长度】
}
//将下标k后面的节点删除(删除第k个的结点就传入实参k-1)
void remove(int k) {
ne[k] = ne[ne[k]]; // k->next = k->next->next (等效删除)
}//对应算法题可以不用管idx 【这里会浪费空间(k+1的位置),且idx不能表示长度了 -- 题目不需要用到长度,就没关系】
int main() {
int m;
cin >> m;
init();
while(m--) {
int k,x;
char op;
cin >> op;//cin会过滤空格等,scanf输入字符串可能出错
if(op == 'H') {
cin >> x;
add_to_head(x);
} else if(op == 'D') {
cin >> k; //若无处理,则删除头结点操作无效
if(!k) head = ne[head]; //k == 0时 删除头结点 head指向头结点的下一个节点嵌套ne[存头结点位置head]
remove(k - 1); //delete第k个点
} else {
cin >> k >> x; //题目是在第k个位置操作...而函数定义操作k后面的数 ,
add(k - 1,x); //用k - 1
}
}
for(int i = head; i != -1; i = ne[i]) cout << e[i] << " "; //这种输出结束条件为 i != -1 ,-1为最后的节点next->NULL的值
cout << endl;
return 0;
}
827.双链表
实现一个双链表,双链表初始为空,支持5种操作:
(1) 在最左侧插入一个数;
(2) 在最右侧插入一个数;
(3) 将第k个插入的数删除;
(4) 在第k个插入的数左侧插入一个数;
(5) 在第k个插入的数右侧插入一个数
现在要对该链表进行M次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第k个插入的数并不是指当前链表的第k个数。例如操作过程中一共插入了n个数,则按照插入的时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,…第n个插入的数。
输入格式
第一行包含整数M,表示操作次数。
接下来M行,每行包含一个操作命令,操作命令可能为以下几种:
(1) “L x”,表示在链表的最左端插入数x。
(2) “R x”,表示在链表的最右端插入数x。
(3) “D k”,表示将第k个插入的数删除。
(4) “IL k x”,表示在第k个插入的数左侧插入一个数。
(5) “IR k x”,表示在第k个插入的数右侧插入一个数。
输出格式
共一行,将整个链表从左到右输出。
数据范围
1≤M≤100000
所有操作保证合法。
输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9
双链表不用head【区别于单链表】
因为一般算法题中,单链表的head要用来存一个有效数据(指向下一个结点的地址),
而双链表不用,所以头尾(左右端点)用0 ,1 表示即可【为算法题服务的最常用模板】
记:
L[0](头)= 0,R[0] = 1(尾) //初始化左右指针
【右指针往左走,初始化在最右端,右端点记为idx=1
左指针往右走,初始化在最左端,左端点记为idx = 0】
删除第k个点 R[L[k]] =L[k] ; L[R[k]] = R[k]
(让第k结点的左指针指向右指针的下一个结点下标, k的右指针指向左指针指向的下一个节点的下标地址,
此时k脱离链表,没有指针指向k)
const int N = 100010;
int m;
int e[N],l[N],r[N],idx; //l->next(前驱) , r->next (后继) e存元素值 ,idx当前结点
//初始化 (两个无值的边界结点)
void init(){
//0表示左端点,1表示右端点
l[1] = 0,r[0] = 1; //初始化,右指针r在最右端,l在最左端,左端点idx=0
idx = 2; //注意0,1已经用过了,idx初始值为2
}
//在下标是k的点的右边,插入x
//①idx右 = k右 ②idx左 = k (先③k右(即k+1)的左 = idx) (后④k右 = idx)
void add(int k,int x) // *在k的左边插入x,传入参数k-1 , 就调用参数为 l[k] == (k - 1)
{
e[idx] = x;//赋值
r[idx] = r[k]; // idx.r->next = k.r->next idx右边指向k+1 == k.r->next
l[idx] = k; //idx.l->next = k idx左边指向k
l[r[k]] = idx; //k.r->next->l->next = (k+1).l->next = idx
r[k] = idx; //k.r->next = idx
}
//删除第k个点
void remove()
{
r[l[k]] = r[k]; //k.l->next->r->next(即(k-1).r->next) = k.r->next = (k+1)
l[r[k]] = l[k]; //同理(k+1).l->next = k.l->next = (k-1)
}
int main()
{
cin>>m;
init();
while(m--)
{
int k,int x;
string op; cin>>op;
if(op=="L")
{
cin>>x;
add(0,x);
}
if(op=="R")
{
cin>>x;
add(l[1],x);
}
if(op=="D")
{
cin>>x;
remove(x+1);
}
if(op=="IL")
{
cin >> k >> x ;
add(l[k+1],x);
}
if(op=="IR")
{
int k,x; cin>>k>>x;
add(k+1,x);
}
}
for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";
return 0;
}
/*栈
const int N = 100010
int stk[N] , tt;
//入栈
stk[++tt ] = x;
if(tt > 0) not empty
else empty
//弹出
tt-- ;
*/
/*队列
int q[N] ,hh,tt = -1;
//插入队尾
q[ ++tt ] = x;
//队头弹出
hh++;
//判断是否为空
if(hh < tt ) not empty
else empty
//取队头元素
q[hh]
单调栈(抽象但题型很少) 【左边第一个较小的数】
【单调栈/队列-最常解决的问题:给定序列,求每一个数的左边的离它最近的比它大/小的数(满足某种条件)
单调栈 (单调上升)
题目描述 :
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 -1。
输入输出 :
输入
5
3 4 2 7 5
输出
-1 3 -1 2 2
此题暴力 for (int i = 0;i <=n;i++) for(j = i - 1;j>=0;j–){找到左边第一个小于自身的元素;break} // j开始指向左边第一个元素,开始判断
【找左边的第一个小的元素,就把比右边大的元素删掉(因为有更小的数),肯定不是答案-没有用 - 得到单调栈 】
栈和队列里的存的不是元素值,而是元素下标
记:(在栈顶插入,弹出)
【插入】stk[++ tt] = x ;
【弹出删除】tt - - ;
【判断是否为空】if(tt > 0) not empty ; else empty;
【取栈顶】stk[tt] ; (tt初始值0,第一个入栈元素下标为1)
const int N = 100010;
int n;
int stk[N],tt;
int main()
{
//cin.tie(0); 加速cin读取
//ios::sync_with_stdio(false);
//cin >> n;
scanf("%d",&n);
for(int i = 0;i < n;i++) //每个元素最多出栈一次,进栈一次:2n, 复杂度O(n)
{
int x;
scanf("%d",&x);
while(tt && stk[tt] >= x ) tt--; //保证单调性,大的数放在前面
if(tt) cout << stk[tt] << " "; //栈不为空 (遍历到最后) tt != 0时打印
else cout << -1 << " "; //栈空了还未找到
stk[ ++tt] = x; //加入x,保证前面的数小于x
}
return 0;
}
单调队列 【滑动窗口】
滑动窗口
问题描述:
给定一个大小为n ≤ 106的数组。
有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到k个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为[1 3 -1 -3 5 3 6 7],k为3。
窗口位置 最小值 最大值
[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
输入格式:
输入包含两行。
第一行包含两个整数n nn和k kk,分别代表数组长度和滑动窗口的长度。
第二行有n nn个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式:
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
数据范围:
1 ≤ n , k ≤ 100000
输入样例:(元素长度,窗口大小)
8 3
1 3 -1 -3 5 3 6 7
输出样例:(第一行输出最小值,第二行输出最大值【窗口大小为3,即3个中的最小】)
-1 -3 -3 -3 3 3
3 3 5 5 6 7
每次滑动一格 : 【一进一出】队尾加进一个新元素,队头出队一个元素
q[tt]存放a数组的下标,取值用a[q[tt]] ,因此数组排序不变,队列可以循环使用数组判断
做题:
①先朴素算法模拟 :一进一出维护窗口大小k , 遍历窗口O(k)找最大最小
②优化:把没有用的元素删去,看是否具有单调性【最大最小值分开求】 (最小值:前面的数比后面的数大,则前面一定没有用,删掉,则变成单调递增,最小值一定在窗口的最左边,头指针)
最大值相反,构造单调递减,删去比前面大的元素,此时队头为最大值
记:(在队尾插入元素,在队头弹出元素)
int q[tt],hh,tt = -1; //初始值个人习惯
【插入】q[++tt] = x ;
【弹出】hh++ ;
【判断是否为空】if(hh <= tt) empty ; else empty;
【取队头元素】q[hh] , 队尾也可以取:q[tt]
const int N = 1000010;
int a[N],q[N];
int n,k;
int main(){
scanf("%d%d",&n,&k);
for(int i = 0;i < n;i++) scanf("%d",&a[i]);
//最小值
int hh = 0,tt = -1;
for(int i = 0;i < n;i++)
{
//判断对头是否已经滑出窗口
if(hh <= tt && i - k + 1 > q[hh]) hh++; //队列不为空 && 值
while(hh <= tt && a[q[tt]] >= a[i]) tt--; //循环窗口内值比较
q[++tt] = i; //存答案的下标
if(i >= k - 1) printf("%d ",a[q[hh]]);
}
puts("");
//求最大值 - 从单调上升变成单调递减 条件改成 a[q[tt]] <= a[i]
hh = 0,tt = -1;
for(int i = 0;i < n;i++)
{
//判断对头是否已经滑出窗口(hh头指针-->向右滑动)
if(hh <= tt && i - k + 1 > q[hh]) hh++; //队列不为空 && 窗口位置不越界 滑到 a[n-k-1]为最后一次滑动 【弹出,向右滑动】
while(hh <= tt && a[q[tt]] <= a[i]) tt--;//找最大 ,单调递减队列
//循环窗口内值比较
q[++tt] = i; //存答案的下标 【入队,向右滑动】
if(i >= k - 1) printf("%d ",a[q[hh]]);
}
puts("");
return 0;
}
kmp
给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串 P 在模式串 S 中多次作为子串出现。
求出模板串 P 在模式串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1≤N≤105
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2
先想
1.暴力(朴素)做法:s短串,p串 ,双重循环 for(int i = 0;i < n;i++) bool flag = true;
for(j = 1;j <= m;j++) {if(s[i] != p[j]) 如果不相等 flag = false ,break退出}
记:【p短n,s长m ,从1开始存 ,下标位于 [1,n] 和 [1,m] 】
【具体看代码:next数组过程与kmp非常类似,代码仅把s[i]换成自身p[i]即可】
【ne数组构造过程和kmp匹配过程类似,不相同不断回退:
for(i = 2,j = 0;i < n;i++),j=ne[j]//i = 1第一位失败不用退不用退,若判断if(q[i]==q[j+1]相等) j++ ,
, ne数组存放相同的前后缀长度(即回退下标),循环每轮求得 ne[i] = j】
【kmp过程】
模板串自身前缀相等的位置标记,在匹配失败回退时,只需退到相等前缀位置
再次判断下一位,不行再退j = ne[j]
for(int i = 1,j= 0 ; i <= m ; i ++ )
{
while( j&& s[i] != p[j + 1] ) j = ne[j] //退无可退 或者不匹配,p依据前后缀回退
if( s[i] == p[j + 1] ) j++; // i随着循环增加 , 比较下一位if(j == n) //匹配成功
{按题目要求}
}
预处理出数组next数组 :
子串匹配失败时,要至少向后移动多少,才能和主串(已经匹配过的位置)匹配上
前面的位置和匹配失败的 到移动的位置与主串对应位相等 ,同时也与之前已经匹配的位置相等(等效)
模板串自身的后缀和前缀相等的长度最大值==即可以回溯的位置
用next[j] : 逐位匹配的下一位判断,若 s[i] != p[j+1] 时,j回退到next[j],next[j]数组维护前后缀的最长相等长度
即指向匹配模式串的j指针在不相等时,依据ne[j]数组不断回退,如果回退到0,即从0开始(退回模式串起点)
/*字符串匹配BF
暴力+剪枝优化 朴素解法
int s[N],p[N];//s较短串,p较长串
for(int i = 0;i < n;i++)
{
bool flag = true;//是否成功
for(int j = 1;j <= m;j++)
{
if(s[i] != p[j])
{
flag = false;
break;
}
}
}
*/
const int N = 10010,M = 100010;
int n,m;
char p[N],s[N];//模板主串,模式串 【p在s(长)中寻找匹配的片段】
int ne[N];//回溯数组(最长前后缀数组)
int main()
{ //p长度n ,s长度m
cin >> n >> p + 1 >> m >> s + 1; // 模板主串长度 ,输入主串**(下标从1开始)** ,模式串长度 ,输入模式串(从1开始)
//求next[j]过程(类似kmp匹配的过程) //i = 1的时候没有前后缀,只有一个元素 ,不能包括自己(不用回退,i=2开始)
for(int i = 2,j = 0;i <= n;i ++)
{ //【回退条件:(j == 0不能再退了,最终等待i循环完退出) && (不匹配时需回退)
while( j && p[i] != p[j + 1]) j = ne[j]; //【条件和kmp匹配过程类似,自身找到相同第一个位置,j=ne[j],再判断if(相等) j++ , ne数组存放相同的前后缀长度,循环每轮求得 ne[i] = j】
//若j = 0就从头开始,不用回退了,执行判断语句即可
if(p[i] == p[j + 1]) j++;
ne[i] = j;//第i个位置的最长前后缀相等长度 ,长度为j
}
//kmp匹配过程 O(n),只看一个变量判断复杂度,i是m次(while循环j回溯等效至少每次减1,即最多执行m次) // s(长)为 m
for(int i = 1,j = 0;i <= m;i++)//下标i=1开始匹配 ,next的j = 0开始存储回退下标next[0] = 0, next[1] = 0
{ //【回退条件:(j != 0) && (不匹配时判断回退位置)
while(j && s[i] != p[j + 1]) j = ne[j]; //若j = 0就从头开始,不用回退了,执行判断语句即可
if( s[i] == p[j + 1] ) j++;//模式串已经匹配到的长度位置
if(j == n)//匹配成功
{ //【此处按题目要求】输出(所有)匹配成功的主串的第一位的位置下标
//注意:我们存的是从1开始,但题目下标从0开始,所以第一位下标为i-n
printf("%d ",i - n); //输出匹配成功的主串第一位的位置 ,即主串当前位置i减去模式串长度n加上1得到第一位下标 ,即i - n + 1 ,再减去1 ,等效从0开始存储的下标值
j = ne[j];//题目要求所有位置,成功后回退到第一个位置 p[(j = 0) + 1]
//j得到回退值,新一轮从头开始移动,s下标i仍继续增加
}
}
return 0;
}