基础算法入门10——数据结构模拟1

  1. 模拟单向链表
  2. 模拟双向链表
  3. 模拟队列
  4. 模拟栈
  5. 单调栈
  6. 单调队列
  7. 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 n106 的数组。

有一个大小为 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-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 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

我们将找出窗口中的最大值和最小值分为两部分,一部分找出窗口最大的,一部分找出窗口最小的,将窗口想象成队列,不妨保证每次队列的头是最小值或最大值

image-20220420223349313

以找队列最小值为例:我们发现,当队列里的一个值的后头,存在比这个值还小的值,那么这个值,就永远不可能成为窗口中的最小值,因此可以将这个值删掉,那么也就是说,我们需要保证整个队列的里面的值呈现递增的状态

同理,当我们要去找整个队列中的最大值,就需要保证整个队列呈现递减

注意:单调队列里面存的是数组的下标

#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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值