栈与递归
栈的特征:后进先出,也就是说先入栈的,最后才出的来。
函数递归:函数递归的时候需要创建函数栈帧,它的行为根栈的特征很相似。
递归有两种,第一种,线性深入。第二种,树形遍历
如图:
什么时候使用栈?
原理:当你发现问题的过程根栈的运动轨迹很相似的时候。
常见的有:
匹配型问题
括号序列
给定一个长度为 n 的字符串 s,字符串由 (
, )
, [
, ]
组成,问 s 是不是一个合法的括号序列。
合法的括号序列的定义是:
- 空串是一个合法的括号序列。
- 若
A
是一个合法的括号序列,则(A)
,[A]
也是合法的括号序列。 - 若
A
,B
都是合法的括号序列,则AB
也是合法的括号序列。
输入格式
第一行一个整数 nn。
接下来一行一个长度为 nn 的字符串 ss 。
输出格式
如果 ss 是合法的括号序列,输出 Yes
,否则输出 No
。
思路:仔细思考问题,可以发现匹配的时候一定是和最近的一次进行匹配,所以要求满足后入先出的特点。
左括号入栈,遇到右括号进行匹配,匹配不上就是No,匹配的上就把左括号出栈。
下面来看代码:
#include <iostream>
#include <algorithm>
#include <stack>
using namespace std;
int n;
char str[100001];
char s[200001];//用来模拟栈的数组
int top = 0;
int flag = 0;
int main()
{
cin >> n;
cin >> str;
for(int i = 0; i < n; i++) {
if(str[i] == '[' || str[i] == '(') {
s[++top] = str[i];
} else {
if(str[i] == ']') {
if(s[top] != '[') {
flag = 1;
break;
} else {
top--;//抵消
}
} else {
if(s[top] != '(') {
flag == 1;
break;
} else {
top--;
}
}
}
}
if(flag || top) {
cout << "No";
} else {
cout << "Yes";
}
return 0;
}
需要注意的点:
- 如果最后栈不为空,那么说明,没有右括号了,但是还有左括号没有被匹配,也是错误的。
字符串处理1
给定一个长度为n的字符串s,字符串由小写字母a..z组成。
小明来对这个字符串进行操作,他会从头到尾检查这个字符串,如果发现有两个相同的字母并排在一起,就会把这两个字符都删掉。小明会重复这个操作,直到没有相邻的相同字母。
你需要给出处理完成的字符串。
输入格式
第一行一个整数n。
接下来一行一个长度为n的字符串s。
输出格式
输出最后处理完成的字符串,有可能是空串。
思路:依次把字符串入栈,如果遇到相等的就top–,根括号匹配几乎一样。
代码:
#include <iostream>
#include <algorithm>
using namespace std;
int n;
char str[100001];
char s[100001];
int top = 0;
int main() {
cin >> n;
cin >> str;
s[++top] = str[0];
for(int i = 1; i < n; i++) {
if(s[top] != str[i]) {
s[++top] = str[i];
} else {
top--;
}
}
for(int i = 1; i <= top; i++) {//注意这里是top,我们需要新的字符串
printf("%c", s[i]);
}
return 0;
}
题型:出栈序列
出栈序列判断
现在有一个栈,有n个元素,分别为1,2,…,n。我们可以通过push和pop操作,将这n个元素依次放入栈中,然后从栈中弹出,依次把栈里面的元素写下来得到的序列就是出栈序列。
比如n=3,如果执行push 1, push 2, pop, push 3, pop, pop,那么我们pop操作得到的元素依次是2,3,1。也就是出栈序列就是2,3,1。
现在给定一个合法的出栈序列,请输出一个合法的由push和pop操作构成的出栈序列。这里要求push操作一定是按1,2,…,n的顺序。
输入格式
第一行一个整数n,接下来一行n个整数,表示出栈序列。
输出格式
输出2n行,每行一个push x或pop的操作,可以发现一个出现序列对应的操作序列是唯一的。
第一种思路:
如果我发现我目前的top不是我出栈序列数组里面的当前的值的话,那么就一直push,直到push到我当前的值为止。然后pop当前值。然后继续下一个。依次类推。如图
代码如下:
#include <iostream>
#include <algorithm>
#include <stack>
using namespace std;
int n;
int s[100001];
int top = 0;
int l = 0;
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
if(s[top] != x) {//因为我是从1开始的,而我们的数字里面必定没有0,所以第一次进入循环的时候是必定是不相等的
for(int j = l + 1; j <= x; j++) {
printf("push %d\n", j);
s[++top] = j;
}
l = x;
}
printf("pop\n");
top--;
}
return 0;
}
需要注意的点是:
- 我们用栈进行了模拟,我们pop之后一定要top–
- 因为我们每次都必定循环到可以pop的地方,所以这里不可以写else,不然就不会输入任何pop
第二种思路:
题目链接:
代码如下:
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
if(pushV.size() == 0 || popV.size() == 0 || pushV.size() != popV.size())
{
return false;
}
int i = 0;//pushV的
int j = 0;//popV的
stack<int> st;
while(i < pushV.size())
{
//不bb那么多,先加上再说
st.push(pushV[i]);
i++;
//这个时候考虑是否是删除
while(!st.empty() && st.top() == popV[j])//删完,全部去掉
{
st.pop();
j++;
}
}
return j == popV.size();
}
};
列出所有的出栈序列
现在有一个栈,有n个元素,分别为1,2,…,n。我们可以通过push和pop操作,将这n个元素依次放入栈中,然后从栈中弹出,依次把栈里面的元素写下来得到的序列就是出栈序列。
比如n=3,如果执行push 1, push 2, pop, push 3, pop, pop,那么我们pop操作得到的元素依次是2,3,1。也就是出栈序列就是2,3,1。
请按字典序,输出所有n个元素的可行的出栈序列。
输入格式
第一行,一个整数n。
输出格式
若干行,每行n个整数,表示出栈序列。
思路:我们实际上可以发现(一开始我挂了一张图),栈的行为和函数栈帧的行为是完全一致的。这道题的要求是求出所有的出栈序列,也就是说我的入栈和出栈的所有的组合构成了一个集合,我们完全可以使用函数递归来进行模拟
这道题当然要用到DFS深度优先搜索 + 回溯算法了,可以仔细想一下,我们可以在index == 0的时候入栈,index == 1的时候可以入栈也可以出栈这样不就是一颗递归树吗
如图:
这课树怎么遍历??
我们需要记录些什么东西
- 当前的数字是多少,我们轮到下标-----index
- 我需要记录入栈的记录,用来完全模拟这个过程-----stack input
- 记录出栈的记录,这个是要打印的-----vector output
- 把对应下标所对应的值用一个数组记录下来-----vector arr
需要注意的是,题目要求的是字典序,所以我们必须先出栈再入栈,有机会马上出栈就可以达成字典序
因为这么搞的话可以从***树的遍历顺序***来知道理由
先来分析代码的主体部分(看上面的图就可以很清楚的明白为什么要回溯了)
//要求字典序,所以先选择出栈
if(input.size())//如果这个input的东西里面已经没有了,那么我们就不能够出栈了
{
int num = input.top();
intput.pop();
output.push(num);
dfs(index);//出栈的时候我们这里的index是不需要加1的
//回溯
input.push(num);
output.pop();
}
//入栈
input.push(arr[index]);
dfs(index + 1);
input.pop();
拓展:如果我们先写出栈的代码再写入栈的代码的话,那么就是字典序的完全相反的顺序的序列
递归的结束部分
if(index == n)
{
stack<int> inputtemp;//用一个临时的栈把全局的引过来,否则会把原来的栈破坏掉
vector<int> outputtemp;
while(inputtemp.size())
{
outputtemp.push_back(inputtemp.top());
inputtemp.pop();
}
}
整体代码:
#include<stack>
#include<iostream>
#include<vector>
using namespace std;
vector<int> arr;
int n;
stack<int> input;//正宫,用来模拟栈这个过程。输入push进入栈,记录入栈
vector<int> output;//这个是专门用来挪位置的。输出pop出栈,记录出栈
void dfs(int index)
{
if (index == n)
{
stack<int> intputtemp(input);
vector<int> outputtemp(output);
while (intputtemp.size())
{
int num = intputtemp.top();
intputtemp.pop();
outputtemp.push_back(num);
}
for (auto e : outputtemp)
{
cout << e << " ";
}
cout << endl;
return;
}
//因为我们要字典序,所以要先出栈
if (input.size())
{
int num = input.top();
input.pop();
output.push_back(num);
dfs(index);
input.push(num);
output.pop_back();
}
//再考虑入栈
input.push(arr[index]);
dfs(index + 1);
input.pop();
}
int main()
{
cin >> n;
arr.resize(n + 1);
for (int i = 0; i < n; i++)
{
arr[i] = i + 1;
}
dfs(0);
return 0;
}
栈与递归的思想升华:用递归把栈逆序
如果我们想把栈逆序的话,我们可以准备另外一个栈来导入,但是这里我们要求必须使用递归。
递归要有黑盒思想:即你定义一个递归,并且相信这个递归可以办到这件事,相当于这个递归就是一个可以完成任务的函数一样,不必太关心函数里面是怎么实现的
例如:栈里面有几个元素:
如图:
我要把这个栈逆序的话,最原始的思维就是,把1, 2, 3的方块全部拿出来,然后把第一个拿出来了的方块放到栈底,假如我们是小孩儿的话也会这么做,因此我们定义函数。
我们发现,我们需要的是第一个出栈的数字到时候要第一个进栈,也就是说先进先出,这不是栈的结构特征啊,是队列的,但是我们的函数的行为特征是栈的,而不是队列的,所以我们很难使用一个很简单的单元递归来解决这个问题,我们必须要使用某种方式,让递归完成队列的操作,数据结构中我们使用两个stack,那么我们也可以使用两个递归来实现这个想法
public static void reverse(Stack<Integer> stack)
{
if(stack.isEmpty())
{
return ;
}
int i = f(stack);//这个f函数的含义是,我们先递归拿到栈底的元素,然后再把栈底的元素删除掉
reverse(stack);
stack.push(i);
}
//这个递归的写法应该是这样的
public static int f(stack<Integer> stack)
{
int result = stack.pop();//先把result引出来的巧妙效果就是可以把最后一个元素无视掉,这样刚好达成了删除的目的
if(stack.isEmpty())
{
return result;
}
else
{
int last = f(stack);
stack.push(result);
return last;
}
}
本文未完待续,我所有的文章以后都会补充。。。。