栈行为和递归行为

栈与递归

栈的特征:后进先出,也就是说先入栈的,最后才出的来。

函数递归:函数递归的时候需要创建函数栈帧,它的行为根栈的特征很相似。

递归有两种,第一种,线性深入。第二种,树形遍历

如图:

在这里插入图片描述

什么时候使用栈?

原理:当你发现问题的过程根栈的运动轨迹很相似的时候。

常见的有:

匹配型问题

括号序列

给定一个长度为 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. 如果最后栈不为空,那么说明,没有右括号了,但是还有左括号没有被匹配,也是错误的。

字符串处理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;
}

需要注意的点是:

  1. 我们用栈进行了模拟,我们pop之后一定要top–
  2. 因为我们每次都必定循环到可以pop的地方,所以这里不可以写else,不然就不会输入任何pop

第二种思路:

题目链接:

在这里插入图片描述

https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&&tqId=11174&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking

代码如下:

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的时候可以入栈也可以出栈这样不就是一颗递归树吗

如图:
在这里插入图片描述

这课树怎么遍历??

我们需要记录些什么东西

  1. 当前的数字是多少,我们轮到下标-----index
  2. 我需要记录入栈的记录,用来完全模拟这个过程-----stack input
  3. 记录出栈的记录,这个是要打印的-----vector output
  4. 把对应下标所对应的值用一个数组记录下来-----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;
    }
}

本文未完待续,我所有的文章以后都会补充。。。。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

胡桃姓胡,蝴蝶也姓胡

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值