Acwing算法基础课学习笔记(四)--数据结构之单链表&&双链表&&模拟栈&&模拟队列&&单调栈&&单调队列&&KMP

单链表

算法题中最常考的单链表就是邻接表(用来存储图和数),比如最短路问题,最小生成树问题,最大流问题。双链表用于优化某些问题。
利用数组来表达单链表:存储值和指针的两个数组利用下标进行关联。
Acwing算法基础课学习笔记(四)--数据结构之单链表&&双链表&&模拟栈&&模拟队列&&单调栈&&单调队列&&KMP_第1张图片
需要注意的是,head不是节点,只是指向某个节点的指针。head存的是链表第一个点的下标,形象地看就好像是指向了头结点。
这里贴一个非常有趣的题解

#include 

using namespace std;

const int N = 100010;

int n;
int h[N], e[N], ne[N], head, idx;

//对链表进行初始化
void init(){
    head = -1;//最开始的时候,链表的头节点要指向-1,
    //为的就是在后面进行不断操作后仍然可以知道链表是在什么时候结束
    /*
    插句题外话,我个人认为head其实就是一个指针,是一个特殊的指针罢了。
    刚开始的时候它负责指向空结点,在链表里有元素的时候,它变成了一个指向第一个元素的指针

    当它在初始化的时候指向-1,来表示链表离没有内容。
    */
    idx = 0;//idx在我看来扮演两个角色,第一个是在一开始的时候,作为链表的下标,让我们好找
    //第二在链表进行各种插入,删除等操作的时候,作为一个临时的辅助性的所要操作的元素的下
    //标来帮助操作。并且是在每一次插入操作的时候,给插入元素一个下标,给他一个窝,感动!
    /*
    再次插句话,虽然我们在进行各种操作的时候,元素所在的下标看上去很乱,但是当我们访问的
    时候,是靠着指针,也就是靠ne[]来访问的,这样下标乱,也就我们要做的事不相关了。
    另外,我们遍历链表的时候也是这样,靠的是ne[]
    */
}
//将x插入到头节点上
void int_to_head(int x){//和链表中间插入的区别就在于它有head头节点
    e[idx] = x;//第一步,先将值放进去
    ne[idx] = head;//head作为一个指针指向空节点,现在ne[idx] = head;做这把交椅的人换了
    //先在只是做到了第一步,将元素x的指针指向了head原本指向的
    head = idx;//head现在表示指向第一个元素了,它不在是空指针了。(不指向空气了)
    idx ++;//指针向下移一位,为下一次插入元素做准备。
}

//将x插入到下标为k的点的后面
void add(int k, int x){
    e[idx] = x;//先将元素插进去
    ne[idx] = ne[k];//让元素x配套的指针,指向它要占位的元素的下一个位置
    ne[k] = idx;//让原来元素的指针指向自己
    idx ++;//将idx向后挪
    /*
    为了将这个过程更好的理解,现在
    将指针转变的这个过程用比喻描述一下,牛顿老师为了省事,想插个队,队里有两个熟人
    张三和李四,所以,他想插到两个人中间,但是三个人平时关系太好了,只要在一起,就
    要让后面的人的手插到前面的人的屁兜里。如果前面的人屁兜里没有基佬的手,将浑身不
    适。所以,必须保证前面的人屁兜里有一只手。(张三在前,李四在后)
    这个时候,牛顿大步向前,将自己的手轻轻的放入张三的屁兜里,(这是第一步)
    然后,将李四放在张三屁兜里的手抽出来放到自己屁兜里。(这是第二步)
    经过这一顿骚操作,三个人都同时感觉到了来自灵魂的战栗,打了个哆嗦。
    */
}

//将下标是k的点后面的点个删掉
void remove(int k){
    ne[k] = ne[ne[k]];//让k的指针指向,k下一个人的下一个人,那中间的那位就被挤掉了。
}
int main(){
    cin >> n;
    init();//初始化
    for (int i = 0; i < n; i ++ ) {
        char s;
        cin >> s;
        if (s == 'H') {
            int x;
            cin >> x;
            int_to_head(x);
        }
        if (s == 'D'){
            int k;
            cin >> k;
            if (k == 0) head = ne[head];//删除头节点
            else remove(k - 1);//注意删除第k个输入后面的数,那函数里放的是下标,k要减去1
        }
        if (s == 'I'){
            int k, x;
            cin >> k >> x;
            add(k - 1, x);//同样的,第k个数,和下标不同,所以要减1
        }
    }

    for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ' ;
    cout << endl;

    return 0;
}

我的代码:

#include 

using namespace std;

const int N = 100010;

//head表示头节点的下标
//e[i]表示节点i的值
//ne[i]表示节点i的next指针是多少,节点i的下一个点的位置(下标/地址)
//idx存储当前已经用到了哪个点
int head, idx, e[N], ne[N];

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

//将x插到头节点
void add_to_head(int x)
{
	e[idx] = x;
	ne[idx] = head;
	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()
{
	int m;
	cin >> m;
	init();
	while (m--)
	{
		int k, x;
		char op;
		cin >> op;
		if ('H' == op)
		{
			cin >> x;
			add_to_head(x);
		}
		else if ('D' == op)
		{
			cin >> k;
			if (!k)    head = ne[head];
			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;
}

双链表

#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的右边插入一个数
void add(int k, int x)
{
	e[idx] = x;
	r[idx] = r[k];
	l[idx] = k;
	l[r[k]] = idx;
	r[k] = idx;
	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 ("L" == op)
		{
			cin >> x;
			add(0, x);
		}
		else if ("R" == op)
		{
			cin >> x;
			add(l[1], x);
		}
		else if ("D" == op)
		{
			cin >> k;
			remove(k + 1);
		}
		else if ("IL" == op)
		{
			cin >> k >> x;
			add(l[k + 1], x);
		}
		else
		{
			cin >> k >> x;
			add(k + 1, x);
		}
	}
	for (int i = r[0]; i != 1; i = r[i])	cout << e[i] << ' ';
	cout << endl;
	return 0;
}

模拟栈

#include 

using namespace std;

const int N = 100010;

int stk[N], tt;

int main()
{
	int m;
	cin >> m;
	while (m--)
	{
		string op;
		int x;

		cin >> op;
		if ("push" == op)
		{
			cin >> x;
			stk[++tt] = x;
		}
		else if ("pop" == op)	tt--;
		else if ("empty" == op)	cout << (tt ? "NO" : "YES") << endl;
		else	cout << stk[tt] << endl;
	}

	return 0;
}

模拟队列

#include 

using namespace std;

const int N = 100010;

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

int main()
{
	int m;
	cin >> m;
	while (m--)
	{
		int x;
		string op;
		cin >> op;
		if ("push" == op)
		{
			cin >> x;
			q[++tt] = x;
		}
		else if ("pop" == op)	hh++;
		else if ("empty" == op)	cout << (hh <= tt ? "NO" : "YES") << endl;
		else cout << q[hh] << endl;
	}
	return 0;
}

单调栈

单调栈和单调队列虽然很抽象难以理解,但是好消息在于能用的时机还是有限的。

#include 

using namespace std;

const int N = 100010;
int stk[N], tt;

int main()
{
	int n;
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
	{
		int x;
		scanf("%d", &x);
		while (stk[tt] >= x && tt)	tt--;//当栈不为空且栈顶元素大于x,则舍去
		if (tt)	printf("%d ", stk[tt]);//如果栈内有元素,则输出结果
		else printf("-1 ");

		stk[++tt] = x;//将x压入栈
	}
	return 0;
}

单调队列

最经典的应用就是求滑动窗口的最大值最小值。
我们用q来表示单调队列,p来表示其所对应的在原列表里的序号。

由于此时队中没有一个元素,我们直接令1进队。此时,q={1},p={1}。

现在3面临着抉择。下面基于这样一个思想:假如把3放进去,如果后面2个数都比它大,那么3在其有生之年就有可能成为最小的。此时,q={1,3},p={1,2}

下面出现了-1。队尾元素3比-1大,那么意味着只要-1进队,那么3在其有生之年必定成为不了最小值,原因很明显:因为当下面3被框起来,那么-1也一定被框起来,所以3永远不能当最小值。所以,3从队尾出队。同理,1从队尾出队。最后-1进队,此时q={-1},p={3}

出现-3,同上面分析,-1>-3,-1从队尾出队,-3从队尾进队。q={-3},p={4}。

出现5,因为5>-3,同第二条分析,5在有生之年还是有希望的,所以5进队。此时,q={-3,5},p={4,5}

出现3。3先与队尾的5比较,3<5,按照第3条的分析,5从队尾出队。3再与-3比较,同第二条分析,3进队。此时,q={-3,3},p={4,6}

出现6。6与3比较,因为3<6,所以3不必出队。由于3以前元素都<3,所以不必再比较,6进队。因为-3此时已经在滑动窗口之外,所以-3从队首出队。此时,q={3,6},p={6,7}

出现7。队尾元素6小于7,7进队。此时,q={3,6,7},p={6,7,8}。

那么,我们对单调队列的基本操作已经分析完毕。因为单调队列中元素大小单调递*(增/减/自定义比较),
因此,队首元素必定是最值。按题意输出即可。

#include 

using namespace std;

const int N = 1000010;

int a[N], q[N];

int main()
{
    int n, 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 ++ ;//判断对头是否划出窗口
        while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;//如果队尾大于要加入的值,则出队
        q[ ++ tt] = i;
        if (i >= k - 1) printf("%d ", a[q[hh]]);//满足前k个数才输出
    }

    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]]);//满足前k个数才输出
    }

    puts("");

    return 0;
}

KMP字符串

不错的题解
//KMP算法

#include 

using namespace std;

const int N = 100010, M = 1000010;//N为模板串长度,M为模式串长度
int n, m, ne[N];
char p[N], s[M];//s为模式串,p为模板串(匹配串)

int main()
{
	cin >> n >> p + 1 >> m >> s + 1;//下标从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++;//满足匹配条件,打印开头下标, 从0开始
		if (j == n)
		{
		    //匹配完成后的具体操作
            //如:输出以0开始的匹配子串的首字母下标
			printf("%d ", i - n);
			j = ne[j]; //再次继续匹配
		}
	}
	return 0;
}

你可能感兴趣的:(学习笔记,刷题练习,Acwing)