2.1数据结构 | 数组模拟链表、单调栈、单调队列、kmp算法

2.1 数据结构(一)

这是我的一个算法网课学习记录,道阻且长,好好努力

2.1.1 链表与邻接表:树与图的存储

链表 是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

就像一条 火车 ,里面的数据就是我们的车厢,而指针就是将数据链接起来的链条,所以我们称之为链表。

实现可以使用结构体或者数组,数组的优势在于运行时间快

  • 数组模拟单链表

    实现一个单链表,链表初始为空,支持三种操作:

    1. 向链表头插入一个数;

    2. 删除第 k 个插入的数后面的数;

    3. 在第 k 个插入的数后插入一个数。

    注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

    #include 
    
    using namespace std;
    
    const int N = 100010;
    
    // head 表示头节点的下标
    // e[i] 表示节点i的值
    // ne[i] 表示节点i的next指针是多少
    // idx 存储当前节点已经用到了哪个点
    
    int head, e[N], ne[N], idx;
    
    // 初始化
    void init()
    {
        head = -1; //初始化head需要为-1 否则会超时
        idx = 0;
    }
    
    // 将x插到头节点的位置,即head指向x
    void add_to_head(int x)
    {
        e[idx] = x;
        ne[idx] = head;
        head = idx ++ ;
    }
    
    // 将x插入到下标为k的节点之后
    void add(int k, int x)
    {
        e[idx] = x;
        ne[idx] = ne[k];
        ne[k] = idx ++ ;
    }
    
    // 删除下标为k的点之后的那个点
    void remove(int k)
    {
        ne[k] = ne[ne[k]];
    }
    
    int main()
    {
        int m;
        cin >> m;
    
        init(); //注意初始化
    
        while (m -- )
        {
            int k, x;
            char op;
            cin >> op;
    
            if (op == 'H')
            {
                cin >> x;
                add_to_head(x);
            }
            else if (op == 'D') {
                cin >> k;
                if (!k) head = ne[head];
                else remove(k - 1);
            }
            else
            {
                cin >> k >> x;
                add(k - 1, x);
            }
        }
        for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
        cout << endl;
    
        return 0;
    }
    
  • 数组模拟双链表

    实现一个双链表,双链表初始为空,支持5 种操作:

    1. 在最左侧插入一个数;
    2. 在最右侧插入一个数;
    3. 将第 k 个插入的数删除;
    4. 在第 k 个插入的数左侧插入一个数;
    5. 在第 k 个插入的数右侧插入一个数
    #include 
    
    using namespace std;
    
    const int N = 100010;
    
    int e[N], l[N], r[N], idx;
    
    // 初始化
    void init() // 注意观察初始化的数值
    {
        r[0] = 1, l[1] = 0, idx = 2;
    }
    
    // 在下标为k的元素右边,插入x
    void add(int k, int x) //
    {
        e[idx] = x;
        r[idx] = r[k];
        l[idx] = k;
        l[r[k]] = idx;
        r[k] = idx ++ ;
    }
    
    // 删除下标为k的元素
    void remove(int k)
    {
        r[l[k]] = r[k];
        l[r[k]] = l[k];
    }
    
    int main()
    {
        int m;
        cin >> m;
    
        init();
    
        while (m -- )
        {
            string op;
            cin >> op;
    
            int k, x;
    
            if (op == "L") // 在链表最左端插入数x
            {
                cin >> x;
                add(0, x);
            }
            else if (op == "R") // 在链表最右端插入数x
            {
                cin >> x;
                add(l[1], x);
            }
            else if (op == "D") // 将第k个插入的数删除
            {
                cin >> k;
                remove(k + 1);
            }
            else if (op == "IL") // 在第k个插入的数左侧插入一个数
            {
                cin >> k >> x;
                add(l[k + 1], x);
            }
            else if (op == "IR") // 在第k个插入的数右侧插入一个数
            {
                cin >> k >> x;
                add(k + 1, x);
            }
        }
    
        for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << " ";
        cout << endl;
    
        return 0;
    }
    

2.1.2 栈与队列:单调队列、单调栈

先进后出(LIFO),队列先进先出(FIFO)。

  • 模拟栈

​ 实现一个栈,栈初始为空,支持四种操作:

  1. push x – 向栈顶插入一个数 x;
  2. pop – 从栈顶弹出一个数;
  3. empty – 判断栈是否为空;
  4. query – 查询栈顶元素。
#include 

using namespace std;

const int N = 100010;

int stk[N], tt; // stk[N]为数组模拟栈,tt为指针指向栈顶元素

int main()
{
    int m; 
    cin >> m;
    
    while (m -- ) 
    {
        string op;
        cin >> op;
        
        // 插入
        if (op == "push")
        {
            int x;
            cin >> x;
            stk[ ++ tt] = x;
        }
        // 弹出
        else if (op == "pop") tt --;
        // 判断是否为空
        else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;
        // 输出栈顶元素
        else cout << stk[tt] << endl;
    }
    
    return 0;
}
  • 模拟队列

​ 实现一个队列,队列初始为空,支持四种操作:

  1. push x – 向队尾插入一个数 x;

  2. pop – 从队头弹出一个数;

  3. empty – 判断队列是否为空;

  4. query – 查询队头元素。

#include 

using namespace std;

const int N = 100010;

int q[N], hh, tt = -1;

int main()
{
  int m;
  cin >> m;

  while (m -- )
  {
      string op;
      cin >> op;
  	
      // 在队尾插入元素
      if (op == "push")
      {
          int x;
          cin >> x;
          q[ ++ tt] = x;
      }
      // 弹出队头元素
      else if (op == "pop") hh ++ ;
      // 判断是否为空
      else if (op == "empty") cout << (hh <= tt ? "NO" : "YES") << endl;
      // 输出队头元素
      else cout << q[hh] << endl;
  }

  return 0;
}
  • 单调栈

​ 给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

#include 

using namespace std;

const int N = 100010;

int stk[N], tt;

int main()
{
    int m;
    cin >> m;

    while (m -- )
    {
        int x;
        cin >> x;

        while (tt && stk[tt] >= x) tt -- ;

        if (!tt) cout << "-1 ";
        else cout << stk[tt] << " ";

        stk[ ++ tt] = x;
    }

    return 0;
}

所谓单调栈,其实就是为了输出每个数左边最近的比它小的数字,而利用的工具。我们对于数列中的每一个数在进栈之前先进行一个判断:如果栈为空,则其左边没有数字,更谈不上找比x小的数;如果栈非空且栈顶元素大于等于x ,则弹出栈顶元素(因为由于x的存在,栈顶元素将永远不会被输出,留在栈中反而碍事)。通过不断弹出比x大的栈顶元素,使得while循环结束后,栈顶元素一定小于x。这样操作得到的栈可以一直保持元素单调递增,故称单调栈

  • 单调队列

    例题:滑动窗口

    给定一个大小为 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

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

#include 

using namespace std;

const int N = 1000010;

int n;
int a[N], q[N];

int main()
{
    int k;
    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 ++;
        // hh <= tt 判断队列是否为空
        // 保证队列中的元素始终是窗口中元素的子集 (队列里滑出窗口的元素要出队)

        while (hh <= tt && a[q[tt]] >= a[i]) tt --;
        // 保证队列单调递增 队头元素为窗口中元素的最小值

        q[ ++ tt] = i;
        // q[N]数组 存储数列元素的下标

        if (i >= k - 1) printf("%d ", a[q[hh]]);
        // 当 i >= k - 1 时,窗口被元素填满,输出队头元素
    }
    puts("");

    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("");

    return 0;
}

算法在没有优化之前时间复杂度是O(kn)的,优化后是O(n)的。

对于这一题,单调队列的操作实际上是指不断的滑动这个窗口,[am, am+1, …],在这个过程(对于输出最小值的情况)中会有这样一些情况:

首先,读取的下一个元素a[i],若该元素大于队中元素,则进队;若小于或者等于队中某元素,则该元素出队,a[i]元素进队。

以及,当队头元素的下标超过 i - k + 1时,即队列元素滑出窗口是,需要对队头指针 hh 进行递增。

2.1.3 kmp

例题:kmp字符串

给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串 P 在模式串 S 中多次作为子串出现。

求出模板串 P 在模式串 S 中所有出现的位置的起始下标。

#include 

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
char p[N], s[M];
int ne[N]; // next数组 避免与c++保留字冲突 写成ne

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;

    //求next数组
    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; 
    }

    // 进行kmp匹配
    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;
}

当已经知道之前遍历过的字符,利用这些信息避免暴力算法中回退(backup)的步骤。也就是说 i 指针不发生回退,而是一直向前移动。

将字串移动到已经匹配的位置,继续向后匹配。如果匹配,继续向前;如果不匹配,继续回跳。借助next数组实现了这一操作。

暴力算法的时间复杂度是O(nm)的,而kmp算法是线性时间复杂度O(n)的。

你可能感兴趣的:(数据结构与算法_基础学习,数据结构,链表,算法)