C++ 算法基础课 04 —— 数据结构_单链表/双链表/栈/单调栈/队列/单调队列(滑动窗口)/KMP

1 单链表(用数组模拟链表)

  • 单链表用的最多的是存储图和树
  • e[N]表示当前结点的值,ne[N]表示指向下一个结点。都是整数型数组,空结点的下标用-1表示
  • ne[N] = N + 1记录指向下一个结点
    在这里插入图片描述
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 在链表头插入一个数a
void insert(int a)
{
    e[idx] = a, ne[idx] = head, head = idx ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}

1.1 模板

  • ne[ ]存储节点的next指针,就是ne[k]里面存的是k+1,就是ne[k] = k + 1
#include<iostream>
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;
    idx = 0;
}

// 将x值插到头结点(头插法)
void add_to_head(int x)
{
	// head 表示头结点的下标
    e[idx] = x;// 将x的值存下来
    ne[idx] = head;// 将红色的指针1指向之前head存的,相当于ne[idx] ——> head
    head = idx;// 将head指向红色指针2,相当于 head ——> idx
    idx ++;
}

// 将x插到下标k的点后面
void add(int k, int x)// idx就是插入的点,ne[x]存的是x点下一点坐标值
{
	e[idx] = x;// 先存下x值
	ne[idx] = ne[k];// 红颜色的指针1指向k的下一个点(k点就是点2,k的下一个点是点3)
	ne[k] = idx;// 点k指向idx,就是插入的点
	idx ++;
}

// 将下标为k的点后面的点删除,比如k=1就是要删除2号点
void remove(int k)
{
	ne[k] = ne[ne[k]];// 就是说将下下个指针赋给k的下一个指针,跳过第k+1个点
}

1.1.1 头部插入元素模板

  • 头部插入元素
    在这里插入图片描述
// 将x值插到头结点(头插法)
void add_to_head(int x)// idx表示插入点的下标值,插入的第1个点idx = 0
{
	// head 表示头结点的下标,存储链表头,head的存储的值时0,head——>0
	// ne[ ]存储节点的next指针,就是ne[k]里面存的是k+1,就是ne[k] = k + 1
    e[idx] = x;// 将x的值存下来
    ne[idx] = head;// 红色指针1指向之前head存的,之前是head指向节点0,现在换成ne[idx]指向节点0, 这里的ne[idx]存储的就是idx+1
    head = idx;// 将head指向红色指针2,相当于 head ——> idx,idx = 0
    idx ++;
}

1.1.2 第k个位置插入元素模板

  • 第 k 个位置插入元素
    在这里插入图片描述
// 将x插到下标k的点后面,k是从0开始的
void add(int k, int x)// idx就是插入的点,ne[x]存的是x点下一点坐标值
{
	// k=2,表示将点插入到点2的后面,原来ne[2] = 3,现在ne[idx] = 3,ne[2] = idx
	e[idx] = x;// 先存下x值
	ne[idx] = ne[k];// 红颜色的指针1指向k的下一个点(k点就是点2,k的下一个点是点3)
	ne[k] = idx;// 点k指向idx,就是插入的点, 2——>idx
	idx ++;
}

1.1.3 删除元素模板

在这里插入图片描述

// 将下标为k的点后面的点删除,比如k = 1就是要删除点2
void remove(int k)
{
	ne[k] = ne[ne[k]];// 就是说将下下个指针赋给k的下一个指针,跳过第k+1个点
}

1.2 习题1 —— 826.单链表

Acwing 826.单链表

  • 刚开始头结点 head 指向空,第1个点插入点的下标为0,…,第 k 个插入点的下标为 k-1,就是idx的值(idx从0开始)
    在这里插入图片描述
  • H 9:插入第1个点,下标 idx 为 0,值 e 为 9
    在这里插入图片描述
  • I 1 1:第1个点后面插入新的点,即第2个插入的点,下标 idx = 1,值 e 为 1
    在这里插入图片描述
  • D 1:删除第1个插入的点的下一个点,就是下标 idx = 1, 值 e 为 1 的点(这张图有问题,应该删除第2个点),保留下标为0,e = 9的点
    在这里插入图片描述
  • D 0:删除头结点,就是将整个链表删除
  • H 6:插入头结点,值 e 为 6,idx = 2;
  • I 3 6:第3个插入的点(idx = 2)后面插入点,该点 idx = 3,值为 6
  • I 4 5:第4个插入的点(idx = 3)后面插入点,该点 idx = 4,值为 5(第1个插入的点,idx = 0)
  • I 4 5:第4个插入的点(idx = 3)后面插入点,该点 idx = 5,值为 5
  • I 3 4:第3个插入的点(idx = 2)后面插入点,该点 idx = 6,值为 4
    在这里插入图片描述
  • D 6:删除第6个插入的点的下一个点,就是 idx = 5的点的下一个点,即idx = 4的点(所以这张图也是有问题的,应该删除idx = 4的点)
    在这里插入图片描述
#include<iostream>
using namespace std;

const int N = 100010;
// k 表示当前点是第k个插入的点, 从1开始算(由题意可知输入进来的值),
// 代入进来的时候需要 k - 1, 这样也可以从0开始算,第k个插入的点下标就是k-1
// idx 表示新插入的点的下标, 从0开始算

// head表示头结点的下标
// e[i]表示节点i的值
// ne[i]表示节点i的next指针是多少,就是该点的指针指向
// idx存储当前已经用到了哪个点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;// 刚开始head指向空集
    idx = 0;
}

// 将x值插到头结点(头插法)
void add_to_head(int x)
{
	// head 表示头结点的下标
    e[idx] = x;// 将x的值存下来。刚开始插入点,e[0] = x,就是将x值存在e[0]
    ne[idx] = head;// 将红色的指针1指向之前head存的,相当于ne[idx] ——> head。刚开始插入点,ne[0] = -1,第1个点指向空集
    head = idx;// 将head指向红色指针2,相当于 head ——> idx。刚开始插入的第1个点,head = 0
    idx ++;
}

// 将x插到第k个插入的点的后面(和模板不一样,模板的k是第k个位置,这里的k表示第k个插入的点,就是第k-1的位置)
void add(int k, int x)// idx就是插入的点,ne[x]存的是x点下一点坐标值
{
	e[idx] = x;// 先存下x值
	ne[idx] = ne[k];// 红颜色的指针1指向k的下一个点(k点就是点2,k的下一个点是点3), ne[k] = 2,通过赋值操作,nex[idx] = 2(点3),下标为2,表示第3个插入的点
	ne[k] = idx;// 点k指向idx,就是插入的点
	idx ++;
}

// 将下标为k的点后面的点删除,比如k = 1就是要删除k = 2点
void remove(int k)
{
	ne[k] = ne[ne[k]];// 就是说将下下个指针赋给k的下一个指针,跳过第k+1个点
}

int main()
{
    int m;
    cin >> m;// 一共要进行m次操作
    
    init();// 需要进行初始化
    
    while(m--)
    {
        int k, x;
        char op;
        
        cin >> op;
        if(op == 'H')// 头部插入点的操作
        {
            cin >> x;
            add_to_head(x);
        }
        else if(op == 'D')// 进行删除第k点后面的点的操作
        {
        	// k 表示当前点是第k个插入的点, 从1开始算,第k个插入的点下标就是k-1,算法都是按照下标进行的
            cin >> k;
            if(!k) head = ne[head];// 如果k = 0, 则head指向下下个指针
            remove(k - 1);
        }
        else
        {
            cin >> k >> x;
            add(k - 1, x);
        }
    }
    for(int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';// ne[i]就是不断向前走一步,ne[i]存的是下一个点的下标
    cout << endl;
}

2 双链表

2.1 模板

  • 下标0是最左边的点,下标1是最右边的点
  • 0是左端点,1是右端点,0号点右边是1号点,1号点的左边是0号点
    在这里插入图片描述
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点
    l[1] = 0, r[0] = 1;// 1号点的左边是0号点,0号点右边是1号点
    idx = 2;// 0和1已经被占用了,idx从2开始
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

2.1.1 插入操作

在这里插入图片描述

  • 必须先修改 l[r[k]] = idx 才能进行 l[k] = idx
    在这里插入图片描述
// 先进行初始化
void init()
{
    //0是左端点,1是右端点
    l[1] = 0, r[0] = 1;// 1号点的左边是0号点,0号点右边是1号点
    idx = 2;// 0和1已经被占用了,idx从2开始
}

// 在下标是k的点的右边插入x。如果是在k的左边插入x,则直接调用add(l[k], x)
void add(int k, int x)
{
    e[idx] = x;
    r[idx] = r[k];// k指针的右边赋给idx的右边(右边红色箭头)
    l[idx] = k;// idx指针的左边指向k(左边红色箭头)
    l[r[k]] = idx;// k右边指针再指向idx(必须先进行右边绿色箭头)
    l[k] = idx;// k的左边指向idx(再进行左边绿色箭头)
    idx ++;
}

2.1.2 删除操作

  • 直接将点的左边等于右边,点的右边等于左边
    在这里插入图片描述
// 删除第k个结点
void remove(int k)
{
    r[l[k]] = r[k];// k左边的右边直接等于k的右边
    l[r[k]] = l[k];// k右边的左边直接等于k的左边
}

2.2 习题1 —— 827.双链表

Acwing 827.双链表

#include<iostream>
using namespace std;

const int N = 100010;

int m;
int e[N], l[N], r[N], idx;// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点

// 初始化
void init()
{
    //0是左端点,1是右端点
    l[1] = 0, r[0] = 1;// 1号点的左边是0号点,0号点右边是1号点
    idx = 2;// 0和1已经被占用了,idx从2开始
}

// 在下标是k的点的右边插入x
void add(int k, int x)
{
    e[idx] = x;
    r[idx] = r[k];// k指针的右边赋给idx的右边
    l[idx] = k;// idx指针的左边指向k
    l[r[k]] = idx;// k右边指针再指向idx
    l[k] = idx;// k的左边指向idx
}

// 删除第k个结点
void remove(int k)
{
    r[l[k]] = r[k];// k左边的右边直接等于k的右边
    l[r[k]] = l[k];// k右边的左边直接等于k的左边
}

3 栈(用数组模拟栈)

  • 栈先进后出,队列先进先出

3.1 模板

// tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空
if(tt > 0) not empty;
else empty;
  • 上课代码
#include<iostream>
using namespace std;
const int N = 100010;

int stk[N], tt;// stk[N]表示栈,tt表示栈顶,默认tt初始值为0

// 栈顶插入x
stk[++ tt] = x;

// 栈顶弹出x
tt--;// 栈顶下标--即可

// 判断栈是否为空
if(tt > 0) not empty;
else empty;

// 栈顶
stk[tt];

3.2 习题1 —— 828.模拟栈

Acwing 828.模拟栈

4 单调栈

  • 双指针暴力做法,找到距离i最近的比i小的值,j是从i-1的位置开始找起,即找到i的左边第一个比i小的数(离i最近的小的数)
    在这里插入图片描述

4.1 模板

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

4.2 习题1 —— 830.单调栈

Acwing 830.单调栈

#include<iostream>
using namespace std;

const int N = 100010;
int stk[N], tt = 0;// tt默认初始值为0

int main()
{
	cin.tie(0);
	ios::sync_with_stdio(false);// 提高速度
    int n;// 读入第1行的数
    cin >> n;
    
    for(int i = 0; i < n; i++)
    {
        int x;
        cin >> x;
        while(tt && stk[tt] >= x) tt--;// 栈不为空且栈顶元素大于给的x值,就弹出栈顶元素
        if(tt) cout << stk[tt] << ' ';// 如果栈不为空,则输出距离x最近的比x小的值
        else cout << -1 << ' ';
        
        stk[++ tt] = x;// 把x插入到栈里面,每个元素只能出入栈一次,时间复杂度为2n
    }
    return 0;
}
  • 使用scanf进行读入,可以提升程序的运行速度
int main()
{
    int n;// 读入第1行的数
    scanf("%d",&n);
    
    for(int i = 0; i < n; i++)
    {
        int x;
        scanf("%d",&x);
        while(tt && stk[tt] >= x) tt--;// 栈不为空且栈顶元素大于给的x值,就弹出栈顶元素
        if(tt) printf("%d ",stk[tt]);// 如果栈不为空,则输出距离x最近的比x小的值
        else printf("-1");
        
        stk[++ tt] = x;// 把x插入到栈里面
    }
    return 0;
}

5 队列

5.1 模板

5.1.1 普通队列

  • 队头 hh 在左边,队尾 tt 在右边
  • 队头弹出一个数据,则 hh++,队尾插入一个数据,则 tt++
    在这里插入图片描述
// hh 表示队头,tt表示队尾
// 在队尾插入元素,在队头弹出元素
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh <= tt)
{
}
  • 上课代码
// hh 表示队头,tt表示队尾
// 在队尾插入元素,在队头弹出元素
int q[N], hh = 0, tt = -1;// tt初始为-1,如果保证队头hh在队尾tt后面的话,hh应该小于tt才对

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;// 如下图所示hh++, 则队头数据弹出

// 取队头元素
q[hh];

// 取队尾元素
q[tt];

// 判断队列是否为空
if (hh <= tt) not empty
else empty;

在这里插入图片描述

5.1.2 循环队列

// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh != tt)
{
}

5.2 习题1 —— 829.模拟队列

Acwing 829.模拟队列

6 单调队列

6.1 模板

常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;// hh表示队头,tt表示队尾。
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;
}

6.2 习题1 —— 154.滑动窗口

Acwing 154.滑动窗口

  • 前面的一个点比后面的一个点大(紧挨着的两个点),在求最小值时,前面大的点没用的,就会被弹出去。就是出现有前面一个点大于后面一个点这样的逆序对,就把大的点删掉。这样数列变成严格单调上升的队列。队头就是最小值
    在这里插入图片描述
  • i是枚举的右端点,k是区间的长度,队列q[hh]存的是下标
    在这里插入图片描述
  • 队头是指滑动窗口左边,队尾是滑动窗口右边。每次移动把新元素插到队尾,队头弹出滑出去的元素
#include<iostream>
using namespace std;

const int N = 1000010;
int n, k;// n表示数组长度,k表示滑动窗格大小
int q[N], a[N];// 数组a存原来的值,q存单调队列

int main()
{
    scanf("%d%d", &n, &k);
    for(int i = 0; i < n; i++) scanf("%d",&a[i]);// 先把所有字符读进来
    
    // 求最小值
    int hh = 0, tt = -1;// hh表示队头,tt表示队尾(-1表示空)
    for(int i = 0; i < n; i++)
    {
        // 判断队头是否已经滑出窗口(队列里面存的是下标), 下面true的话表示已经滑出
        // 队头小于队尾,且i - k + 1表示窗格队头下标,q[hh](队列)存的是最小值的下标,如果小于就说明上一次的最小值在窗格内
        if (hh <= tt && i - k + 1 > q[hh]) hh ++;
        
        while (hh <= tt && a[q[tt]] >= a[i]) tt --;// q[tt]里面存的是下标,如果队尾的值大于当前值,则弹出队尾值
        
        q[++ tt] = i;// 把当前数的下标插入到队列, 因为i有可能是最小值,所以需要先进行添加
        if(i >= k - 1) printf("%d ",a[q[hh]]);// 前k-1个数之后开始输出,如果i>=k-1,才开始输出,队头是最小值
        
        // 输出队列数据,看是否符合预期
        // for(int j = hh; j <= tt;j ++) printf("%d ",a[q[j]]);
        // puts("");
    }
    puts("");// 换行
    
    // 求最大值
    hh = 0, tt = -1;// hh表示队头,tt表示队尾(-1表示空)
    for(int i = 0; i < n; i++)
    {
        // 判断队头是否已经滑出窗口(队列里面存的是下标), 下面true的话表示已经滑出
        // 队头小于队尾,且i - k + 1表示窗格队头下标
        if (hh <= tt && i - k + 1 > q[hh]) hh ++;
        
        while (hh <= tt && a[q[tt]] <= a[i]) tt --;// q[tt]里面存的是下标,如果队尾的值大于当前值,则弹出队尾值
        
        q[++ tt] = i;// 把当前数的下标插入到队列, 因为i有可能是最小值,所以需要先进行添加
        if(i >= k - 1) printf("%d ",a[q[hh]]);// 队头是最大值
    }
    puts("");// 换行
    
    return 0;
}

7 KMP

  • 暴力解法
    在这里插入图片描述
  • next[i] = j:表示数组p在[1,j]这个区间的值与[i-j+1,i]这个区间的值相等,如图1和图2所示。也就是说红色数组,[1, j]和[i-j+1, i]这两段数组相同。比如j=3,i=5,则[1,3]区间的数和[3,5]区间的数相同。
  • next[i]里面存放的是[1,i]这个区间最长公共串的长度。
    在这里插入图片描述
  • 图2
    在这里插入图片描述
  • 当蓝色在i位置,红色在 j+1 位置不匹配时,绿色是红色的一部分,使得满足红色数组部分p[1, m] = p[j - m + 1, j]两部分相等。就是红绿部分匹配符合next[j] = m式子。
  • 绿色的圈是 i,红色的圈是 j。s[i] 不同于 p[j+1],s[i-1] 相同于 p[j]
    在这里插入图片描述
  • Si 匹配 Pj+1

7.1 模板

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; 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 <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

7.2 习题1 —— 831.KMP字符串

Acwing 831.KMP字符串
详细解释

  • 图中绿色圈和红色的圈不能匹配,就是s[i] != p[j+1]
    在这里插入图片描述
  • 如果不匹配就向后移动一格,ne[j] + 1 = j,在判断ne[j]后面的一个数是否和位置 i的数字相同
    在这里插入图片描述
#include<iostream>
using namespace std;

const int N = 100010, M = 1000010;// 大于给定的数

int n, m;// n表示模式串p的长度,m表示字符串S的长度
char p[N], s[M];// 字符串数组
int ne[N];// C++中定义为next会报错

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;// p和s的下标从1开始
    
    // 求next[]数组过程
    for(int i = 2, j = 0; i <= n; i++)// i从2开始,i = 2时,j = 1,next[j = 1] = 0
    {
        while(j && p[i] != p[j + 1]) j = ne[j];// 退而求其次,就是整体向后移动一格
        if(p[i] == p[j + 1]) j ++;// 匹配上的话,j就向后移动一位
        ne[i] = j;// p[1,j] = p[i-j+1, i]
    }
    
    // kmp匹配过程
    for(int i = 1, j = 0;i <= m; i++)
    {
        // j没有退回起点(退回起点需要重新匹配),且s[i]和p[j+1]不能相匹配(如图所示,与s[i]匹配的是p[j+1],所以i=1时,j=0)
        while(j && s[i] != p[j + 1]) j = ne[j];// 绿色的圈不能和红色的圈匹配, ne[j]表示是[1,j]这个区间最长公共串的长度(红色)
        if(s[i] == p[j + 1]) j++;// 如果两个已经匹配,则j移动到下一个位置
        if(j == n)// 匹配成功
        {
            printf("%d ", i - n);// 起始位置,题目下标从0开始
            j = ne[j];// 往后退一步
        }
    }
    return 0;
}
  • j = ne[j]
    在这里插入图片描述
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2021 Nqq

你的鼓励是我学习的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值