基础算法笔记整理系列

数据结构基础一

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;//数组模拟使用-1来表示空
    idx = 0;
}

//将x插入到头结点的位置`在这里插入代码片`
void add_to_head(int x)
{
    e[idx] = x;//存下值
    ne[idx] = head;//存下next指针
    head = idx;//更新头结点
    idx ++;
}

//将x插入到下标为k的结点后面
void add(int k,int x)
{
	e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx ++;
}

//删除下标是k的后面的点
void remove(int k)
{
    ne[k] = ne[ne[k]];//指向下一个的下一个
}

int main()
{
    return 0;
}

2.双链表(数组模拟)

双链表也就是同时储存上一个点的指针和下一个点的指针

#include<iostream>
using namespace std;

const int N = 100010;
int n;
int e[N],l[N],r[N],idx; //这里就不用定义头结点和尾结点了,我们可以使用第0个点和第1个点偷懒,e[N]代表结点中存储的值,r[N]代表每一个结点的下一个点的指针(也就是数组存储中的下标),l[N]代表每一个结点上一个点的指针,idx记录现在已经使用到了哪个数组下标

//初始化
void init(){
    //0表示左端点,1表示右端点
    r[0] = 1;
    l[1] = 0;
    idx = 2;
}

//第k点的右边插入   在k的左边插入一个新的点直接调用add(l[k],x),也就是在k的左边插入等于在k的左边第一个点的右边插入一个点
void add(int k,int x){
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx;
    idx ++;
}

//删除结点
void remove(int k){
    r[l[k]] = r[k]; //k点左边的第一个点的右指针指向k点的右结点,直接跳过k点
    l[r[k]] = l[k]; //k点右边的第一个点的左指针指向k点的左结点,直接跳过k点
}

int main(){
	return 0;
}

3.栈和队列(数组模拟)

栈:先进后出 (想象成单口的水井,每次进行一次操作,不是拿出就是放入,先被放入的会更晚拿出来)

队列:先进先出(想象成双口的罐子,每次进行一次操作,从上面的口放入,从下面的口拿出,所以先放入队列的元素会先拿出来)

栈的数组模拟
#include<iostream>
using namespace std;

const int N = 100010;
int stk[N],tt;//stk数组所代表的就是栈,tt代表的是栈顶元素的下标

//插入
stk[++ tt] = x;//就是将新的元素放在了数组的最右端,并且将其下标记录

//删除栈顶元素
tt --;//栈顶元素的下标记录减一,就相当于删除了栈顶元素

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

//取出栈顶元素
stk[tt];

int main()
{
    return 0;
}
队列的数组模拟
#include<iostream>
using namespace std;

const int N = 100010;
int q[N],hh,tt;//hh表示队头元素,tt表示队尾元素

//插入
h[++tt] = x;

//弹出
hh++;

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

//取出队头元素
q[hh];
 
int main()
{
    return 0;
}
单调栈和单调队列

单调栈一般应用:给定一个序列,找到这个序列中每一个数左边(右边)离他最近并且比它小或者大的数的位置,存在就返回这个数,不存在就返回这个-1。

单调栈

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

输入格式

第一行包含整数 N ,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式

共一行,包含 N 个整数,其中第 ii 个数表示第 ii 个数的左边第一个比它小的数,如果不存在则输出 −1

数据范围

1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105
1 ≤ 数 列 中 元 素 ≤ 1 0 9 1≤数列中元素≤10^9 1109

输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2

暴力做法类似于双指针算法,就是遍历一个数左边所有的数,来查找到离这个数最近且比这个数小的数。

使用单调栈优化,就是使用栈来存放所有的数据,在输入数据的时候就看一下栈顶元素是不是比当前的数大,如果栈顶元素大于你当前读入的元素,说明后面读入的所有数都不可能以栈顶元素为答案,因为你新读入的数大于栈顶元素并且距离左边更近,所以当读入的数据小于栈顶元素的时候就弹出栈顶元素。

代码
#include <iostream>
using namespace std;
const int N = 100010;

int n,stk[N],tt;//stk数组模拟栈,tt记录栈顶元素

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false); //输入优化,使得cin的输入更快
    cin >> n;
    for(int i = 0; i < n; i ++){
        int x;
        cin >> x;
        while(tt && stk[tt] >= x)tt--; //判断栈顶元素是否大于等于当前插入元素
        if(tt) cout << stk[tt] <<" ";  //如果栈不为空,并且上一个判断完成,说明此时的栈顶元素就满足条件
        else cout << "-1 ";            //如果栈为空,并且上一个判断完成,说明这些数据中这个数的左边没有满足条件的数
        stk[++tt] = x;                 //向栈中插入新的元素
    }
    return 0;
}

时间复杂度分许:每个元素最多进栈出栈一次,所以是O(n)的时间复杂度。

单调队列一般应用:求一个滑动窗口里的最大值或者最小值

滑动窗口(单调队列)

给定一个大小为 n ≤ 1 0 5 n≤10^5 n105的数组

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 k 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7]k 为 33。

窗口位置最小值最大值
[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

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

输入格式

输入包含两行。

第一行包含两个整数 nk,分别代表数组长度和滑动窗口的长度。

第二行有 n 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7

暴力做法,双重遍历,时间复杂度O(n2)。

使用单调队列来优化,就拿寻找最小值举例,如果向右移动后的数据是最小的数,那么窗口中的前两个数在寻找最小值的过程中是不是就没有用了,因为最新进入窗口的数更小。寻找最大值优化也是类似的。

代码
#include <iostream>
using namespace std;
const int N = 1000010; //数据范围较大,注意使用scanf和printf输入输出,或者使用ios优化和tie优化

int n,k;
int a[N],q[N];//a数组是用来存储数组,q数组是用来存储队列

int main()
{
    int hh = 0,tt = -1;//hh表示队头,tt表示队尾
    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 && 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]]);//输出,队列是一个一个进的,所以前k - 1个数字依次进入窗口中的时候,窗口中的数字还不是k个,这个时候还没有输出,所以需要判断一下
    }
    
    puts("");
    
    //最大值做法基本一致,只需要更改输入时的判断条件即可
    hh = 0,tt = -1;
    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 && 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;
}

从代码的实现过程中不难看出,单调栈和单调队列都是利用在插入的时候进行判断弹出了栈顶元素或者是队尾元素,通过这样的方式来维护栈和队列,使得其始终是单调递增或者单调递减的,这样就能满足某些寻找最大值或者最小值的条件。

4.KMP

KMP算法就是匹配字符串的优化算法,给定模板字符串和目标字符串,需要寻找目标字符串是不是模板字符串的子串,如果是的话就返回字串的初始下标

暴力做法:暴力做法就是遍历每一个模板字符串中的每一个字符,将模板字符串中的每一个字符当作目标字符串的初始字符,来匹配后面的字符串是不是和目标字符串相匹配。

for(int i = 0; i < n; i ++){
    bool flag = true;
    for(int j = 0; j < n; j ++){
        if(s[j] != p[j]){
            flag = false;
            break;
        }
    }
}

KMP算法思路:每次匹配完一个字符后,如果相同就匹配两个字符串的下一个字符,如果不相同,按照暴力思路的话,就需要从下一个模板字符串的字符开始重新依次循环匹配,而KMP算法就是通过优化,减少不必要的移动后再次匹配。例如一个模板字符串abababababcabab(p[]),一个目标字符串ababcabab(s[]),按照暴力思路的话,第一次匹配的时候第五个字符不同,就会向后移动一位,但是很显然,移动一位后模板字符串的首字符变成了b,很明显这就是无效的移动。那我们再想一想,目标字符串ababcabab以s[3]为终点的子串,以及以s[0]为起点的子串,最大长度是2(就是ab),注意观察,我们能发现,暴力模拟每次匹配失败的字符前的都是匹配成功的,那么以s[0]为起点的子串,它与以s[3]为终点的子串相等,那么也就和模板字符串匹配失败的字符的前一个字符相同长度的子串是相同的,所以可以直接将目标字符串向后移动多位,这个位数就是匹配失败的字符的前一个字符的下标和这个字符的最大子串长度的差。

KMP演示代码
#include <iostream>
using namespace std;
const int N = 100010,M = 1000010;
int n,m;
char p[N],s[M];//s数组就是模板字符串,p数组就是目标字符串
int ne[N];

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;
    
    //求next数组,其实与KMP核心思想类似,差别就是拿着两个相等的字符串再进行比较,去寻找模板字符串中的每个i所能对应的最大的j。然后将这个j更新到next数组中
    for(int i = 2, j = 0; i <= n; i ++){ //首先,i从2开始
        while(j && p[i] != p[j + 1]) j = ne[j]; //如果j不为0,并且字符不匹配,那么就相当于将目标字符串向前移动
        if(p[i] == p[j + 1]) j ++;//如果匹配成功,那么就能将j向后移动
        ne[i] = j; //更新next
    }
    
    //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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值