数据结构/算法笔记(2)-数组模拟链表,栈和队列 & KMP算法

静态链表

  • 单链表
int e[N], ne[N], head, idx;	// e[i]存结点数据,en[i]存下个结点的坐标
//初始化
void init(){
    head = -1;	//head 是当前首个存数据的结点坐标。设 NULL 为 -1
    idx = 0;	//idx 从0起,按创建顺序记录最后一个坐标
}	//切记:下标即坐标,与链表顺序无关
void add_to_head(int x){
    
}
void add(int k, int x){
    
}	//默认向 k 点的后面插,O(1)。若从前面插就只能遍历,O(n)。注意 k ≥ 0
void remove(int k){
    
}	//内存泄漏,所以仅限算法竞赛

静态链表用空间换时间,动态链表用时间换空间,各有所长。

静态链表的结点地址是固定的,所以不需要结构体来绑定数据

原理基本相同,函数体略

  • 双向链表
int e[N], l[N], r[N], idx;
void init(){
    r[0] = 1, l[1] = 0;
    idx = 2;	//没有头结点,初始化时定义两个起始结点
}
//函数思路和单链表相同,但 en[i] 变为 l[i] 和 r[i],函数写其中一个方向就够了,略

链接数据只需要一个方向。另一个方向是方便双向读取


栈和队列

栈和队列,但是用数组模拟(梅开二度)

//栈,只有一端,先进后出
int stk[N], tt;	//补充一下,全局变量默认为0
cin >> stk[tt++];	//入栈
cout << stk[tt], tt--;	//出栈

//队列,有两端,先进先出
int queue[N], hh, tt;
cin >> stk[tt++];	//入列
cout << stk[hh], hh++;	//出列

栈例:输入长度 n 的数列,输出每个数左边第一个小于他的数,若没有,输出-1

for(i = 0; i < n; i++)
{
    scanf("%d", &x);
    if(tt && stk[tt] >= x) tt--;	//由题知,连续输出是升序列,升序片段起点是-1
    if(tt) printf("%d ", stk[tt]);
    else printf("-1 ");
    stk[tt+] = x;	//时间O(n)
}

队例:输入长度 n 的数列,输出所有长度为 k 的滑动窗口中最小值

//分析:滑动窗口是一个队列,优化方式和栈一样,屏蔽掉没用的数,但队列还要右进左出
int q[N];	//hh 和 tt 保存的值为窗口中有效序列的首尾
for(i = 0; i < n; i++) scanf("%d", &a[i]);

for(i = 0; i < n; i++){
    if(hh <= tt && q[hh] < i - k + 1) hh++;	//窗口移动时 hh 匹配下一个有效队头
    
    while(hh <= tt && a[q[tt]] > a[i]) tt--;	//新数不满足升序,踢出旧数
    q[tt++] = i;	//加入新数,暂时满足升序
    
    if(i >= k - 1) printf("%d", a[q[hh]]);	//窗口完整,则开始输出
}

栈和队列按出入方式区分,利用其特点解决相应问题。

先用暴力思路解决,利用数据性质去除无效数据,就可以显著优化算法。


KMP 算法

先举个栗子:有数组 a[N] 和 p[M],N>M,现在要判断在 a 中能否找到与 p 相同的一段。

于是有朴素算法:从 i = 1 开始遍历枚举,遇到不同元素就 break,我们只看break前的部分,可以发现:

设 p 右移后再次匹配 a ,且与原序列有交集,说明 i 前的部分满足首尾有相同序列。

于是可以认为解决问题的门路是,找到短数组中最长的首尾相同序列。——KMP算法

  • (知识点参考——王道论坛)

  • 初步优化:在朴素算法基础上,不必重置 i ,遇到不匹配的项,只重置 j 即可

  • 二次优化:j 不一定要回到起点,若 j 前面有首位相同序列,优先回到首位序列的下一位进行判定。出于这种考虑,新增一个 next[] 数组记录 j 可以跳到的位置,下标从1开始,记录每一项匹配失败后能回到最靠后的下标。

计算next数组:

从图形角度:数组相对自己的复制体整体右移,使前后的相同序列对齐并继续判定,复制体上的 j 不动,所以 j 相对数组是左移,此时 j 项对齐的数组本体位置就是next[j] 。由于如果第一项就不匹配,则直接右移一步,再将 i i i j j j 自增,匹配下一项,于是有 next[j] = 0

从算法角度:利用双指针,其中 i i i 遍历数组, j j j 对每个 i i i 寻找 next[i] ,其实就是对自己KMP

int i = 1, j = 0;	//i 和 j 错开一格,是为了保证真前缀和真后缀,仅限p对自己KMP找next
while(i < M){
	if(j == 0 || p[i] == p[j]){
    	++i, ++j;	//j 为每个 i 扫描相同序列,匹配就假设下一个失配然后记录 next
    	next[i] = j;	//考研的时候手算,即为最大相同序列长度+1
	}	
    else j = next[j];	//i j 前面是一样的,所以退回 next,自然 next 为 0 时才会重置
}

三次优化:有时候,next可能会退回前一个相同字符的位置,导致增加了无意义的判定,因此

int nextval[n];	nextval[1] = 0;
//在求出一个点的next值后追加
if(p[next[j]] == p[j]) 	nextval[j] = nextval[next[j]];
else 	nextval[j] = next[j];	//于是每一位的next都能一次归位

p[M] 对自己KMP,就可以找到自己在数组匹配中的移动方式,从而和 a[N] 进行KMP

get_next(p, nextval);	//上面那个KMP封成一个函数,参数为2个数组
int i = 1, j = 1;
while(i <= n && j <= m){
    if(j == 0 || a[i] == p[j]){
        ++i; ++j;
    }else{
        j = next[j];
    }
}
if(j > n) return i - n;	//利用KMP,完美找到 a 中的 p,返回起点
else return 0;	//a 中没有匹配的 p
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值