[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法

单/双 链表,栈/单调栈,队列/单调队列

  • 单链表
  • 双链表
  • 队列
  • 单调栈
  • 滑动窗口,单调队列
  • KMP算法

问题来源 : ACWing
https://www.acwing.com/blog/content/277/


为什么要使用数组来模拟实现这些数据结构?

可能存在一种情况:以单链表为例,题目要求完成一个链表的操作,但是他的数据范围很大,也就是要完成很多节点的插入操作,那么这时候new大量的节点就会有超时的风险,所以说就需要使用数组来模拟实现一个静态的数据结构

但是,这些静态的数据结构知识实现的方式不同,在一些性能方面,还是和链式结构是相同的。比如说,中间插入一个元素,还是要先找到前一个元素,这样的时间复杂度还是O(n)

单链表

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第1张图片
还是要注意一点,这里的中间插入和删除,他并不是对链表抽象后的数据中第K个,而是我们插入顺序中,第K个插入的节点,所以说使用数组才可以O(1)

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第2张图片

#include 

using namespace std;

const int N = 1e5 + 10;

// head 头结点的下标
// e[i] 表示 i 号节点的值
// ne[i] 表示 i 号节点的下一个节点的下标
int head,e[N],ne[N];
int idx;    // 当前以经使用的节点数
int n;

void Init() {
    head = -1;
    idx = 0;
}

void push_front(const int x) {
    int p = idx++;
    e[p] = x;
    ne[p] = head;
    head = p;
}

// 中间插
void insert(const int k,const int x) {
    if(k >= idx) return ;
    
    int p = idx++;
    e[p] = x;
    ne[p] = ne[k];
    ne[k] = p;
}

// 删除
void erase(const int k) {
    if(k == -1) {               // 删除头结点
        head = ne[head];
    } else {
        ne[k] = ne[ne[k]];      // 删除中间节点
    }
}

int main() {
    Init();
    
    cin >> n;
    char oper;
    int k,x;
    for(int i = 0; i < n; i++) {
        cin >> oper;
        if(oper == 'H') {
            cin >> x;
            push_front(x);          // 头插
        } else if(oper == 'D') {
            cin >> k;
            erase(k - 1);           // 删除
        } else if(oper == 'I') {
            cin >> k >> x;
            insert(k - 1,x);        // 中间插入
        }
       
    }
    
    int p = head;
    while(p != -1) {
        cout << e[p] << " ";
        p = ne[p];
    }
    return 0;
}

双链表

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第3张图片
跟我们正常链表的思维差不多,维护一个pre数组,表示当前节点的前一个节点;再维护一个ne数组,表示当前节点的下一个节点。

然后依次实现头插尾插中间删,以及中间插入。(头插和尾插也可以使用中间插入转换一下),然后实现一个带头结点的双向循环链表
[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第4张图片
因为在这里,我们使用头结点做了一个哨兵位,将0号下标的位置占据了,所以第K次插入的数据即为K下标的数,也就是Idx == K

#include 
#include 

using namespace std;

const int N = 1e5 + 10;

int head,e[N],pre[N],ne[N],idx;

void init() {
    head = idx, e[idx] = -1,pre[idx] = idx,ne[idx] = idx, idx++;
}

// 头插
void push_front(const int x) {
    e[idx] = x, pre[ne[head]] = idx, ne[idx] = ne[head], pre[idx] = head, ne[head] = idx++;
}

// 尾插
void push_back(const int x) {
    e[idx] = x, ne[pre[head]] = idx, pre[idx] = pre[head], ne[idx] = head, pre[head] = idx++;
}

// 中间删
void erase(const int k) {
    pre[ne[k]] = pre[k], ne[pre[k]] = ne[k];
}

// 第 k 个数的后面插入 x
void insert(const int k,const int x) {
    e[idx] = x, pre[ne[k]] = idx, ne[idx] = ne[k], pre[idx] = k, ne[k] = idx++;
}

int main() {
    int n;
    cin >> n;
    init();
    
    while(n --) {
        string str;
        cin >> str;
        
        int k,x;
        if(str == "R") {
            cin >> x;
            push_back(x);
        } else if(str == "D") {
            cin >> k;
            erase(k);
        } else if(str == "L") {
            cin >> x;
            push_front(x);
        } else if(str == "IL") {
            cin >> k >> x;
            insert(pre[k],x);
        } else if(str == "IR") {
            cin >> k >> x;
            insert(k,x);
        }
    }
    
    for(int i = ne[head]; i != head; i = ne[i]) cout << e[i] << " ";
    return 0;
}

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第5张图片

栈的特点:后进先出

#include 
#include 

using namespace std;

const int N = 1e5 + 10;
int f[N],top;

void push(const int x) {
    f[top++] = x;
}

void pop() {
    top--;
}

int query() {
   return f[top - 1]; 
}

string empty() {
    return top == 0 ? "YES" : "NO";
}

int main() {
    int n;
    cin >> n;
    int x;
    while(n --) {
        string str;
        cin >> str;
        if(str == "push") {
          cin >> x;
          push(x);
        } else if(str == "pop") {
            pop();
        } else if(str == "query") {
            cout << query() << endl;
        } else if(str == "empty") {
            cout << empty() << endl;
        }
    }
    
    return 0;
}

队列

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第6张图片

队列的特点:先进先出

#include 
#include 

using namespace std;

const int N = 1e5 + 10;
int f[N],front,tail;

void push(const int x) {
    f[tail++] = x;
}

void pop() {
    front++;
}

int query() {
   return f[front]; 
}

string empty() {
    return front == tail ? "YES" : "NO";
}

int main() {
    int n;
    cin >> n;
    int x;
    while(n --) {
        string str;
        cin >> str;
        
        if(str == "push") {
          cin >> x;
          push(x);
        } else if(str == "pop") {
            pop();
        } else if(str == "query") {
            cout << query() << endl;
        } else if(str == "empty") {
            cout << empty() << endl;
        }
    }
    
    return 0;
}

单调栈

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第7张图片
大体思路就是,当插入一个数的时候,他肯定存在两种情况:

  1. 在这个数之前,存在比自己小的数,那么结果肯定是这个第一个比自己小的数的大小
  2. 没有比自己小的数,那么结果就是-1

最朴素的做法,就是每次插入一个数的时候,从后向前暴力搜索前面所有的情况,找到第一个符合的数,然后退出。
[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第8张图片
那么可以思考一个问题,当我们插入一个数的时候,如果在左边第一个比自己小的数中间的区间中,这些比自己大的数还可能被使用吗?
[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第9张图片

#include 
#include 

using namespace std;

int main() {
    stack<int> st;
    int n;
    cin >> n;
    
    while(n --) {
        int num;
        cin >> num;
        
        int ans = -1;
        while(!st.empty()) {
            int t = st.top();
            if(t >= num) st.pop();      // 栈顶元素比当前元素大,进行压缩
            else {
                ans = t;                // 栈顶元素即为第一个比自己小的,返回结果
                break;
            }
        }
        st.push(num);
        cout << ans << " ";
    }
    
    return 0;
}

滑动窗口,单调队列

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第10张图片
整体的思想就是维护一个K大小的窗口,然后每次求这个窗口中的最大值,或者最小值。
[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第11张图片

在朴素的做法中,可以每次遍历K个窗口中的每个数,求出这样的一个最大值或者最小值。但是不妨思考下,这样的时间复杂度为O(n * k),这在本题中 n1e6,而 k > 0 && k < n来说是很恐怖的一个时间复杂度了。

本题是一个基于滑动窗口的单调队列问题,也是一个经典题。
思路就是,每次在队列中维护一个最长为K区间的窗口,每一次确保窗口的最小值或者最大值在窗口的左侧,也就是队头位置。

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第12张图片
那么对于这组数据,对于求最大值和求最小值,我们就可以对原本K大小的固定窗口进行一次压缩,确保窗口的开始位置一定是当前窗口最值的一个下标

以求当前窗口的最大值为例:

  1. 当我们求最大值的时候,我们得确保插入一个数后,当前窗口的大小为[1 - k]
  2. 插入一个数,得先去掉前面比自己小的数。(这里可以想象为一个大饼,把他在后面一放,就会把前面比自己小的饼都压碎)
  3. 实际上,因为当前窗口都是合法大小的,我们插入一个新的数据后,可能会去掉一些元素,可以细想一下,从这些元素到当前位置的这个区间M中,一定符合这个M <= K,那么这个M区间中的最大值就是新插入的这个数。
#include 

using namespace std;

const int N = 1e6 + 10;
int f[N],q[N];
int n,k;

int main() {
    scanf("%d%d",&n,&k);
    for(int i = 0; i < n; i++) scanf("%d",&f[i]);
    
    int hh = 0, tt = -1;
    
    // 当前窗口最小值
    for(int i = 0; i < n; i++) {
        // 判断队列中是否有 k 个元素
        if(hh <= tt && i - k + 1 > q[hh]) hh++;
        // 压缩掉前面比自己大的数
        while(hh <= tt && f[q[tt]] >= f[i]) tt--;
        
        q[++tt] = i;
        if(i >= k - 1) printf("%d ",f[q[hh]]);
    }
    
    printf("\n");
    // 当前窗口最大值
    hh = 0, tt = -1;
    for(int i = 0; i < n; i++) {
        if(hh <= tt && i - k + 1 > q[hh]) hh++;
        while(hh <= tt && f[q[tt]] <= f[i]) tt--;
        q[++tt] = i;
        if(i >= k - 1) printf("%d ",f[q[hh]]);
    }
    
    return 0;
}

KMP算法

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第13张图片

在朴素的做法中,一般都是这样想的

for(int i = 1; i <= n; i++) {
	bool flag = true;
	int le = i;
	for(int j = 1; j <= m; j++) {
		if(le >= n) flag = false,break;
		if(s[le++] != t[j]) flag = false,break;
	}
	if(flag == true) return true;
}
return false;

但是由于每一次的不匹配,都只会对原串向后跳转1位,然后继续匹配待匹配的字符串,这样的时间复杂度为O(n * m),很高了。

KMP算法的设计思想:

[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第14张图片
以每一个点为终点的后缀,和那个点的前缀相等,只有这个相等的长度最大,就可以确保每次跳跃的位置越多,重复判断的长度越少。
[AcWing],单/双链表,栈/单调栈,队列/单调队列的数组模拟,字符串匹配问题的KMP算法_第15张图片

#include 

using namespace std;

const int N = 1e5 + 10, M = 1e6 + 10;

int n,m;
char p[N],s[M];
int ne[N];

int main() {
    cin >> n >> p + 1 >> m >> s + 1;    // 字符串下标从1开始
    
    // 构造next数组的过程
    // ne[1] = 0
    for(int i = 2, j = 0; i <= n; i++) {
        while(j && p[i] != p[j + 1]) j = ne[j];
        if(p[i] == p[j + 1]) j++;
        ne[i] = j;
    }
    
    // 匹配字符串的过程
    for(int i = 1, j = 0; i <= m; i++) {
        while(j && s[i] != p[j + 1]) j = ne[j];
        if(s[i] == p[j + 1]) j++;
        if(j == n) {
            // 匹配成功
            printf("%d ",i - n);
            j = ne[j];
        }
    }
    return 0;
}

你可能感兴趣的:(题解,队列,链表,字符串,KMP)