数据结构学习笔记 1-3 递归与栈(Stack)解决表达式求值 与 LeetCode真题图解(Java)

喜欢该类型文章可以给博主点个关注,博主会持续输出此类型的文章,知识点很全面,再加上LeetCode的真题练习,每一个LeetCode题解我都写了注释,比较适合新手入门数据结构与算法,后续也会更新进阶的文章。
课件参考—开课吧《门徒计划》

1-3 递归与栈(Stack)解决表达式求值

栈基础知识

上篇文章我们学习了队列,队列是先进先出,两端都可以进行操作。

而本章我们学习的数据结构是,栈只有一端可以操作,它只能在一个头进出,而且还是先进后出

栈的数据结构比队列更严谨一些,它没有队列那些花里胡哨的东西,什么循环啊,什么链式结构等。

栈很简单,只要描述的是从一头进,然后还是从这头出,那么它就是一个栈。

栈就是堵住了一头的队列。

栈的插入删除对应着入栈出栈

栈的出栈

我们需要在物理层面把④这个元素删掉吗?这样做是没有必要的,我们直接让top值减1,在栈中top指向谁,谁就是栈顶元素。

image-20220114194316181

栈的入栈

直接让top加1,往上移一位,并且把top指向的地方变成我们想要插入的元素。

image-20220114195113248

栈适合解决的问题

image-20220114195825440
本题链接 (文章后面会讲这个题,先给大家铺垫一下思想,很重要)

看我们的括号是否合法, 如果是这样的(())([{}]),就是合法的,如果是这样的())[{],就是不合法的。

题中有三个类型的括号,我们先将问题简化一下:

image-20220114201907524

是否所有的左括号(都能找到对应的右括号)

image-20220114202523611

image-20220114202908374

代码实现

image-20220114203140736

这样我们不用栈也实现了这个括号匹配的问题,但我们栈的意义在哪里呢?我们想一下程序还能不能更优化,我们真的需要两个变量来记录我们左括号和右括号的数量吗?我们来看下图的代码优化:

image-20220114203453152

我们将rnum这个变量去掉了,碰到左括号lnum + 1,碰到右括号lnum - 1,如果在遍历过程中lnum小于0,那说明右括号的数量超过了左括号,所以我们只用一个变量就完成了这个程序。

image-20220114203735733

学习重点:用其他抽象的思想来模拟我们栈的形式。

只要它能描述为我从一头进,并且从同样的一头出,那么它就是,从这题来看,+1就是我们的入栈,-1就是我们的出栈,再抽象一点,左括号(就是我们的进栈,右括号)就是我们的出栈,当括号的顺序是这样时:()),就是非法操作,因为我在对一个空栈进行出栈操作,有的时候我们想出了一个操作,但却发现跟我们栈的模式是非常相似的,其实栈跟我们的队列一样,无处不在。

对于第三点,一对()可以等价为一个完整的事件,什么是完整的事件?我们可以假设一个函数:

funA() {
	...
}

有开始有结束,这就是一个完整的事件。

那对于第四点,(())可以看做事件于事件之间的完全包含关系,我们可以看做是:

funA() {
	funB() {
		...
	}
}

可以理解为两个函数之间的包含关系。

一对()不能单纯的只理解为是一对括号,它可能是函数的调用,也可能是一种二叉树的结构,有可能有很多种表现形式。

最终的,能表示一个东西的开始和结束,然后里面包含着小的事件,那么它就是一个的表现形式。

image-20220114205738768

我们想解决左括号(和右括号的问题),那我们应该求解中间被包含的问题,然后我们不断地拆解,最后拆解成一个我们已知的问题。

image-20220114210817050

为什么递归可以解决那种很复杂的问题,它其实用的就是栈的思想,以斐波那契数列举例,f(n) = f(n - 1) + f(n - 2) ,它其实就可以理解为是f(n)完全包含f(n - 1) + f(n - 2) ,然后最后递到我们已知的值,f(0) = 1, f(1) = 1,再逐步的归回去。

通过以上案例,我们应该更加理解了这个数据结构。

栈的典型应用场景

image-20220115151656059

大家看到这个图是不是有些熟悉呢?对了,这个就是我们上篇文章的队列,讲到过的线程池,线程池中有任务队列,那线程池中的线程其实就是我们的栈,每一个线程就是一个线程栈,我们线程空间的局部变量本质上就是存储到我们的栈空间里,我们再用函数的定义深一步的理解:

funA() {
	int a, b, c;
	
	funB() {
		int d;
	}
}

我们先执行funA()funA()函数定义了a,b,c,所以a,b,c先入栈,然后执行到funB()funB()函数定义了d,d也入栈,之后funB()执行完,d随之出栈,然后funA()执行完,c,b,a也按顺序出栈。

我们在一个函数中定义的局部变量只能在该函数中使用,出了这个函数就用不了这个变量了,就是因为这个栈

image-20220115155521198

image-20220115162005577

我们把这个3 + 5转换成二叉树的形式,那么问题就变成了:当我执行到了这个加法的时候,我就把左子树的3和右子树的5拿出来,然后再把根节点的+拿出来,变成3 + 5的形式解决这个问题,但是这个转换大家是不是觉得有点脑残哈哈哈😂,那我们来看下面的图:

image-20220117230338476

我们稍微复杂一点,4 + 5是我们刚才实现过的小问题,然后在看根节点*,执行*时会调用3+,但是根节点不知道+是什么,所以+就会去找4和5,然后返回这两个值,就可以进行3 * (4 + 5)的运算了;这个时候我们就借助这样的一个思想,既然+号这个大问题我们解决不了,我们就缩小这个问题的数据规模范围,我们把大规模变为小规模,逐一缩小到我们能解决的问题。

正常实现这个表达式求值我们可以搞一个逆波兰式,借助两个栈,一个是数据栈,一个是符号栈,来回导解决这个问题。

我们先找到未知的父问题,然后逐步的拆解成我们已知的子问题求解。

其实这也就是递归就是我们一步一步的找子问题,当找到已知的子问题时,再一步一步的回去。

我们实现这个代码的话,要先找到优先级最高的计算符号。

代码实现

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while (true) {
            char[] str = sc.next().toCharArray();
            System.out.println(calc(str, 0, str.length - 1));
        }
    }

    // 计算符号的优先级
    private static int calc(char[] str, int l, int r) {
        int pos = -1, pri = Integer.MAX_VALUE - 1, temp = 0;
        for (int i = l; i <= r; i++) {
            int cur = Integer.MAX_VALUE;
            switch (str[i]) {
                case '(': temp += 9999; break; // ()的优先级最高,temp的权重应最大
                case ')': temp -= 9999; break;
                case '+':
                case '-': cur = temp + 1; break;
                case '*':
                case '/': cur = temp + 2; break;
            }
            if (cur <= pri) {
                pos = i;
                pri = cur;
            }
        }
        if (pos == -1) { // 表明运算式中没有符号
            int num = 0;
            for (int i = l; i <= r; i++) {
                if (str[i] < '0' || str[i] > '9') continue;
                num = num * 10 + (str[i] - '0');
            }
            return num;
        }
        int a = calc(str, l, pos - 1); // 记录符号左边的值
        int b = calc(str, pos + 1, r); // 记录符号右边的值

        switch (str[pos]) {
            case '+': return a + b;
            case '-': return a - b;
            case '*': return a * b;
            case '/': return a / b;
        }

        return 0;
    }
}

LeetCode真题

经典面试题—栈的基本操作

LeetCode面试题 03.04.化栈为队

难度:easy

用两个栈实现一个队列

我们s2是队列的尾,进行push操作,s1是队列头,进行pop操作。

image-20220118182611025

但是我们将元素pushs2后,想弹出我们最开始push的元素要怎么做呢?元素是在栈底的,所以我们要将s2的元素都放到s1里面

image-20220118182905031

然后s1就可以从栈顶pop元素了,这样就用两个栈实现了一个队列。

image-20220118182931331

只要s1中有元素,那么pop肯定轮不到s2,肯定是s1中的元素先pop,所以说我们每次pop时都可以判断一下s1是否为空,如果s1是空那就要把s2的元素都搬运到s1里面去,然后再pop

push是肯定在s2中进行。

我们直接用系统栈Stack实现。

LeetCode题解:代码实现


LeetCode682.棒球比赛

难度:easy

四个规则转换为栈的形式:

  1. 入栈
  2. 入栈元素是前两个栈顶元素相加的值
  3. 入栈元素是当前栈顶元素 * 2
  4. 出栈

这个题非常非常简单,就是考察对栈的理解,以及非常基础的编码能力。

LeetCode题解:代码实现


LeetCode946.验证栈序列

难度:mid

一道非常经典的面试题,给我们两串数字,第一串是入栈顺序,第二串是出栈顺序,问第二串的出栈顺序有没有可能由第一串的入栈顺序所形成,看示例1,我们并不一定是要全部执行完push再进行pop,有可能元素还没有全部push进去我们就将某一元素pop出去。

image-20220118224810125

我们可以模拟一下,如果我们栈顶元素跟pop的元素不相等我们就一直往里pushpush进1的时候栈顶元素不是4,那我们就想办法让它变为4,我们就一直push到4,然后再pop,再看栈顶元素是不是5,不是5就继续push,5push入栈之后就可以pop出栈了,随后3 2 1依次pop

LeetCode题解:代码实现

经典面试题—栈结构扩展应用

LeetCode20.有效的括号

难度:easy

来了来了,刚才铺垫的这个题终于来了,直接上图:

image-20220119185825150

我们遍历给定字符串s。

当我们遇到一个左括号时,我们会希望在后面的字符中,有一个相同类型的右括号将其闭合,由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。

当我们遇到一个右括号时,我们就可以弹出栈顶的左括号,判断两个括号是否能闭合,如果不是相同类型的括号或者栈中没有括号,我们这个字符串就是无效的。

为了能快速判断括号的类型,我们可以创建一个Map帮助我们。

创建一个栈来存储我们的左括号。

开始遍历我们的字符串,如果是左括号,我们就将该字符加入到栈中。

如果是右括号,我们就判断栈顶的元素和当前的元素是否能完全闭合。

1

LeetCode题解:代码实现


LeetCode1021.删除最外层的括号

难度:easy

这个题就是我们在上面说过的完全包含问题,例如这个:"(()())(())""(()())"就是事件1,"(())"就是事件2,我们要将事件最外层的括号删除,最后返回:"()()" + "()" = "()()()"

所以我们可以设置一个变量,将我们最外层的左括号和右括号过滤掉:

image-20220120190616287

opened遇到左括号'('值就+1,遇到右括号')'值就-1,我们需要取到里面的括号,直接可以用判断将外层括号过滤掉。

LeetCode题解:代码实现


LeetCode145.二叉树的后序遍历

难度:easy

二叉树的后续遍历就是:左右根

我们不难看出,这道题可以用递归来解决,而且跟我们之前举的例子:表达式求值很像,当我们此时的左右根中,左结点或者右结点形状不是单独结点时,我们就继续遍历它的左右根,此时这个问题解决不了,我们就向下解决它的子问题,形成了递归求解。

image-20220127121219905

递归的实现方式还是非常简单的,但题中有一句话:

进阶:递归算法很简单,你可以通过迭代算法完成吗?

我们使用栈来辅助我们实现迭代,我们需要两个栈来实现,一个栈负责存值,一个栈负存树的结点。

因为我们栈是先进后出,二叉树的后序遍历是左右根,我们不妨将入栈的顺序转化为:根右左,这样出栈的顺序就变为:左右根了。

image-20220206225149665

LeetCode题解:两种方法代码实现


LeetCode227.基本计算器 II

难度:mid

其实这个题就是我们上面的表达式求值问题,我们已经用递归实现了,执行代码是没问题的

image-20220206234801737

但我提交发现居然超时了

image-20220206232526993

它卡了我一个巨离谱的用例,我也是第一次遇见,忍不住吐槽一下,感兴趣的可以提交我这个代码试一下,看它这个用例到底有多长

class Solution {
    public int calculate(String s) {
        return calc(s.toCharArray(), 0, s.length() - 1);
    }
    public int calc(char[] str, int l, int r) {
        int pos = -1, pri = 999999 - 1, temp = 0;
        for (int i = l; i <= r; i++) {
            int cur = 999999;
            switch (str[i]) {
                case '(': temp += 9999; break; // ()的优先级最高,temp的权重应最大
                case ')': temp -= 9999; break;
                case '+':
                case '-': cur = temp + 1; break;
                case '*':
                case '/': cur = temp + 2; break;
            }
            if (cur <= pri) {
                pos = i;
                pri = cur;
            }
        }
        if (pos == -1) { // 表明运算式中没有符号
            int num = 0;
            for (int i = l; i <= r; i++) {
                if (str[i] < '0' || str[i] > '9') continue;
                num = num * 10 + (str[i] - '0');
            }
            return num;
        }
        int a = calc(str, l, pos - 1); // 记录符号左边的值
        int b = calc(str, pos + 1, r); // 记录符号右边的值

        switch (str[pos]) {
            case '+': return a + b;
            case '-': return a - b;
            case '*': return a * b;
            case '/': return a / b;
        }

        return 0;
    }
}

image-20220206234917140

以我目前的水平暂时还不知道怎么优化这个代码,但没过就是没过,我们要想想别的办法。

所以我们可以使用栈来完成这个题。

我们采用双栈,一个是数字栈,一个是符号栈。

但我们要注意一点,假如运算式是3 + 2 x 2,我们先将3入栈到s1中,然后将+号入栈到s2中,所有元素都入栈是这样的情况:

image-20220207121947481

当运算式的元素都入栈后,我们弹出s2的栈顶元素,再弹出s1的两个栈顶元素进行计算,将运算后的值再放进s1中,然后再进行一遍弹栈操作,求得结果。

image-20220207122306618

最后将我们求得的结果入栈到s1中。

image-20220207122439752

但我们的运算式假设是3 x 2 + 2,我们还可以像上面一样入栈了吗?如下图:

image-20220207122639913

显然是不可以的,在s2中,我们的+号在x号上面了,那也就意味着我们在进行计算的时候会先计算加号而不是乘号。

所以我们要给予符号优先级。

+号入栈时,发现栈顶元素的优先级比它要高,这时我们就将s2不停的pop,直到栈顶元素不小于它为止。

LeetCode题解:代码实现

题解中还有单栈解法,时间会比我的双栈代码优化一点。

总结

栈这个结构还是比较简单的,有很多代码实现可能都是以栈的形式来实现的,我们的目的是以栈结构来理解递归,我们学习编程语言遇到的第一个难点,就是递归,但我们学习的过程中大部分都是先学了递归,然后再学栈,但其实学完递归我们大概率也是一种很懵的状态,如果我们先学栈这个数据结构,再学递归,那就会很清晰的理解为什么递归可以把大问题化成小问题了。

我们由浅入深,用括号问题来引入栈的概念,最后得到了一个结论:栈可以处理具有完全包含关系的问题,从而我们可以用栈来解释递归问题了。

注意:
本文重点是让大家理解栈,先把栈理解好了再去理解递归,后续还会有数据结构-二叉树的文章更深层的理解递归。
文章链接:
据结构学习笔记 2-1 二叉树(Binary Tree)与 LeetCode真题(Java)

  • 49
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 45
    评论
以下是使用递归函数编写C语言算术表达式求值程序的示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <ctype.h> int get_value(char* expr); int get_term(char* expr, int* index); int get_factor(char* expr, int* index); int main() { char expr[100]; printf("Enter an arithmetic expression: "); scanf("%s", expr); int value = get_value(expr); printf("Result: %d\n", value); return 0; } // 递归函数值 int get_value(char* expr) { int index = 0; int value = get_term(expr, &index); while (expr[index] != '\0') { char op = expr[index]; if (op == '+') { index++; value += get_term(expr, &index); } else if (op == '-') { index++; value -= get_term(expr, &index); } else { printf("Invalid operator: %c\n", op); exit(1); } } return value; } // 获取项 int get_term(char* expr, int* index) { int value = get_factor(expr, index); while (expr[*index] != '\0') { char op = expr[*index]; if (op == '*') { (*index)++; value *= get_factor(expr, index); } else if (op == '/') { (*index)++; int factor = get_factor(expr, index); if (factor == 0) { printf("Division by zero!\n"); exit(1); } value /= factor; } else { break; } } return value; } // 获取因子 int get_factor(char* expr, int* index) { int value = 0; if (expr[*index] == '(') { (*index)++; value = get_value(expr); if (expr[*index] != ')') { printf("Missing closing parenthesis!\n"); exit(1); } (*index)++; } else if (isdigit(expr[*index])) { while (isdigit(expr[*index])) { value = value * 10 + (expr[*index] - '0'); (*index)++; } } else { printf("Invalid character: %c\n", expr[*index]); exit(1); } return value; } ``` 这个程序可以处理带有加减乘除运算符和括号的算术表达式。它使用了三个递归函数,分别用于整个表达式的值、获取一个项的值和获取一个因子的值。其中,`get_value()` 函数调用 `get_term()` 函数来获取每个项的值,然后根据运算符进行加或减运算。`get_term()` 函数调用 `get_factor()` 函数来获取每个因子的值,然后根据运算符进行乘或除运算。`get_factor()` 函数根据当前字符是数字还是左括号,分别获取数字或者递归计算括号内的表达式的值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 45
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小成同学_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值