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


一、单链表

动态链表

struct Node
{
	int val;
	struct Node* next;
}

静态链表

采用数组模拟链表,使用数组 e [ N ] e[N] e[N] 存储节点的值, n e [ N ] ne[N] ne[N] 存储该节点下一节点的下标。


两者优劣

算法答题与笔试中一般使用静态链表,追求速度快,但是会导致空间的浪费。工程中一般使用动态链表,追求对空间的利用率,但相应的使用new创建新的节点会导致速度缓慢。


模拟单链表

题目

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

①向链表头插入一个数;
②删除第 k k k 个插入的数后面的数;
③在第 k k k 个插入的数后插入一个数。

现在要对该链表进行 m m m 次操作,进行完所有操作后,从头到尾输出整个链表。

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

输入格式:
第一行包含整数 m m m,表示操作次数。

接下来 m m m 行,每行包含一个操作命令,操作命令可能为以下几种:

H x,表示向链表头插入一个数 x
D k,表示删除第 k 个插入的数后面的数(当 k0 时,表示删除头结点)。
I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。

输出格式:
共一行,将整个链表从头到尾输出。

数据范围:
1 ≤ m ≤ 100000 所有操作保证合法。 1≤m≤100000所有操作保证合法。 1m100000所有操作保证合法。

输入样例:

10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6

输出样例:

6 4 6 5

代码实现

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int e[N], ne[N], idx, head;

void init()
{
	head = -1;
	idx = 0;
}
void push_front(int x)
{
	e[idx] = x;
	ne[idx] = head;
	head = idx++;
}
void insert(int k, int x)
{
	e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx++;
}
void erase(int k)
{
	ne[k] = ne[ne[k]];
}

int main()
{
	int m = 0;
	init();
	cin >> m;
	while (m--)
	{
		int k, x;
		char op;
		cin >> op;
		switch (op)
		{
		case 'H':
			cin >> x;
			push_front(x); break;
		case 'D':
			cin >> k;
			if (!(k - 1)) head = ne[head]; // 注意头节点的删除
			erase(k - 1); break;
		case 'I':
			cin >> k >> x;
			insert(k - 1, x); break;
		default:
			cout << "Please enter seriously!" << endl; break;
		}
	}
	for (int i = head; ~i; i = ne[i]) cout << e[i] << ' ';
	return 0;
}

二、双链表

模拟双链表

题目

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

①在最左侧插入一个数;
②在最右侧插入一个数;
③将第 k k k 个插入的数删除;
④在第 k k k 个插入的数左侧插入一个数;
⑤在第 k k k 个插入的数右侧插入一个数。

现在要对该链表进行 m m m 次操作,进行完所有操作后,从左到右输出整个链表。

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

输入格式:
第一行包含整数 m m m,表示操作次数。

接下来 m m m 行,每行包含一个操作命令,操作命令可能为以下几种:

L x,表示在链表的最左端插入数 x
R x,表示在链表的最右端插入数 x
D k,表示将第 k 个插入的数删除。
IL k x,表示在第 k 个插入的数左侧插入一个数。
IR k x,表示在第 k 个插入的数右侧插入一个数。

输出格式:
共一行,将整个链表从左到右输出。

数据范围:
1 ≤ m ≤ 100000 1≤m≤100000 1m100000 所有操作保证合法。

输入样例:

10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2

输出样例:

8 7 7 3 2 9

代码实现

在 “D”, “IL”, “IR” 要用 k+1 的原因是 双链表的起始点是 2,每个插入位置 k 的真实位置应该为 k-1+2 = k+1 (在单链表中为 k-1)。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int e[N], l[N], r[N], idx;

void init()
{
	r[0] = 1, l[1] = 0, idx = 2; // 0这个位置标识,留给了head; 1这个位置标识,留给了tail
}
void insert(int k, int x)
{
	e[idx] = x;
	l[idx] = k;
	r[idx] = r[k];
	l[r[k]] = idx;
	r[k] = idx++;
}
void erase(int k)
{
	r[l[k]] = r[k];
	l[r[k]] = l[k];
}
int main()
{
	init();
	int m, k, x;
	cin >> m;
	while (m--)
	{
		string op;
		cin >> op;
		if (op == "L")
		{
			cin >> x;
			insert(0, x);
		}
		else if (op == "R")
		{
			cin >> x;
			insert(l[1], x);
		}
		else if (op == "D")
		{
			cin >> k;
			erase(k + 1);
		}
		else if (op == "IL")
		{
			cin >> k >> x;
			insert(l[k + 1], x);
		}
		else if (op == "IR")
		{
			cin >> k >> x;
			insert(k + 1, x);
		}
		else
			cout << "Please enter seriously!" << endl;
	}

	for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
	return 0;
}

三、栈

模拟栈

题目

题目描述:
实现一个栈,栈初始为空,支持四种操作:

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

现在要对栈进行 m m m 个操作,其中的每个操作 3 3 3 和操作 4 4 4 都要输出相应的结果。

输入格式
第一行包含整数 m m m,表示操作次数。

接下来 m m m 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式
对于每个 emptyquery 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YESNOquery 操作的查询结果为一个整数,表示栈顶元素的值。

数据范围:
1 ≤ m ≤ 100000 1≤m≤100000 1m100000
1 ≤ x ≤ 1 0 9 1≤x≤10^9 1x109
所有操作保证合法。

输入样例:

10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty

输出样例:

5
5
YES
4
NO

代码实现

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int stack[N], top;
int main()
{
	int m, x;
	cin >> m;
	string op;
	top = -1;
	while (m--)
	{
		cin >> op;
		if (op == "push")
		{
			cin >> x;
			stack[++top] = x;
		}
		else if (op == "pop")
			--top;
		else if (op == "empty")
			cout << (top == -1 ? "YES" : "NO") << endl;
		else if (op == "query")
			if (top != -1) cout << stack[top] << endl;
		else
			cout << "Please enter seriously!" << endl;
	}
	return 0;
}

表达式求值

题目

题目描述:
给定一个表达式,其中运算符仅包含 +, -, *, /(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

  • 数据保证给定的表达式合法。
  • 题目保证符号 只作为减号出现,不会作为负号出现,例如, − 1 + 2 −1+2 1+2, ( 2 + 2 ) ∗ ( − ( 1 + 1 ) + 2 ) (2+2)∗(−(1+1)+2) (2+2)((1+1)+2) 之类表达式均不会出现。
  • 题目保证表达式中所有数字均为正整数。
  • 题目保证表达式在中间计算过程以及结果中,均不超过 2 31 − 1 2^{31}−1 2311
  • 题目中的整除是指向 0 0 0 取整,也就是说对于大于 0 0 0 的结果向下取整,例如 5 / 3 = 1 5/3=1 5/3=1,对于小于 0 0 0 的结果向上取整,例如 5 / ( 1 − 4 ) = − 1 5/(1−4)=−1 5/(14)=1。C++ 和 Java 中的整除默认是向零取整;Python 中的整除 // 默认向下取整,因此 Python 的 eval() 函数中的整除也是向下取整,在本题中不能直接使用。

输入格式:
共一行,为给定表达式。

输出格式:
共一行,为表达式的结果。

数据范围:
表达式的长度不超过 1 0 5 10^5 105

输入样例:

(2+2)*(1+1)

输出样例:

8

代码实现

创建两个栈,一个用来装符号,一个用来装数字。

#define _CRT_SECURE_NO_WARNINGS

#include<iostream>
#include<stack>
#include<unordered_map>

using namespace std;
stack<int> num;
stack<char> op;

unordered_map<char, int> h{{ '+', 1}, { '-', 1 }, { '*', 2 }, { '/', 2 }, { '(', 0 }};

void eval()
{
	int a = num.top();
	num.pop();
	int b = num.top();
	num.pop();
	char p = op.top();
	op.pop();
	int res = 0;
	switch (p)
	{
	case '+':
		res = a + b;
		break;
	case '-':
		res = a - b;
		break;
	case '/':
		res = a / b;
		break;
	case '*':
		res = a * b;
		break;
	}
	num.push(res);
}
int main()
{
	cin.tie(0);
	ios::sync_with_stdio(false);
	string s;
	cin >> s;
	for (int i = 0; i < s.size(); ++i)
	{
		if (isdigit(s[i]))
		{
			int x = 0;
			while (i < s.size() && isdigit(s[i]))
			{
				x = x * 10 + s[i] - '0';
				i++;
			}
			i--;
			num.push(x);
		}
		else if (s[i] == '(') op.push(s[i]);
		else if (s[i] == ')')
		{
			while (op.top() != '(') eval();
			op.pop();
		}
		else
		{
			while (op.size() && h[op.top()] >= h[s[i]]) eval();
			op.push(s[i]);
		}
	}
	while (op.size()) eval();
	cout << num.top() << endl;
	return 0;
}

四、队列

模拟队列

题目

题目描述:
实现一个队列,队列初始为空,支持四种操作:

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

现在要对队列进行 m m m 个操作,其中的每个操作 3 3 3 和操作 4 4 4 都要输出相应的结果。

输入格式:
第一行包含整数 m m m,表示操作次数。

接下来 m m m 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式:
对于每个 emptyquery 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YESNOquery 操作的查询结果为一个整数,表示队头元素的值。

数据范围:
1 ≤ M ≤ 100000 1≤M≤100000 1M100000
1 ≤ x ≤ 1 0 9 1≤x≤10^9 1x109
所有操作保证合法。

输入样例:

10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6

输出样例:

NO
6
YES
4

代码实现

普通队列解法

左开右闭的区间——[hh, tt)

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int q[N], hh, tt;
int main()
{
	int m, x;
	cin >> m;
	string op;
	while (m--)
	{
		cin >> op;
		if (op == "push")
		{
			cin >> x;
			q[tt++] = x;
		}
		else if (op == "pop")
			++hh;
		else if (op == "empty")
			cout << (hh >= tt ? "YES" : "NO") << endl;
		else if (op == "query")
			cout << q[hh] << endl;
		else
			cout << "Please enter seriously!" << endl;
	}
}

循环队列解法

相比普通队列的解法,循环队列可以在再利用空间, 同时也可以增加判断队列是否占满的函数。

if (tt == N) tt = 0; // 循环:从N - 1后回到0
if (hh == N) hh = 0; // 循环:从N - 1后回到0
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int q[N], hh, tt;
int main()
{
	int m, x;
	cin >> m;
	string op;
	while (m--)
	{
		cin >> op;
		if (op == "push")
		{
			cin >> x;
			q[tt++] = x;
			if (tt == N) tt = 0; // 循环:从N - 1后回到0
		}
		else if (op == "pop")
		{
			++hh;
			if (hh == N) hh = 0; // 循环:从N - 1后回到0
		}
		else if (op == "empty")
			cout << (hh >= tt ? "YES" : "NO") << endl;
		else if (op == "query")
			cout << q[hh] << endl;
		else
			cout << "Please enter seriously!" << endl;
	}
}

五、单调栈

实现单调栈

题目

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

输入格式:
第一行包含整数 n n n,表示数列中元素个数。

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

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

数据范围:
1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105

1 ≤ n ≤ 1 0 9 1≤n≤10^9 1n109

输入样例:

5
3 4 2 7 5

输出样例:

-1 3 -1 2 2

单调栈原理

单调栈里装的是什么?

动态维护一个栈,把后续问题的答案都维持在这个栈中,把肯定不再有用的数字从栈中去掉。

  • 动态维护,随进随维护,不需预处理。

  • O ( N 2 ) O(N^2) O(N2) 的时间复杂度降为 O ( N ) O(N) O(N)

  • 此类 左侧(或右侧)最近,比自己大的(或小的),用单调栈。


代码实现

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int stk[N], tt = -1;
int main()
{
	cin.tie(nullptr);
	ios::sync_with_stdio(false); // 优化输入\输出速度
	int n, x;
	cin >> n;
	for (int i = 0; i < n; ++i)
	{
		cin >> x;
		while (tt != -1 && stk[tt] >= x) tt--;
		if (tt != -1) cout << stk[tt] << ' ';
		else cout << -1 << ' ';

		stk[++tt] = x;
	}
	return 0;
}

优化输入输出速度:
cin cout 相比于 scanf printf,速度较为缓慢。
可以使用以下代码进行优化:

cin.tie(nullptr);
ios::sync_with_stdio(false);

cin.tie(0) 的作用和 cin.tie(nullptr) 是等价的,用于将cin与其他输出流的绑定解除,同时将cin的缓冲区设置为无缓冲(unbuffered),这样在读取输入时不会有额外的I/O操作。因为 cin 默认与 cout 绑定,并且在每次输入之前都会刷新cout的缓冲区,因此会导致额外的I/O操作,从而降低输入效率。

ios::sync_with_stdio(false) 的作用同样是为了提高输入输出效率,但是和cin.tie(0) 的作用不同。ios::sync_with_stdio(false) 是将C++标准流和C标准流的同步关闭,这样可以提高C++标准流的输入输出效率,但是会影响到C标准流(如 printfscanf )的输出效率。因此,如果需要同时使用C++标准流和C标准流,不应该关闭同步。


六、单调队列

滑动窗口

题目

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

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

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

以下是一个例子:

该数组为[1 3 −1 −3 5 3 6 7] k k k 3 3 3

窗口位置最小值最大值
[ [ [ 1 1 1 3 3 3 − 1 −1 1 ] ] ] − 3 −3 3 5 5 5 3 3 3 6 6 6 7 7 7 − 1 −1 1 3 3 3
1 1 1 [ [ [ 3 3 3 − 1 −1 1 − 3 −3 3 ] ] ] 5 5 5 3 3 3 6 6 6 7 7 7 − 3 −3 3 3 3 3
1 1 1 3 3 3 [ [ [ − 1 −1 1 − 3 −3 3 5 5 5 ] ] ] 3 3 3 6 6 6 7 7 7 − 3 −3 3 5 5 5
1 1 1 3 3 3 − 1 −1 1 [ [ [ − 3 −3 3 5 5 5 3 3 3 ] ] ] 6 6 6 7 7 7 − 3 −3 3 5 5 5
1 1 1 3 3 3 − 1 −1 1 − 3 −3 3 [ [ [ 5 5 5 3 3 3 6 6 6 ] ] ] 7 7 7 3 3 3 6 6 6
1 1 1 3 3 3 − 1 −1 1 − 3 −3 3 5 5 5 [ [ [ 3 3 3 6 6 6 7 7 7 ] ] ] 3 3 3 7 7 7

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

输入格式:
输入包含两行。

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

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

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

输出格式:
输出包含两个。

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

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

输入样例:

8 3
1 3 -1 -3 5 3 6 7

输出样例:

 -1 -3 -3 -3 3 3
 3 3 5 5 6 7

代码实现

这里使用的是双端队列。
双端队列(Double Ended Queue,简称为Deque)是一种允许在队列的两端进行插入和删除操作的数据结构

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int q[N], hh, tt;
int a[N], k;

int main()
{
	cin.tie(nullptr);
	ios::sync_with_stdio(false);

	int n, k;
	cin >> n >> k;
	for (int i = 0; i < n; ++i) cin >> a[i];

	hh = 0, tt = -1;
	for (int i = 0; i < n; ++i)
	{
		if (hh <= tt && q[hh] < i + 1 - k) hh++;
		while (hh <= tt && a[q[tt]] >= a[i]) tt--; // 注意是从队尾往前去除较大数
		q[++tt] = i;
		if (i + 1 - k >= 0) cout << a[q[hh]] << ' ';
	}
	cout << endl;

	hh = 0, tt = -1;
	for (int i = 0; i < n; ++i)
	{
		if (hh <= tt && q[hh] < i + 1 - k) hh++;
		while (hh <= tt && a[q[tt]] <= a[i]) tt--; // 注意是从队尾往前去除较小数
		q[++tt] = i;
		if (i + 1 - k >= 0) cout << a[q[hh]] << ' ';
	}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值