栈知识及编程练习总结

目录

一、背景知识

二、栈的应用

(一)在Spring或计算机科学中的应用举例

(二)实际开发中的应用举例

三、相关编程练习

1、有效的括号(Valid Parentheses)

2、最小栈(Min Stack)

3、每日温度(Daily Temperatures)

4、用栈实现队列(Implement Queue using Stacks)

5、用队列实现栈(Implement Stack using Queues)

6、接雨水(Trapping Rain Water)

7、逆波兰表达式求值(Evaluate Reverse Polish Notation)

8、基本计算器(Basic Calculator)

9、简化路径(Simplify Path)

10、岛屿数量

扩展:其他最优解法展示

11、用数组实现一个栈

12、基本数学运算表达式求值

13、 IP 范围判断


干货分享,感谢您的阅读!祝你逢考必过!

一、背景知识

栈(Stack)是一种线性数据结构,它遵循后进先出(Last-In-First-Out,LIFO)的原则,这意味着最近添加的元素最先被访问,而最先添加的元素最后被访问。这种特性使栈非常适合用于某些应用,比如函数调用、表达式求值、括号匹配、回溯等。

当我们需要保存一些数据,而且需要以特定的顺序进行访问时,栈就是一个非常有用的数据结构。它通常有以下三种基本操作:

  1. 入栈(Push):向栈中添加元素。元素会被添加到栈顶,也就是最后一个元素之后。
  2. 出栈(Pop):从栈中移除并返回栈顶元素。栈顶元素是最后一个入栈的元素。
  3. 查看栈顶元素(Peek):查看栈顶元素,但是不从栈中移除它。

另外,还有两个与栈相关的概念:栈底和栈顶。栈底是栈的第一个元素,而栈顶是最后一个元素。因为栈是后进先出的,所以最后进入栈的元素是在栈顶,而最先进入栈的元素是在栈底。

栈还有一些重要的特点,如下所示:

  1. 空栈:如果栈中没有任何元素,则称之为空栈。
  2. 栈的大小:栈的大小表示栈中可以存储多少元素。当栈的大小已满时,再次尝试将元素入栈会导致栈溢出。
  3. 动态栈:动态栈是一种特殊类型的栈,可以根据需要自动增加或减少大小。这种栈的大小不是固定的,而是根据需要动态变化的。

按照以上的背景,展示举例:在写程序的时候,会碰到函数 A 要调用函数 B,函数 B 要调用函数 C 这样连续的处理。这个时候,在函数 A 的上面是函数 B,函数 B 的上面是函数 C。(图来自图灵社区

二、栈的应用

(一)在Spring或计算机科学中的应用举例

Spring 底层中,栈数据结构经常用于实现方法调用栈。Spring 框架的核心是 IoC 容器和 AOP 框架,而这些框架的实现都需要大量的方法调用。当我们使用 Spring 调用一个方法时,Spring 会将该方法及其参数压入一个方法调用栈中,然后执行该方法。如果该方法又调用了其他方法,则 Spring 会将这些方法和它们的参数也压入方法调用栈中,直到所有方法执行完毕并返回结果。

此外,在 Web 应用程序中,栈数据结构也经常用于处理 HTTP 请求。当一个请求到达服务器时,Web 服务器会将该请求添加到请求队列中,然后使用栈数据结构来处理请求。每个请求都可以看作是一个方法调用,因此服务器可以使用栈来实现方法调用栈,以便管理请求处理流程。

当然,栈数据结构在计算机科学中有许多其他应用,以下是一些例子:

  1. 编译器和解释器中,栈常用于存储编译或解释器上下文,以便实现程序的递归或迭代执行。
  2. 操作系统中,栈被用于存储程序调用栈、系统调用和中断处理。
  3. 数据库管理系统中,栈被用于实现查询操作的解析和执行。
  4. 网络路由器中,栈被用于存储路由表,以便实现路由选择算法。
  5. 图形学中,栈被用于存储矩阵堆栈,以便实现变换操作。
  6. 数学中,栈被用于实现逆波兰表达式和表达式求值等算法。
  7. 计算机图形学中,栈被用于实现渲染管道,以便对图像进行各种变换和处理。

总之,栈数据结构是计算机科学中非常常见和有用的数据结构,被广泛应用于许多领域,包括编程语言、操作系统、数据库管理系统、网络通信、图形学和数学等。

(二)实际开发中的应用举例

在实际开发中,栈数据结构的应用也非常广泛。以下是一些实际开发中常见的栈应用举例:

  1. 浏览器的前进和后退功能。在浏览器中,用户可以通过点击“后退”和“前进”按钮来浏览他们之前访问过的网页。这些网页的访问历史被存储在栈中,每当用户点击“后退”或“前进”按钮时,浏览器就会从栈中弹出(或推入)一个网页。
  2. 程序调用堆栈。在计算机程序中,方法调用通常被实现为栈结构。每当一个方法被调用时,它的参数和返回地址会被压入栈中,然后在该方法执行完毕后被弹出。
  3. 表达式求值。在编写编译器或计算器程序时,常常需要实现表达式求值算法。这些算法通常使用栈来实现中间计算结果的存储和操作。
  4. 括号匹配。在编写程序时,常常需要对括号进行匹配和检查。这个问题可以通过使用栈来解决,将左括号压入栈中,遇到右括号时弹出栈顶元素并检查是否匹配。
  5. 逆波兰表达式求值。逆波兰表达式是一种无括号的数学表达式,使用栈可以方便地对逆波兰表达式进行求值。
  6. 回文字符串检查。回文字符串是一种正反读都相同的字符串,例如“level”和“racecar”。这个问题可以通过使用栈来解决,将字符串的一半字符压入栈中,然后从栈中弹出元素并与剩余字符进行比较。
  7. 缓存回退。在一些应用程序中,用户可能需要在多个页面之间切换。为了提高性能,程序通常会使用栈来缓存用户的页面历史,以便快速回退到之前的页面。

三、相关编程练习

1、有效的括号(Valid Parentheses)

题目描述:给定一个只包括括号的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

示例 1:输入:s = "()"            输出:true

示例 2:输入:s = "()[]{}"       输出:true

示例 3:输入:s = "(]"            输出:false

示例 4:输入:s = "([)]"          输出:false

示例 5:输入:s = "{[]}"          输出:true

提示:

  1. 1 <= s.length <= 104
  2. s 仅由括号 '()[]{}' 组成

解题思路

这道题的最优解是使用栈来解决。

我们可以遍历给定字符串,对于每个左括号,将其对应的右括号压入栈中,当遇到右括号时,弹出栈顶元素并判断是否与该右括号相同,若相同则继续遍历,若不相同则该字符串不合法。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 给定一个只包括括号的字符串 s ,判断字符串是否有效。
 * 有效字符串需满足:左括号必须用相同类型的右括号闭合。
 * 左括号必须以正确的顺序闭合。
 * 示例 1:输入:s = "()"            输出:true
 * 示例 2:输入:s = "()[]{}"        输出:true
 * 示例 3:输入:s = "(]"            输出:false
 * 示例 4:输入:s = "([)]"          输出:false
 * 示例 5:输入:s = "{[]}"          输出:true
 * <p>
 * 提示:
 * 1 <= s.length <= 104
 * s 仅由括号 '()[]{}' 组成
 * @date 2023/4/8  22:17
 */
public class ValidStr {

    /**
     * 最优解是使用栈来解决。
     * <p>
     * 我们可以遍历给定字符串,
     * 对于每个左括号,将其对应的右括号压入栈中,
     * 当遇到右括号时,弹出栈顶元素并判断是否与该右括号相同,
     * 若相同则继续遍历,若不相同则该字符串不合法。
     */
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '(' || c == '[' || c == '{') {
                stack.push(c);
            } else {
                if (stack.isEmpty()) {
                    return false;
                }
                char top = stack.peek();
                if ((top == '(' && c == ')') 
                        || (top == '[' && c == ']') 
                        || (top == '{' && c == '}')) {
                    stack.pop();
                } else {
                    return false;
                }
            }
        }
        return stack.isEmpty();
    }

    public static void main(String[] args) {
        /*验证有效的括号代码*/
        String s = "()[]{}";
        boolean result = new ValidStr().isValid(s);
        System.out.println(result);

        s = "(]";
        result = new ValidStr().isValid(s);
        System.out.println(result);

        s = "([)]";
        result = new ValidStr().isValid(s);
        System.out.println(result);

        s = "{[]}";
        result = new ValidStr().isValid(s);
        System.out.println(result);
    }
}

2、最小栈(Min Stack)

题目描述:设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) —— 将元素 x 推入栈中。
  • pop() —— 删除栈顶的元素。
  • top() —— 获取栈顶元素。
  • getMin() —— 检索栈中的最小元素。

示例:

输入:["MinStack","push","push","push","getMin","pop","top","getMin"]

[[],[-2],[0],[-3],[],[],[],[]]

输出:[null,null,null,null,-3,null,0,-2]

解释:

MinStack minStack = new MinStack();

minStack.push(-2);

minStack.push(0);

minStack.push(-3);

minStack.getMin(); // 返回 -3

minStack.pop();

minStack.top(); // 返回 0

minStack.getMin(); // 返回 -2

提示:

  • pop、top 和 getMin 操作总是在 非空栈 上调用。

解题思路

该问题的最优解是使用一个栈和一个辅助栈来实现。

在栈中存储元素,辅助栈中存储当前栈中的最小值。

每次压入元素时,将其压入栈中,同时比较该元素和辅助栈的栈顶元素,若小于等于辅助栈栈顶元素,则将其也压入辅助栈中。

在弹出元素时,若该元素等于辅助栈的栈顶元素,则同时弹出该元素和辅助栈的栈顶元素。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
 * <p>
 * push(x) —— 将元素 x 推入栈中。
 * pop() —— 删除栈顶的元素。
 * top() —— 获取栈顶元素。
 * getMin() —— 检索栈中的最小元素。
 * @date 2023/4/8  22:29
 */
public class MinStack {
    Stack<Integer> stack = new Stack<>();
    Stack<Integer> minStack = new Stack<>();

    public MinStack() {
    }

    public void push(int val) {
        stack.push(val);
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }

    public void pop() {
        if (stack.peek().equals(minStack.peek())) {
            minStack.pop();
        }
        stack.pop();
    }

    public int top() {
        return stack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }

    public static void main(String[] args) {
        /*验证最优解*/
        MinStack minStack = new MinStack();
        minStack.push(-2);
        minStack.push(0);
        minStack.push(-3);
        /*-3*/
        System.out.println(minStack.getMin());
        minStack.pop();
        /*0*/
        System.out.println(minStack.top());
        /*-2*/
        System.out.println(minStack.getMin());
    }
}

3、每日温度(Daily Temperatures)

题目描述:给定一个数组 T,求出 T 中每个元素与后面第一个比它大的元素之间的距离。

如果不存在这样的元素,则对应位置的值为 0

例如,给定数组 T = [73, 74, 75, 71, 69, 72, 76, 73],应返回 [1, 1, 4, 2, 1, 1, 0, 0]

提示:每个温度值 T[i] 的范围是 [30, 100]

解题思路

该问题可以使用单调栈来解决,单调栈的基本思路是维护一个栈,栈中的元素单调递减或递增。

对于本题,我们需要维护一个单调递减的栈,存储数组 T 中元素的下标,每次将一个元素压入栈中时,如果该元素比栈顶元素对应的元素大,则栈顶元素出栈,并且计算栈顶元素到该元素的距离,将距离存储到对应的结果数组中。直到该元素比栈顶元素小,将该元素入栈。

具体实现时,我们遍历数组 T,并将每个元素的下标入栈。当遇到一个比栈顶元素对应的元素大的元素时,我们可以通过栈顶元素的下标计算出距离,并将其存储到结果数组中。重复该过程直到遍历完整个数组。

时间复杂度为O(n),其中n是数组 T 的长度。空间复杂度为O(n)。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;

/**
 * @author yanfengzhang
 * @description 给定一个数组 T,求出 T 中每个元素与后面第一个比它大的元素之间的距离。
 * <p>
 * 如果不存在这样的元素,则对应位置的值为 0。
 * 例如,给定数组 T = [73, 74, 75, 71, 69, 72, 76, 73],应返回 [1, 1, 4, 2, 1, 1, 0, 0]。
 * 提示:每个温度值 T[i] 的范围是 [30, 100]。
 * @date 2023/4/8  22:39
 */
public class DailyTemperatures {

    /**
     * 遍历数组 T,并将每个元素的下标入栈。
     * 当遇到一个比栈顶元素对应的元素大的元素时,
     * 我们可以通过栈顶元素的下标计算出距离,并将其存储到结果数组中。
     * 重复该过程直到遍历完整个数组。
     * <p>
     * 时间复杂度为O(n),其中n是数组 T 的长度。空间复杂度为O(n)。
     */
    public int[] dailyTemperatures(int[] T) {
        int n = T.length;
        int[] res = new int[n];
        Deque<Integer> stack = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            while (!stack.isEmpty() && T[i] > T[stack.peek()]) {
                int j = stack.pop();
                res[j] = i - j;
            }
            stack.push(i);
        }
        return res;
    }

    public static void main(String[] args) {
        int[] T = {73, 74, 75, 71, 69, 72, 76, 73};
        int[] res = new DailyTemperatures().dailyTemperatures(T);
        /*输出 [1, 1, 4, 2, 1, 1, 0, 0]*/
        System.out.println(Arrays.toString(res));
    }
}

4、用栈实现队列(Implement Queue using Stacks)

题目描述:使用栈实现队列,使得队列的操作 push、pop、peek、empty 的时间复杂度均为 O(1)

解题思路

两个栈实现队列的思路如下:

我们用两个栈stack1和stack2来模拟队列的操作。stack1负责入队列,stack2负责出队列。当我们需要入队列时,我们将元素压入stack1中;当需要出队列时,如果stack2为空,我们将stack1中的元素逐一弹出并压入stack2中,这样stack2中的元素就是按照队列先进先出的顺序排列。我们从stack2中弹出栈顶元素即可。

实现过程中,我们需要注意两点:

  1. 当stack2不为空时,不能将元素从stack1中压入stack2中,否则会打乱先进先出的顺序。
  2. 当stack1和stack2都为空时,队列为空,无法执行出队列操作,需要进行特殊处理。

综上所述,我们可以用两个栈来实现队列,时间复杂度为O(1)。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 使用栈实现队列,
 * 使得队列的操作 push、pop、peek、empty 的时间复杂度均为 O(1)
 * @date 2023/4/8  22:55
 */
public class ImplementQueueUseStacks {
    static class MyQueue {
        private Stack<Integer> stack1;
        private Stack<Integer> stack2;

        /**
         * Initialize your data structure here.
         */
        public MyQueue() {
            stack1 = new Stack<>();
            stack2 = new Stack<>();
        }

        /**
         * Push element x to the back of queue.
         */
        public void push(int x) {
            stack1.push(x);
        }

        /**
         * Removes the element from in front of queue and returns that element.
         */
        public int pop() {
            if (stack2.isEmpty()) {
                while (!stack1.isEmpty()) {
                    stack2.push(stack1.pop());
                }
            }
            return stack2.pop();
        }

        /**
         * Get the front element.
         */
        public int peek() {
            if (stack2.isEmpty()) {
                while (!stack1.isEmpty()) {
                    stack2.push(stack1.pop());
                }
            }
            return stack2.peek();
        }

        /**
         * Returns whether the queue is empty.
         */
        public boolean empty() {
            return stack1.isEmpty() && stack2.isEmpty();
        }
    }

    public static void main(String[] args) {
        MyQueue queue = new MyQueue();

        queue.push(1);
        queue.push(2);
        /*输出 1 */
        System.out.println(queue.peek());
        /*输出 1 */
        System.out.println(queue.pop());
        /*输出 false */
        System.out.println(queue.empty());
    }
}

5、用队列实现栈(Implement Stack using Queues)

题目描述:使用队列实现栈的下列操作:

  • push(x) -- 元素 x 入栈
  • pop() -- 移除栈顶元素
  • top() -- 获取栈顶元素
  • empty() -- 返回栈是否为空

注意:

  • 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
  • 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列,只要是标准的队列操作即可。

解题思路

使用两个队列实现栈,需要对其中一个队列进行逆序操作,这样每次出栈时都可以直接从逆序的队列中获取到栈顶元素,从而实现了栈的功能。

具体实现过程如下:

  • 定义两个队列,用来模拟栈的操作;
  • 入栈时,将元素插入队列1中;
  • 出栈时,将队列1中的元素依次取出并插入到队列2中,直到队列1中只剩下一个元素,然后将这个元素出队并返回;
  • 获取栈顶元素时,同样先将队列1中的元素依次取出并插入到队列2中,直到队列1中只剩下一个元素,然后返回这个元素;
  • 判断栈是否为空,即判断队列1和队列2是否都为空。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.LinkedList;
import java.util.Queue;

/**
 * @author yanfengzhang
 * @description 使用队列实现栈的下列操作:
 * <p>
 * push(x) -- 元素 x 入栈
 * pop() -- 移除栈顶元素
 * top() -- 获取栈顶元素
 * empty() -- 返回栈是否为空
 * 注意:
 * 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
 * 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列,只要是标准的队列操作即可。
 * @date 2023/4/8  23:30
 */
public class ImplementStackUseQueues {

    static class MyStack {
        private Queue<Integer> q1;
        private Queue<Integer> q2;

        /**
         * Initialize your data structure here.
         */
        public MyStack() {
            q1 = new LinkedList<>();
            q2 = new LinkedList<>();
        }

        /**
         * Push element x onto stack.
         */
        public void push(int x) {
            q1.offer(x);
            while (!q2.isEmpty()) {
                q1.offer(q2.poll());
            }
            Queue<Integer> tmp = q1;
            q1 = q2;
            q2 = tmp;
        }

        /**
         * Removes the element on top of the stack and returns that element.
         */
        public int pop() {
            return q2.poll();
        }

        /**
         * Get the top element.
         */
        public int top() {
            return q2.peek();
        }

        /**
         * Returns whether the stack is empty.
         */
        public boolean empty() {
            return q2.isEmpty();
        }
    }


    public static void main(String[] args) {
        MyStack myStack = new MyStack();

        myStack.push(1);
        myStack.push(2);
        myStack.push(3);

        /*3 */
        System.out.println(myStack.pop());
        /*2*/
        System.out.println(myStack.top());
        /*false*/
        System.out.println(myStack.empty());

        myStack.pop();
        myStack.pop();
        /*true*/
        System.out.println(myStack.empty());
    }
}

6、接雨水(Trapping Rain Water)

题目描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能够捕获多少雨水。

解题思路

以下是使用栈实现的思路:

  1. 定义一个栈,并初始化为空。
  2. 遍历数组,如果当前元素小于等于栈顶元素,则将其下标压入栈中。
  3. 如果当前元素大于栈顶元素,则弹出栈顶元素,并计算雨水量:以弹出元素为高度,当前元素和栈顶元素所在位置之间的距离为宽度,宽度和高度求乘积得到雨水量。
  4. 计算完雨水量后,将当前元素下标入栈,重复步骤2和3,直到遍历完数组。
  5. 返回雨水总量。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,
 * 计算按此排列的柱子,下雨之后能够捕获多少雨水
 * @date 2023/4/8  23:48
 */
public class TrappingRainWater {

    /**
     * 在这个实现中,我们首先定义了一个空栈和一个雨水总量的变量 water。
     * 然后,我们遍历高度数组。对于每个高度,我们在栈不为空且当前高度大于栈顶元素高度的情况下,执行以下操作:
     * 取出栈顶元素,作为坑洼的底部。
     * 如果此时栈为空,则无法存储雨水,跳出循环。
     * 计算坑洼的左右两侧边界。
     * 根据左右两侧边界以及底部高度计算雨水量,并将其加入 water 变量中。
     * <p>
     * 在每次处理完高度后,我们将当前下标压入栈中,以便后续计算。最后,我们返回 water 变量即可。
     */
    public int trap(int[] height) {
        Stack<Integer> stack = new Stack<>();
        int water = 0;
        for (int i = 0; i < height.length; i++) {
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                /*取出栈顶元素作为坑洼的底部*/
                int bottom = stack.pop();
                /*如果栈为空,那么无法存储雨水,跳出循环*/
                if (stack.isEmpty()) {
                    break;
                }
                /*计算坑洼的左右两侧边界*/
                int left = stack.peek();
                int right = i;
                /*根据左右两侧边界以及底部高度计算雨水量*/
                int width = right - left - 1;
                int heightDiff = Math.min(height[left], height[right]) - height[bottom];
                water += width * heightDiff;
            }
            /*将当前下标压入栈中*/
            stack.push(i);
        }
        return water;
    }

    /**
     * 在这个示例中,我们将给定的高度数组设置为 height = {0,1,0,2,1,0,1,3,2,1,2,1} ,
     * 期望的结果是能够计算出接到的雨水的总量。
     * 调用 trap(height) 方法并将返回结果打印到控制台上,预期输出结果应该是 6。
     */
    public static void main(String[] args) {
        int[] height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};
        int res = new TrappingRainWater().trap(height);
        /*输出结果应为6*/
        System.out.println(res);
    }


}

7、逆波兰表达式求值(Evaluate Reverse Polish Notation)

题目描述:逆波兰表达式(Reverse Polish Notation,简称RPN),也叫后缀表达式,是一种数学表达式的写法,其中运算符在操作数的后面。例如,将中缀表达式 3 + 4 * 2 / (1 - 5) 转换为逆波兰表达式的过程如下:3 4 2 * 1 5 - / +

给定一个包含正整数、加减乘除符号以及空格的字符串 tokens,请你利用栈的思想,计算该表达式的值。

注意:

  • 整数除法仅保留整数部分。
  • 给定的 tokens 数组中,除数不为 0。
  • tokens 数组长度不超过 1000。

原题目出处:LeetCode链接

解题思路

该题的最优解法是使用栈数据结构,遍历表达式数组 tokens,遇到数字则将其压入栈中,遇到运算符则从栈中取出两个数字进行运算,然后将运算结果再次压入栈中。遍历完表达式数组后,栈顶元素即为表达式的计算结果。

具体算法如下:

  1. 初始化一个空栈 stack
    • 遍历表达式数组 tokens,对于每一个元素 token
    • 如果 token 是数字,则将其转换为整数并压入栈中。
    • 如果 token 是运算符,则从栈中取出两个数字进行运算,然后将运算结果再次压入栈中。
  2. 遍历完表达式数组后,栈顶元素即为表达式的计算结果。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 逆波兰表达式(Reverse Polish Notation,简称RPN),也叫后缀表达式,
 * 是一种数学表达式的写法,其中运算符在操作数的后面。
 * 例如,将中缀表达式 3 + 4 * 2 / (1 - 5) 转换为逆波兰表达式的过程如下:3 4 2 * 1 5 - / +。
 * <p>
 * 给定一个包含正整数、加减乘除符号以及空格的字符串 tokens,请你利用栈的思想,计算该表达式的值。
 * @date 2023/4/8  23:56
 */
public class EvaluateReversePolishNotation {

    /**
     * 最优解法是使用栈数据结构,遍历表达式数组 tokens,
     * 遇到数字则将其压入栈中,遇到运算符则从栈中取出两个数字进行运算,
     * 然后将运算结果再次压入栈中。遍历完表达式数组后,栈顶元素即为表达式的计算结果。
     * <p>
     * 具体算法如下:
     * 初始化一个空栈 stack。
     * 遍历表达式数组 tokens,对于每一个元素 token:
     * 如果 token 是数字,则将其转换为整数并压入栈中。
     * 如果 token 是运算符,则从栈中取出两个数字进行运算,然后将运算结果再次压入栈中。
     * 遍历完表达式数组后,栈顶元素即为表达式的计算结果。
     */
    public int evalRPN(String[] tokens) {
        /*初始化一个空栈*/
        Stack<Integer> stack = new Stack<>();
        /*遍历表达式数组 tokens*/
        for (String token : tokens) {
            /*如果 token 是加号*/
            if (token.equals("+")) {
                /*从栈中取出第二个数字*/
                int num2 = stack.pop();
                /*从栈中取出第一个数字*/
                int num1 = stack.pop();
                /*将两个数字相加,并将运算结果压入栈中*/
                stack.push(num1 + num2);
            }
            /*如果 token 是减号*/
            else if (token.equals("-")) {
                /*从栈中取出第二个数字*/
                int num2 = stack.pop();
                /*从栈中取出第一个数字*/
                int num1 = stack.pop();
                /*将第一个数字减去第二个数字,并将运算结果压入栈中*/
                stack.push(num1 - num2);
            }
            /*如果 token 是乘号*/
            else if (token.equals("*")) {
                /*从栈中取出第二个数字*/
                int num2 = stack.pop();
                /*从栈中取出第一个数字*/
                int num1 = stack.pop();
                /*将两个数字相乘,并将运算结果压入栈中*/
                stack.push(num1 * num2);
            }
            /*如果 token 是除号*/
            else if (token.equals("/")) {
                /*从栈中取出第二个数字*/
                int num2 = stack.pop();
                /*从栈中取出第一个数字*/
                int num1 = stack.pop();
                /*将第一个数字除以第二个数字,并将运算结果压入栈中*/
                stack.push(num1 / num2);
            }
            /*如果 token 是数字*/
            else {
                /*将其转换为整数并压入栈中*/
                stack.push(Integer.parseInt(token));
            }
        }
        /*遍历完表达式数组后,栈顶元素即为表达式的计算结果*/
        return stack.pop();
    }

    public static void main(String[] args) {
        String[] tokens1 = {"2", "1", "+", "3", "*"};
        String[] tokens2 = {"4", "13", "5", "/", "+"};
        String[] tokens3 = {"10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"};
        /*expected output: 9*/
        System.out.println(new EvaluateReversePolishNotation().evalRPN(tokens1));
        /*expected output: 6*/
        System.out.println(new EvaluateReversePolishNotation().evalRPN(tokens2));
        /*expected output: 22*/
        System.out.println(new EvaluateReversePolishNotation().evalRPN(tokens3));
    }
}

8、基本计算器(Basic Calculator)

题目描述:实现一个基本的计算器来计算一个简单的字符串表达式的值。

表达式字符串可能包含左括号 ( 和右括号 ),加号 + 和减号 -,非负整数和空格。

示例 1:输入: "1 + 1".                              输出: 2

示例 2:输入: " 2-1 + 2 "                          输出: 3

示例 3:输入: "(1+(4+5+2)-3)+(6+8)".     输出: 23

注意:

你可以假设所给定的表达式都是有效的。

请不要使用内置的库函数 eval。

解题思路

使用栈存储数字和运算符,遇到括号则递归计算括号内的表达式。

  1. 初始化一个栈 stack 存储数字和运算符,和一个变量 res 存储结果,一个变量 sign 存储运算符的正负。
  2. 遍历表达式 s,如果当前字符是数字,用一个 while 循环读取整个数字并将其压入栈中;如果是加号,则将正号压入栈中,并将 sign 设为正;如果是减号,则将负号压入栈中,并将 sign 设为负;如果是左括号,则递归计算括号内的表达式,并将结果压入栈中;如果是右括号,则将栈顶的运算符弹出并计算到左括号,弹出左括号。
  3. 遍历完表达式后,将栈中剩余的数字和运算符依次弹出并计算结果。

时间复杂度:O(n),其中n是表达式的长度。空间复杂度:O(n)。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description
 * @date 2023/4/9  00:05
 */
public class BasicCalculator {

    /**
     * 使用栈存储数字和运算符,遇到括号则递归计算括号内的表达式。
     * <p>
     * 1 初始化一个栈 stack 存储数字和运算符,和一个变量 res 存储结果,一个变量 sign 存储运算符的正负。
     * 2 遍历表达式 s,如果当前字符是数字,用一个 while 循环读取整个数字并将其压入栈中;
     * 如果是加号,则将正号压入栈中,并将 sign 设为正;
     * 如果是减号,则将负号压入栈中,并将 sign 设为负;
     * 如果是左括号,则递归计算括号内的表达式,并将结果压入栈中;
     * 如果是右括号,则将栈顶的运算符弹出并计算到左括号,弹出左括号。
     * 3 遍历完表达式后,将栈中剩余的数字和运算符依次弹出并计算结果。
     */
    public int calculate(String s) {
        /*数字栈*/
        Stack<Integer> stack = new Stack<>();
        /*当前数字*/
        int num = 0;
        /*当前结果*/
        int result = 0;
        /*符号,1表示加,-1表示减*/
        int sign = 1;
        for (char c : s.toCharArray()) {
            /*如果当前字符是数字*/
            if (Character.isDigit(c)) {
                /*将数字拼接起来*/
                num = num * 10 + (c - '0');
            }
            /*如果当前字符是加号*/
            else if (c == '+') {
                /*先将之前的数字和符号加入结果中*/
                result += sign * num;
                /*重置当前数字*/
                num = 0;
                /*设置符号为加*/
                sign = 1;
            }
            /*如果当前字符是减号*/
            else if (c == '-') {
                /*先将之前的数字和符号加入结果中*/
                result += sign * num;
                /*重置当前数字*/
                num = 0;
                /*设置符号为减*/
                sign = -1;
            }
            /*如果当前字符是左括号*/
            else if (c == '(') {
                /*将当前结果入栈*/
                stack.push(result);
                /*将当前符号入栈*/
                stack.push(sign);
                /*重置当前结果*/
                result = 0;
                /*重置当前符号*/
                sign = 1;
            }
            /*如果当前字符是右括号*/
            else if (c == ')') {
                /*先将当前数字加入结果中*/
                result += sign * num;
                /*重置当前数字*/
                num = 0;
                /*取出栈顶符号乘以当前结果*/
                result *= stack.pop();
                /*加上栈顶数字*/
                result += stack.pop();
            }
        }
        /*处理最后一个数字*/
        if (num != 0) {
            result += sign * num;
        }
        return result;
    }

    public static void main(String[] args) {
        String s = "1 + (2 - 3) + 4";
        int result = new BasicCalculator().calculate(s);
        /*输出4*/
        System.out.println(result);
    }

}

9、简化路径(Simplify Path)

题目描述:给定一个字符串路径,返回其简化的绝对路径。

例如,path = "/home/", => "/home";path = "/../", => "/";path = "/home//foo/", => "/home/foo";path = "/a/./b/../../c/", => "/c"

在简化路径时,要考虑以下规则:

1.路径是由斜杠'/'分隔的一个或多个目录或文件名组成的。

2.在Unix风格的文件系统中,'.'表示当前目录,'..'表示上一级目录。

3.如果路径中包含多个连续斜杠'/',则应将它们替换为一个单独的斜杠'/'。

4.路径应以斜杠'/'开头,并且在路径末尾不应该有斜杠'/'。

请注意,返回的简化路径必须始终以斜杠'/'开头,并且在路径末尾不能有斜杠'/'。

解题思路

最优解法是使用栈。具体做法如下:

  1. 以“/”作为分隔符,将输入路径拆分成若干个部分。
    • 遍历这些部分:
    • 如果部分为空或为“.””,则不做处理,继续遍历下一个部分。
    • 如果部分为“..”,则将栈顶元素弹出,表示返回上级目录。
    • 否则,将部分压入栈中,表示进入下一级目录。
  2. 遍历完所有部分后,将栈中的元素按顺序拼接起来,得到简化后的路径。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 给定一个字符串路径,返回其简化的绝对路径。
 * 例如,path = "/home/", => "/home";path = "/../", => "/";
 * path = "/home//foo/", => "/home/foo";
 * path = "/a/./b/../../c/", => "/c"
 * @date 2023/4/9  00:17
 */
public class SimplifyPath {

    /**
     * 最优解法是使用栈。具体做法如下:
     * <p>
     * 1 以“/”作为分隔符,将输入路径拆分成若干个部分。
     * 遍历这些部分:
     * 如果部分为空或为“.””,则不做处理,继续遍历下一个部分。
     * 如果部分为“..”,则将栈顶元素弹出,表示返回上级目录。
     * 否则,将部分压入栈中,表示进入下一级目录。
     * 2 遍历完所有部分后,将栈中的元素按顺序拼接起来,得到简化后的路径。
     */
    public String simplifyPath(String path) {
        Stack<String> stack = new Stack<>();
        String[] arr = path.split("/");
        for (String s : arr) {
            if (s.equals(".") || s.equals("")) {
                /*对于"."和空字符串,不做任何操作*/
            } else if (s.equals("..")) {
                /*如果是"..", 则需要返回上一级目录*/
                if (!stack.isEmpty()) {
                    stack.pop();
                }
            } else {
                /*否则就是一个普通的路径名,需要入栈*/
                stack.push(s);
            }
        }

        /*构造简化后的路径*/
        StringBuilder sb = new StringBuilder();
        for (String s : stack) {
            sb.append("/").append(s);
        }

        /*如果栈为空,说明简化后的路径是根路径"/" */
        if (stack.isEmpty()) {
            return "/";
        }

        return sb.toString();
    }

    public static void main(String[] args) {
        String path = "/a//b/../../c/";
        /*输出:/c */
        System.out.println(new SimplifyPath().simplifyPath(path));
    }
}

10、岛屿数量

题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

 提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] 的值为 '0' 或 '1'

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/number-of-islands

解法思路

使用栈的解法可以通过深度优先搜索(DFS)来实现。遍历矩阵中每一个元素,对于每一个为1的元素,将其作为起点,进行DFS遍历并将经过的元素标记为0,统计遍历的次数即为岛屿的数量。具体实现思路如下:

  1. 遍历矩阵中的每个元素,当元素为1时,进入DFS遍历。
  2. 进入DFS遍历时,将当前元素标记为0,表示已经遍历过。
  3. 遍历当前元素的四个方向(上、下、左、右),如果该方向上的元素为1,则进入该方向继续DFS遍历。
  4. 当所有与当前元素相连通的元素都被遍历过后,回到上一个节点继续遍历。
  5. 统计遍历的次数即为岛屿的数量。

这种解法的时间复杂度为O(mn),其中m和n分别为矩阵的行数和列数。空间复杂度为O(mn),因为需要使用一个二维数组来标记每个元素是否已经遍历过。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 给你一个由'1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
 * 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
 * 此外,你可以假设该网格的四条边均被水包围。
 * @date 2023/4/9  15:35
 */
public class NumIslands {

    /**
     * 具体实现思路如下:
     * <p>
     * 遍历矩阵中的每个元素,当元素为1时,进入DFS遍历。
     * 进入DFS遍历时,将当前元素标记为0,表示已经遍历过。
     * 遍历当前元素的四个方向(上、下、左、右),如果该方向上的元素为1,则进入该方向继续DFS遍历。
     * 当所有与当前元素相连通的元素都被遍历过后,回到上一个节点继续遍历。
     * 统计遍历的次数即为岛屿的数量。
     */
    public int numIslands(char[][] grid) {
        /*记录岛屿数量*/
        int count = 0;
        int m = grid.length;
        if (m == 0) {
            return 0;
        }
        int n = grid[0].length;
        /*方向数组,用于四个方向的遍历*/
        int[] dx = {0, 0, 1, -1};
        int[] dy = {1, -1, 0, 0};
        /*记录当前位置是否已经被访问*/
        boolean[][] visited = new boolean[m][n];
        /*用栈来模拟深度优先搜索*/
        Stack<int[]> stack = new Stack<>();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                /*如果当前位置是陆地且没有被访问过*/
                if (grid[i][j] == '1' && !visited[i][j]) {
                    /*岛屿数量+1*/
                    count++;
                    /*将当前位置入栈*/
                    stack.push(new int[]{i, j});
                    /*标记当前位置为已访问*/
                    visited[i][j] = true;
                    /*栈不为空时继续搜索*/
                    while (!stack.empty()) {
                        /*取出栈顶元素作为当前位置*/
                        int[] cur = stack.pop();
                        /*四个方向遍历*/
                        for (int k = 0; k < 4; k++) {
                            int x = cur[0] + dx[k];
                            int y = cur[1] + dy[k];
                            /*如果满足条件,则入栈并标记为已访问*/
                            if (x >= 0 && x < m && y >= 0
                                    && y < n
                                    && !visited[x][y]
                                    && grid[x][y] == '1') {
                                stack.push(new int[]{x, y});
                                visited[x][y] = true;
                            }
                        }
                    }
                }
            }
        }
        return count;
    }

     public static void main(String[] args) {
        char[][] grid = {
                {'1', '1', '0', '0', '0'},
                {'1', '1', '0', '0', '0'},
                {'0', '0', '1', '0', '0'},
                {'0', '0', '0', '1', '1'}
        };
        int numIslands = new NumIslands().numIslands(grid);
        /*输出 3*/
        System.out.println(numIslands); 
    }

}

扩展:其他最优解法展示

思路

该问题可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来解决。下面以DFS为例,介绍最优解法思路:

  • 遍历整个二维网格,当发现一个为 '1' 的点时,从这个点开始进行 DFS。在 DFS 过程中,将访问到的 '1' 置为 '0',防止重复访问。同时,沿着当前点的上下左右四个方向继续深入搜索。
  • 深度优先搜索结束后,岛屿的数量加1。继续遍历网格,如果发现下一个点为 '1',则进行新一轮 DFS。
  • 遍历完整个网格后,就可以得到岛屿的数量。

可以用递归或显式的栈来实现 DFS,具体实现方式因人而异。

时间复杂度为 O(mn),其中 m 和 n 分别为网格的行数和列数。

代码展示

以下是基于DFS实现的Java代码,其中用 visited 数组记录每个点是否已经被访问过,从一个点开始搜索时,将其 visited 数组值设为 true,表示已经访问过。在 DFS 中,每次向四个方向递归搜索时,需要先检查坐标是否合法,并且当前点是否为 '1'。如果坐标合法且当前点为 '1',则将其 visited 数组值设为 true,并继续向四个方向递归搜索。

public int numIslands(char[][] grid) {
            /*边界条件判断*/
            if (grid == null || grid.length == 0) {
                return 0;
            }
            int m = grid.length;
            int n = grid[0].length;
            /*记录每个点是否已经被访问过*/
            boolean[][] visited = new boolean[m][n];
            /*岛屿数量计数器*/
            int count = 0;
            /*遍历整个网格*/
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    /*如果当前点为 '1' 且未被访问过,说明发现一个新的岛屿,需要进行 DFS 搜索*/
                    if (!visited[i][j] && grid[i][j] == '1') {
                        /*岛屿数量加1*/
                        count++;
                        /*从当前点开始进行 DFS 搜索*/
                        dfs(grid, visited, i, j);
                    }
                }
            }
            /*返回岛屿数量*/
            return count;
        }

        private void dfs(char[][] grid, boolean[][] visited, int i, int j) {
            /*如果坐标不合法,或当前点已被访问过,或当前点为 '0',则直接返回*/
            if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || visited[i][j] || grid[i][j] == '0') {
                return;
            }
            /*将当前点标记为已访问*/
            visited[i][j] = true;
            /*分别向上、下、左、右四个方向递归搜索*/
            /*上*/
            dfs(grid, visited, i - 1, j);
            /*下*/
            dfs(grid, visited, i + 1, j);
            /*左*/
            dfs(grid, visited, i, j - 1);
            /*右*/
            dfs(grid, visited, i, j + 1);
        }

11、用数组实现一个栈

题目描述:设计一个栈数据结构,使用数组来实现。需要支持入栈、出栈、获取栈顶元素和判断栈是否为空的操作。

解题思路

基本解题思路如下:

  1. 创建一个数组 stack 用于存储栈中的元素。
  2. 维护一个变量 top 表示栈顶的索引位置,初始值为 -1。
  3. 入栈操作:将要入栈的元素放入 stack 数组中 top+1 的位置,然后将 top 的值加 1。
  4. 出栈操作:从 stack 数组中取出 top 位置的元素,然后将 top 的值减 1。
  5. 获取栈顶元素操作:返回 stack 数组中 top 位置的元素。
  6. 判断栈是否为空操作:当 top 的值为 -1 时,表示栈为空。

具体代码展示

package org.zyf.javabasic.letcode.stack;

/**
 * @author yanfengzhang
 * @description 设计一个栈数据结构,使用数组来实现。
 * 需要支持入栈、出栈、获取栈顶元素和判断栈是否为空的操作。
 *
 * 解题思路:
 * 	1.	创建一个数组 stack 用于存储栈中的元素。
 * 	2.	维护一个变量 top 表示栈顶的索引位置,初始值为 -1。
 * 	3.	入栈操作:将要入栈的元素放入 stack 数组中 top+1 的位置,然后将 top 的值加 1。
 * 	4.	出栈操作:从 stack 数组中取出 top 位置的元素,然后将 top 的值减 1。
 * 	5.	获取栈顶元素操作:返回 stack 数组中 top 位置的元素。
 * 	6.	判断栈是否为空操作:当 top 的值为 -1 时,表示栈为空。
 * @date 2023/6/14  23:28
 */
public class ImplementStackUseArray {
    private int[] stack;
    private int top;

    public ImplementStackUseArray(int capacity) {
        stack = new int[capacity];
        /*将 top 初始化为 -1*/
        top = -1;
    }

    /**
     * push(int element) 方法用于将元素入栈,
     * 它首先检查栈是否已满,
     * 然后将元素放入 top+1 的位置,
     * 最后将 top 的值加 1。
     * @param element
     */
    public void push(int element) {
        if (top == stack.length - 1) {
            throw new IllegalStateException("Stack is full");
        }
        stack[++top] = element;
    }

    /**
     * pop() 方法用于出栈,
     * 它首先检查栈是否为空,
     * 然后返回 top 位置的元素,
     * 最后将 top 的值减 1。
     * @return
     */
    public int pop() {
        if (isEmpty()) {
            throw new IllegalStateException("Stack is empty");
        }
        return stack[top--];
    }

    /**
     * peek() 方法用于获取栈顶元素,
     * 它首先检查栈是否为空,然后返回 top 位置的元素。
     * @return
     */
    public int peek() {
        if (isEmpty()) {
            throw new IllegalStateException("Stack is empty");
        }
        return stack[top];
    }

    /**
     * isEmpty() 方法用于判断栈是否为空,
     * 它通过判断 top 的值是否为 -1 来确定
     * @return
     */
    public boolean isEmpty() {
        return top == -1;
    }

    public static void main(String[] args) {
        ImplementStackUseArray stack = new ImplementStackUseArray(5);

        stack.push(10);
        stack.push(20);
        stack.push(30);

        // 输出 Top element: 30
        System.out.println("Top element: " + stack.peek());

        // 输出 Popped element: 30
        System.out.println("Popped element: " + stack.pop());

        // 输出 Top element after pop: 20
        System.out.println("Top element after pop: " + stack.peek());

        // 输出 Is stack empty? false
        System.out.println("Is stack empty? " + stack.isEmpty());
    }
}

12、基本数学运算表达式求值

题目描述:给定一个包含加法、减法、乘法和除法的算术表达式字符串,设计一个程序来计算其结果。也就是说给个1+2*5-6\2字符串,最后计算出来就行

解题思路

要计算一个包含加减乘除的算术表达式,可以使用栈(Stack)数据结构来辅助计算。以下是一种常见的解题思路:

  1. 创建两个栈,一个用于存储操作数(Operand Stack),一个用于存储运算符(Operator Stack)。
  2. 从左到右依次遍历表达式的每个字符。
  3. 如果当前字符是数字,则将其解析为操作数,并将操作数入栈到 Operand Stack。
  4. 如果当前字符是运算符,则执行以下步骤:a. 如果 Operator Stack 为空,或者栈顶运算符是左括号 ‘(’,则将当前运算符入栈到 Operator Stack。 b. 如果当前运算符优先级大于栈顶运算符优先级,或者栈顶运算符是左括号 ‘(’,则将当前运算符入栈到 Operator Stack。 c. 否则,从 Operator Stack 弹出栈顶运算符,从 Operand Stack 弹出两个操作数,进行相应的计算,并将结果入栈到 Operand Stack。重复该步骤直到满足条件 b,然后将当前运算符入栈到 Operator Stack。注意:在比较运算符优先级时,可以使用一定的约定,例如将加法和减法设定为优先级较低,乘法和除法设定为优先级较高。
  5.  如果当前字符是左括号 ‘(’,则将其入栈到 Operator Stack。
  6. 如果当前字符是右括号 ‘)’,则执行以下步骤: 从 Operator Stack 弹出栈顶运算符,从 Operand Stack 弹出两个操作数,进行相应的计算,并将结果入栈到 Operand Stack。重复该步骤直到遇到左括号 ‘(’,将其从 Operator Stack 弹出并丢弃。
  7. 遍历完表达式后,执行以下步骤:a. 如果 Operator Stack 不为空,则从 Operator Stack 弹出栈顶运算符,从 Operand Stack 弹出两个操作数,进行相应的计算,并将结果入栈到 Operand Stack。重复该步骤直到 Operator Stack 为空。b. 最后 Operand Stack 中剩下的唯一元素就是表达式的最终结果。

具体代码展示

package org.zyf.javabasic.letcode.stack;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 给定一个包含加法、减法、乘法和除法的算术表达式字符串,设计一个程序来计算其结果。
 * @date 2023/6/13  23:29
 */
public class ArithmeticExpressionCalculator {

    /**
     *
     * 要计算一个包含加减乘除的算术表达式,可以使用栈(Stack)数据结构来辅助计算。以下是一种常见的解题思路:
     * 	1.	创建两个栈,一个用于存储操作数(Operand Stack),一个用于存储运算符(Operator Stack)。
     * 	2.	从左到右依次遍历表达式的每个字符。
     * 	3.	如果当前字符是数字,则将其解析为操作数,并将操作数入栈到 Operand Stack。
     * 	4.	如果当前字符是运算符,则执行以下步骤:
     * 	•	a. 如果 Operator Stack 为空,或者栈顶运算符是左括号 ‘(’,则将当前运算符入栈到 Operator Stack。
     * 	•	b. 如果当前运算符优先级大于栈顶运算符优先级,或者栈顶运算符是左括号 ‘(’,则将当前运算符入栈到 Operator Stack。
     * 	•	c. 否则,从 Operator Stack 弹出栈顶运算符,从 Operand Stack 弹出两个操作数,进行相应的计算,并将结果入栈到 Operand Stack。重复该步骤直到满足条件 b,然后将当前运算符入栈到 Operator Stack。
     * 	•	注意:在比较运算符优先级时,可以使用一定的约定,例如将加法和减法设定为优先级较低,乘法和除法设定为优先级较高。
     * 	5.	如果当前字符是左括号 ‘(’,则将其入栈到 Operator Stack。
     * 	6.	如果当前字符是右括号 ‘)’,则执行以下步骤:
     * 	•	a. 从 Operator Stack 弹出栈顶运算符,从 Operand Stack 弹出两个操作数,进行相应的计算,并将结果入栈到 Operand Stack。重复该步骤直到遇到左括号 ‘(’,将其从 Operator Stack 弹出并丢弃。
     * 	7.	遍历完表达式后,执行以下步骤:
     * 	•	a. 如果 Operator Stack 不为空,则从 Operator Stack 弹出栈顶运算符,从 Operand Stack 弹出两个操作数,进行相应的计算,并将结果入栈到 Operand Stack。重复该步骤直到 Operator Stack 为空。
     * 	•	b. 最后 Operand Stack 中剩下的唯一元素就是表达式的最终结果。
     * @param expression
     * @return
     */
    public static int calculate(String expression) {
        Stack<Integer> operandStack = new Stack<>();
        Stack<Character> operatorStack = new Stack<>();

        for (int i = 0; i < expression.length(); i++) {
            char ch = expression.charAt(i);
            if (Character.isDigit(ch)) {
                // 当前字符是数字,将其解析为操作数并入栈
                // 将字符转换为整数
                int operand = ch - '0';
                while (i + 1 < expression.length()
                        && Character.isDigit(expression.charAt(i + 1))) {
                    // 继续读取多位数字
                    operand = operand * 10 + (expression.charAt(i + 1) - '0');
                    i++;
                }
                operandStack.push(operand);
            } else if (ch == '(') {
                // 当前字符是左括号,入栈到运算符栈
                operatorStack.push(ch);
            } else if (ch == ')') {
                // 当前字符是右括号,执行计算直到遇到左括号
                while (!operatorStack.isEmpty() && operatorStack.peek() != '(') {
                    calculateTopOperation(operandStack, operatorStack);
                }
                // 弹出左括号 '('
                if (!operatorStack.isEmpty() && operatorStack.peek() == '(') {
                    operatorStack.pop();
                }
            } else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
                // 当前字符是运算符
                while (!operatorStack.isEmpty()
                        && shouldEvaluateTopOperation(ch, operatorStack.peek())) {
                    calculateTopOperation(operandStack, operatorStack);
                }
                operatorStack.push(ch);
            }
        }

        // 执行剩余的计算
        while (!operatorStack.isEmpty()) {
            calculateTopOperation(operandStack, operatorStack);
        }

        // 最后 Operand Stack 中剩下的唯一元素就是表达式的最终结果
        return operandStack.pop();
    }

    private static boolean shouldEvaluateTopOperation(char currentOperator, char topOperator) {
        if (topOperator == '(' || topOperator == ')') {
            return false;
        }
        if ((currentOperator == '*' || currentOperator == '/')
                && (topOperator == '+' || topOperator == '-')) {
            return false;
        }
        return true;
    }

    private static void calculateTopOperation(Stack<Integer> operandStack, Stack<Character> operatorStack) {
        char operator = operatorStack.pop();
        int operand2 = operandStack.pop();
        int operand1 = operandStack.pop();
        int result = 0;
        switch (operator) {
            case '+':
                result = operand1 + operand2;
                break;
            case '-':
                result = operand1 - operand2;
                break;
            case '*':
                result = operand1 * operand2;
                break;
            case '/':
                result = operand1 / operand2;
                break;
        }
        operandStack.push(result);
    }

    public static void main(String[] args) {
        String expression1 = "1+2*5-6/2";
        int result1 = calculate(expression1);
        System.out.println(expression1 + "=" + result1);

        String expression2 = "1 + (2 - 3) + 4";
        int result2 = calculate(expression2);
        System.out.println(expression2 + "=" + result2);
    }
}

13、 IP 范围判断

题目描述:给定一个二叉树,每个节点包含一个 IP 段的范围,其中 ip1 和 ip2 分别表示左子树和右子树的 IP 范围。现在给定一个 IP 地址,判断该 IP 是否在任意节点的 IP 范围内。

解题思路

为了判断一个 IP 地址是否在二叉树的某个节点的 IP 范围内,我们可以采用非递归的方式进行树的遍历,并在遍历过程中进行比较。以下是一种可能的解法:

  1. 定义一个栈 stack,用于存储待遍历的节点。
  2. 将根节点入栈。
  3. 循环遍历栈,直到栈为空:• 弹出栈顶节点 node。• 判断当前 IP 地址是否在 node 的 IP 范围内。如果在范围内,则返回 true。• 如果 node 的左子节点不为空且当前 IP 小于等于左子节点的 IP 范围的最大值,将左子节点入栈。• 如果 node 的右子节点不为空且当前 IP 大于等于右子节点的 IP 范围的最小值,将右子节点入栈。
  4. 如果遍历完整个树,仍未找到匹配的 IP 范围,则返回 false。

通过以上方法,我们可以判断给定的 IP 地址是否在二叉树的任意节点的 IP 范围内。该算法的时间复杂度取决于树的深度,最坏情况下为 O(n),其中 n 是树中节点的个数。

具体代码展示

先初始化定义IP树节点如下:

package org.zyf.javabasic.letcode.stack.base;

/**
 * @author yanfengzhang
 * @description
 * @date 2023/6/11  23:54
 */
public class IPTreeNode {
    public String ip1;
    public String ip2;
    public IPTreeNode left;
    public IPTreeNode right;

    public IPTreeNode(String ip1, String ip2) {
        this.ip1 = ip1;
        this.ip2 = ip2;
    }
}

具体代码分析展示如下:

package org.zyf.javabasic.letcode.stack;

import org.zyf.javabasic.letcode.stack.base.IPTreeNode;

import java.util.Stack;

/**
 * @author yanfengzhang
 * @description 给定一个二叉树,每个节点包含一个 IP 段的范围,其中 ip1 和 ip2 分别表示左子树和右子树的 IP 范围。
 * 现在给定一个 IP 地址,判断该 IP 是否在任意节点的 IP 范围内。
 * @date 2023/6/11  00:09
 */
public class IPAddressRange {
    public boolean isIPInRange(IPTreeNode root, String ip) {
        if (root == null || ip == null) {
            return false;
        }

        Stack<IPTreeNode> stack = new Stack<>();
        stack.push(root);

        while (!stack.isEmpty()) {
            IPTreeNode node = stack.pop();

            if (isIPInRange(ip, node.ip1, node.ip2)) {
                return true;
            }

            if (node.left != null) {
                stack.push(node.left);
            }

            if (node.right != null) {
                stack.push(node.right);
            }
        }

        return false;
    }

    private boolean isIPInRange(String ip, String startIP, String endIP) {
        long ipValue = ipToLong(ip);
        long startValue = ipToLong(startIP);
        long endValue = ipToLong(endIP);

        return ipValue >= startValue && ipValue <= endValue;
    }

    private long ipToLong(String ip) {
        String[] parts = ip.split("\\.");
        long result = 0;

        for (int i = 0; i < 4; i++) {
            result = result * 256 + Long.parseLong(parts[i]);
        }

        return result;
    }

    public static void main(String[] args) {
        IPTreeNode root = new IPTreeNode("192.168.0.1", "192.168.0.100");
        root.left = new IPTreeNode("10.0.0.1", "10.0.0.10");
        root.right = new IPTreeNode("172.16.0.1", "172.16.0.50");

        IPAddressRange solution = new IPAddressRange();
        String ip = "10.0.0.5";
        boolean isInRange = solution.isIPInRange(root, ip);
        // 输出 10.0.0.5 is in range: true
        System.out.println(ip + " is in range: " + isInRange);
    }
}

评论 55
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

张彦峰ZYF

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

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

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

打赏作者

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

抵扣说明:

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

余额充值