- 模拟单向链表
- 模拟双向链表
- 模拟队列
- 模拟栈
- 单调栈
- 单调队列
- KMP算法
为什么要模拟数据结构??
在这里我们都是用数组来模拟一些常用的数据结构(单链表,队列...),如果是像传统的使用new方法来动态创建节点,new方法是很慢的,因此在一些算法比赛中单单是new出来所有节点就会超时,因此我们呢用数组来模拟,创建静态链表等。当然了,会c++的STL也不是不行,只不过我们可以模拟这些简单的数据结构来解决一些简单的问题,效率可能是要比STL更高的,毕竟STL由于功能很完善,因此STL如果用来简单一些简单的问题,其实是有点大材小用的;另一方面,可以帮助我们提前了解一些简单的常见的数据结构。
1.单向链表
用于邻接表(存储图和树)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=100010;
int e[N],ne[N],head,idx;
//e存储i个节点的值
//ne存储i个节点指向的下一个节点
//head头指针指向头节点,head也是一个指针和ne[i]的性质一样,存储的是头结点的地址(这里的地址就是数组下标)
//idx表示能插入值得位置
void init()//初始化,头指针指向-1,idx表示0号位置空余
{
head=-1;
idx=0;
}
void add_to_head(int x)
{
e[idx]=x;//idx位置上赋值
ne[idx]=head;//idx指向head指向的位置
head=idx;//head指向idx
idx++;//idx后移,得到新的空余位置
}
void add(int k,int x)//在k节点后面一个节点插入x
{
e[idx]=x;
ne[idx]=ne[k];
ne[k]=idx;
idx++;
}
void remove(int k)//将k点后面的那个节点删除
{
ne[k]=ne[ne[k]];
}
int main()
{
init();
return 0;
}
注意:!!!!!
由于我们的节点的编号是从0开始的,因此当让我们对第K个节点进行操作时,其实应该是对序号为k-1的节点进行操作,同时如果是要删除头节点,我们要进行特殊处理
if(!k)head=ne[head];
2.双向链表
不同于单向链表,每个节点除了知道自己后一个节点的地址,还知道自己前一个节点的位置
一般用于一些算法的优化
#include <iostream>
#include <cstring>
#include <algorithm>
#include <string>
using namespace std;
const int N=1000010;
int e[N],r[N],l[N],idx;
/*
e[N]存储的是节点的值
r[N]存储的是节点前一个节点的地址
l[N]存储的是节点下一个节点的地址
idx表示当前节点
*/
void init()
{
r[0]=1,l[1]=0;//作为边界的存在
idx=2;//节点从2号开始
}
void add(int k,int x)//在k点右边插入x
{
e[idx]=x;
r[idx]=r[k];
l[idx]=k;
l[r[k]]=idx;
r[k]=idx;
idx++;
}
/*
当我们要在k点的左边插入x 我们就调用add(l[k],x) 注意不能是add(k-1,x)因为k-1不一定是前一个节点的地址
*/
void remove(int k)//删除k点
{
r[l[k]]=r[k];
l[r[k]]=l[k];
}
3.栈
一种数据先进后出的数据结构
const int N=100010;
int stk[N],idx;//idx表示栈顶下标
void pop()//弹出
{
idx--;
}
void push(int x)//压入
{
stk[++idx]=x;
}
void if_empty()//判断是否为空
{
if(idx==0)
puts("YES");
else
puts("NO");
}
void que()//x
{
cout<<stk[idx]<<endl;
}
4.队列
一种数据先进先出的数据结构
// top 表示队头,tail表示队尾
int q[N], top = 0, tail = -1;//初始化是习惯问题
// 向队尾插入一个数
q[ ++ tail] = x;
// 从队头弹出一个数
top ++ ;
// 队头的值
q[top];
// 判断队列是否为空
if (top <= tail)
{
not empty
}
else
{
is empty
}
5.单调栈
用于解决:给定一个序列,求解每个数左边第一个比他小的数(或者其他性质)
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;//保证整个栈内的元素是递增/递减趋势
stk[ ++ tt] = i;
}
实列
#include<iostream>
using namespace std;
const int N=1000010;
int stack[N],top;
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
while(top&&stack[top]>=x)top--;
//如果栈顶元素比当前数值大,那么说明x后面的数的最近的最小值一定不是当前栈顶元素,因此弹出
if(top)cout<<stack[top]<<" ";//当前栈顶元素一定是离x最近的并且是比x小的数
else cout<<"-1 ";
stack[++top]=x;
}
return 0;
}
6.单调队列
滑动窗口,求一“窗口”中的最小值/最大值
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ; //保证队列单调递增/单调递减
q[ ++ tt] = i;
}
实例:滑动窗口
给定一个大小为 n ≤ 1 0 6 n \le 10^6 n≤106 的数组。
有一个大小为 k k k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k k k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为
[1 3 -1 -3 5 3 6 7]
, k k k 为 3 3 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 n n 和 k k k,分别代表数组长度和滑动窗口的长度。
第二行有 n n n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3 1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3 3 3 5 5 6 7
我们将找出窗口中的最大值和最小值分为两部分,一部分找出窗口最大的,一部分找出窗口最小的,将窗口想象成队列,不妨保证每次队列的头是最小值或最大值
以找队列最小值为例:我们发现,当队列里的一个值的后头,存在比这个值还小的值,那么这个值,就永远不可能成为窗口中的最小值,因此可以将这个值删掉,那么也就是说,我们需要保证整个队列的里面的值呈现递增的状态
同理,当我们要去找整个队列中的最大值,就需要保证整个队列呈现递减
注意:单调队列里面存的是数组的下标
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1000010;
int a[N],q[N],hh,tt=-1;
//原理:保证进入到队列的元素值具有单调性
int main()
{
int n,k;
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
for(int i=0;i<n;i++)
{
if(hh<=tt&&q[hh]<i-k+1) hh++;//窗口头元素出队列,q[hh]必须大于等于i-k+1,这样子q[hh]~i有k个数
while(hh<=tt&&a[q[tt]]>=a[i])tt--;//队尾不单调,弹出
q[++tt]=i;//下标进入队列
if(i>=k-1)//保证队列里面的数字个数有k个
printf("%d ",a[q[hh]]);//最开始队列里面的数字不满k个,那么就不必输出
}
puts("");
hh=0,tt=-1;
for(int i=0;i<n;i++)
{
if(hh<=tt&&q[hh]<i-k+1) hh++;//窗口头元素出队列
while(hh<=tt&&a[q[tt]]<=a[i])tt--;//队尾不单调,弹出
q[++tt]=i;//下标进入队列
if(i>=k+1)printf("%d ",a[q[hh]]);//特判一下,一开始窗口中的数字不足k个,就不需要输出了
}
return 0;
}
7.KMP算法
以 O ( N ) O(N) O(N)的时间复杂度在一个长串中匹配出一段子串
两个字符串p[N]
与q[M]
,q比p短
这里我们要为子串p
创建前缀数组ne[N]
,使得ne[i]=j
表示p[1 ~ j]和p[i-j+1 ~ i]
这两段字符串相等
这样当我们q[i]!=p[j+1]
的时候,j=ne[j]
就能保证刚刚匹配到的q[i-j+1~~i-1]与p[1~~j]
这两段字符串是匹配得上的,因此这个时候就可以继续比较q[i]和p[j+1]
,如果不相等,继续j=ne[j]
。这里的j=ne[j]
就是避免了有些时候指针j重新回到字符串p的开头重新匹配而浪费一些时间。
首先我们来模拟匹配的过程,先忽略ne[N]数组
的创建
for(int i=1,j=0;i<=m;i++)
{
while(j&&p[i]!=s[j+1])j=ne[j];//p[i]!=s[j+1] 指针j开始递归
if(p[i]==s[j+1])j++;// 继续匹配
if(j==n)//匹配成功
{
printf("%d ",i-n);
j=ne[j];//进行下一个子串的匹配
}
}
ne[N]数组
的创建:可以理解为p串和自己进行匹配操作
for(int i=2,j=0;i<=n;i++)
{
while(j&&s[i]!=s[j+1])j=ne[j];
if(s[i]==s[j+1])j++;
ne[i]=j;
}
完整代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=100010;
const int M=1000010;
char s[N],p[M];
int n,m,ne[N];
int main()
{
cin>>n>>s+1>>m>>p+1;
for(int i=2,j=0;i<=n;i++)
{
while(j&&s[i]!=s[j+1])j=ne[j];
if(s[i]==s[j+1])j++;
ne[i]=j;
}
for(int i=1,j=0;i<=m;i++)
{
while(j&&p[i]!=s[j+1])j=ne[j];
if(p[i]==s[j+1])j++;
if(j==n)
{
printf("%d ",i-n);
j=ne[j];
}
}
return 0;
}
注意:
- 两个字符串都是从1作为下标开始存储,只是为了方便,如果是从0开始,可能会遇到数组下标越界的情况
- ne数组的创建中,是从i=2开始匹配,j=0
- 匹配注意比较的是q[i]与p[j+1],这样做是因为j=ne[j],这里ne[j]表示匹配串下标从1到j的最长前后缀的长度,大概也是为了避免一些下标越界的事情发生,也挺混乱的,反正可以背代码,考的不多hhh