【算法社区】从零开始的DS生活 轻松和面试官扯一个小时栈

简介: 详细介绍了栈的概念和性质,简要的介绍了栈ADT并附两种实现方式(链式、顺序),列举LeetCode第20题与严蔚敏老师栈和递归的讲解加深对栈的应用,供读者理解与学习,适合点赞+收藏。有什么错误希望大家直接指出~

 引言:Re:从零开始的DS生活 轻松和面试官扯一个小时栈,详细介绍了栈的概念和性质,简要的介绍了栈ADT并附两种实现方式(链式、顺序),列举LeetCode第20题与严蔚敏老师栈和递归的讲解加深对栈的应用,供读者理解与学习,适合点赞+收藏。有什么错误希望大家直接指出~

友情链接:Re:从零开始的DS生活 轻松从0基础写出链表LRU算法Re:从零开始的DS生活 轻松从0基础实现多种队列Re:从零开始的DS生活 轻松从0基础写出Huffman树与红黑树

导读(有基础的同学可以通过以下直接跳转到感兴趣的地方)

栈的基本概念和性质,栈ADT及其顺序,链接实现;

        栈ADT

        栈的顺序,链接实现

栈的应用

        括号匹配的检验-有效括号问题

        栈的经典应用-逆波兰表达式法

        栈与递归

        JVM内存栈


栈的基本概念和性质,栈ADT及其顺序,链接实现;

栈(Stack)又名后进先出表LIFO(Last In First Out),,它是一种运算受限的线性表。 其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底(这句话是性质)。和弹夹的进出顺序是一样的。

image.gif编辑

进栈:入栈或压栈,将新元素放到栈顶元素的上面,使之成为新的栈顶元素。

出栈:退栈,将栈顶元素删除掉,使得与其相邻的元素成为新的栈顶元素。

image.gif编辑

栈ADT

ADT Stack{
    数据对象: D={aijlai属于ElemSet, i=1,2...n, n>=0}
    数据关系: R1={<ai-1,ai>/ai-1, ai属于D, i=2,... n}
    约定an端为栈顶, al端为栈底。
    基本操作:
    InitStack(&S)    // 操作结果:构造一个空栈 S.
    DestroyStack(&S)    // 初始条件:栈S已存在    操作结果:栈S被销毁
    ClearStack(&S)    // 初始条件:栈S已存在    操作结果:将S清为空栈
    StackEmpty(S)    // 初始条件 :栈S已存在    操作结果:若栈S为空栈,则返回TRUE,否则
    FALSE
    Stacklength(S)    // 初始条件:栈S已存在    操作结果:返回S的元素个数,即栈的长度
    GetTop(S, &e)    // 初始条件:栈S已存在且非空    操作结果:用e返回S的栈顶元素
    Push(&S, e)    // 初始条件:栈S已存在    操作结果:插入元素e为新的栈顶元素
    Pop(&S, &e)    // 初始条件:栈S已存在且非空操作结果:删除S的栈顶元素,并用e返回其值
    StackTraverse(S, visit()    //初始条件: 栈S已存在并非空    操作结果:从栈底到栈顶依次对S的每个数据元素调用函数visit(),一旦visit()失败,则返回操作失败。
}ADT Stack    出自《数据结构(C语言版)》严蔚敏、吴伟民著)

image.gif

栈的顺序,链接实现

栈的数组(顺序)实现

image.gif编辑

/**
 * 栈的数组实现--java
 * 数组的长度是固定的,当栈空间不足时,将原数组数据复制到一个更长的数组中
 * 故入栈的时间复杂度为O(N),出栈的时间复杂度依然为O(1)
 *
 * @author macfmc
 * @date 2020/6/12-20:08
 */
public class MyArrayStack {
    /**
     * 容器
     */
    private Object[] stack;
    /**
     * 栈的默认大小
     */
    private static final int INIT_SIZE = 10;
    /**
     * 栈顶索引
     */
    private int index;
    /**
     * 初始化栈_默认构造方法
     */
    public MyArrayStack() {
        this.stack = new Object[INIT_SIZE];
        this.index = -1;
    }
    /**
     * 初始化栈,自定义长度
     */
    public MyArrayStack(int init_size) {
        if (init_size < 0) {
            throw new RuntimeException();
        }
        this.stack = new Object[init_size];
        this.index = -1;
    }
    /**
     * 判断栈是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return index == -1;
    }
    /**
     * 判断是都栈满
     *
     * @return
     */
    public boolean isFull() {
        return index >= stack.length - 1;
    }
    /**
     * 入栈
     *
     * @param obj
     */
    public synchronized void push(Object obj) {
        // 动态扩容
        if (isFull()) {
            Object[] temp = stack;
            // 创建一个二倍大小的数组
            stack = new Object[stack.length * 2];
            //
            System.arraycopy(temp, 0, stack, 0, temp.length);
        }
        stack[++index] = obj;
    }
    /**
     * 查看栈顶元素
     *
     * @return
     */
    public Object peek() {
        if (!isEmpty()) {
            return stack[index];
        }
        return null;
    }
    /**
     * 出栈
     *
     * @return
     */
    public synchronized Object pop() {
        if (!isEmpty()) {
            Object obj = peek();
            stack[index--] = null;
            return obj;
        }
        return null;
    }
    public static void main(String[] args) {
        MyArrayStack stack = new MyArrayStack();
        for (int i = 0; i < 100; i++) {
            stack.push("stack" + i);
        }
        for (int i = 0; i < 100; i++) {
            System.out.println(stack.pop());
        }
    }
}

image.gif

栈的链表(链接)实现

image.gif编辑

/**
 * 栈的链表实现--java
 *
 * @author macfmc
 * @date 2020/6/12-20:08
 */
public class MyLinkedStack<T> {
    /**
     * 单链表
     *
     * @param <T>
     */
    private class Node<T> {
        //指向下一个节点
        Node next;
        //本节点存储的元素
        T date;
    }
    /**
     * 栈中存储的元素的数量
     */
    private int count;
    /**
     * 栈顶元素
     */
    private Node top;
    public MyLinkedStack() {
        count = 0;
        top = null;
    }
    /**
     * 判断是都为空
     *
     * @return true为空
     */
    public boolean isEmpty() {
        return count == 0;
    }
    /**
     * 入栈
     *
     * @param element
     */
    public void push(T element) {
        Node<T> node = new Node<T>();//链表节点
        node.date = element;//链表节点数据
        node.next = top;//链表下一步节点
        top = node;//现在的栈顶
        count++;//栈数量
    }
    /**
     * 出栈
     *
     * @return
     */
    public T pop() {
        if (isEmpty()) {
            throw new RuntimeException();
        }
        T result = (T) top.date;
        top = top.next;
        count--;
        return result;
    }
    /**
     * 获取栈顶
     *
     * @return
     */
    public T peek() {
        if (isEmpty()) {
            throw new RuntimeException();
        }
        return (T) top.date;
    }
    public static void main(String[] args) {
        MyLinkedStack<String> lind = new MyLinkedStack<String>();
        for (int i = 0; i < 10; i++) {
            lind.push("LindedStack" + i);
        }
        for (int i = 0; i < 10; i++) {
            System.out.println(lind.pop());
        }
    }
}

image.gif

栈的应用

括号匹配的检验-有效括号问题

// https://leetcode-cn.com/problems/valid-parentheses/)
class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        char[] chars = s.toCharArray();
        // 遍历括号
        for (char aChar : chars) {
            if (stack.size() == 0) { // 初始化栈,没有的话会报空栈异常EmptyStackException
                stack.push(aChar);
            } else if (isSym(stack.peek(), aChar)) { // 判断栈顶和入栈元素是不是一对括号
                stack.pop();
            } else {
                stack.push(aChar);
            }
        }
        return stack.size() == 0;
    }
    private boolean isSym(char c1, char c2) {
        return (c1 == '(' && c2 == ')') || (c1 == '[' && c2 == ']') || (c1 == '{' && c2 == '}');
    }
    public boolean isValid2(String s) {
        Stack stack = new Stack();
        char[] chars = s.toCharArray();
        for (char c : chars)
            if (stack.size() == 0)
                stack.push(c);
            else if (isSym((char) stack.peek(), c))
                stack.pop();
            else
                stack.push(c);
        return stack.size() == 0;
    }
    private boolean isSym(char c1, char c2) {
        return (c1 == '(' && c2 == ')') || (c1 == '[' && c2 == ']') || (c1 == '{' && c2 == '}');
    }
}

image.gif

栈的经典应用-逆波兰表达式法

中缀表达式转为后缀表达式:

设置一个堆栈,初始时将堆栈顶设置为#

顺序读入中缀表达式,到读到的单词为数字时将其输出,接着读下一个单词;

令x1 为栈顶运算符变量,x2 为扫描到的运算符变量,当顺序从表达试中读到的运算符时赋值给x2,然后比较x1 和 x2 的优先级,若x1 的优先级高于x2的优先级,将x1退栈并输出,接着比较新的栈顶运算符x1,x2的优先级;若 x1的优先级低于x2的优先级,将x2 入栈;如果x1 = “(”且 x2 = “)”,将x1 退栈;若x1的优先级等于x2的优先级且x1 = “#”而x2=“#”时,算法结束

import java.util.ArrayList;
import java.util.Stack;
/**
 * @author macfmc
 * @date 2020/6/13-22:30
 */
public class ReversePolishNotation {
    /**
     * 测试的main方法
     */
    public static void main(String arg[]) {
        String s = "9+(3-1)*3+10/2";
        ArrayList postfix = transform(s);
        for (int i = 0, len = postfix.size(); i < len; i++) {
            System.out.println(postfix.get(i));
        }
        calculate(postfix);
    }
    /**
     * 将中缀表达式转换成后缀表达式
     */
    public static ArrayList transform(String prefix) {
        System.out.println("transform");
        int i, len = prefix.length();// 用字符串保存前缀表达式
        prefix = prefix + '#';// 让前缀表达式以'#'结尾
        Stack<Character> stack = new Stack<Character>();// 保存操作符的栈
        stack.push('#');// 首先让'#'入栈
        ArrayList postfix = new ArrayList();//后缀数组集合
        // 保存后缀表达式的列表,可能是数字,也可能是操作符
        for (i = 0; i < len + 1; i++) {
            System.out.println(i + " " + prefix.charAt(i));
            if (Character.isDigit(prefix.charAt(i))) {// 当前字符是一个数字
                if (Character.isDigit(prefix.charAt(i + 1))) {// 当前字符的下一个字符也是数字(两位数)
                    postfix.add(10 * (prefix.charAt(i) - '0') + (prefix.charAt(i + 1) - '0'));
                    i++;// 序号加1
                } else {// 当前字符的下一个字符不是数字(一位数)
                    postfix.add((prefix.charAt(i) - '0'));
                }
            } else {// 当前字符是一个操作符
                switch (prefix.charAt(i)) {
                    case '(':// 如果是开括号
                        stack.push(prefix.charAt(i));// 开括号只放入到栈中,不放入到后缀表达式中
                        break;
                    case ')':// 如果是闭括号
                        while (stack.peek() != '(') {
                            postfix.add(stack.pop());// 闭括号不入栈,将前一个不是“)”的操作符入栈
                        }
                        stack.pop();// '('出栈
                        break;
                    default:// 默认情况下:+ - * /
                        while (stack.peek() != '#' && compare(stack.peek(), prefix.charAt(i))) {// 比较运算符之间的优先级
                            postfix.add(stack.pop());// 不断弹栈,直到当前的操作符的优先级高于栈顶操作符
                        }
                        if (prefix.charAt(i) != '#') {// 如果当前的操作符不是'#'(结束符),那么入操作符栈
                            stack.push(prefix.charAt(i));// 最后的标识符'#'是不入栈的
                        }
                        break;
                }
            }
        }
        return postfix;
    }
    /**
     * 比较运算符之间的优先级
     * 如果是peek优先级高于cur,返回true,默认都是peek优先级要低
     */
    public static boolean compare(char peek, char cur) {
        if (peek == '*'
                && (cur == '+' || cur == '-' || cur == '/' || cur == '*')) {// 如果cur是'(',那么cur的优先级高,如果是')',是在上面处理
            return true;
        } else if (peek == '/'
                && (cur == '+' || cur == '-' || cur == '*' || cur == '/')) {
            return true;
        } else if (peek == '+' && (cur == '+' || cur == '-')) {
            return true;
        } else if (peek == '-' && (cur == '+' || cur == '-')) {
            return true;
        } else if (cur == '#') {// 这个很特别,这里说明到了中缀表达式的结尾,那么就要弹出操作符栈中的所有操作符到后缀表达式中
            return true;// 当cur为'#'时,cur的优先级算是最低的
        }
        return false;// 开括号是不用考虑的,它的优先级一定是最小的,cur一定是入栈
    }
    /**
     * 计算后缀表达式
     */
    public static double calculate(ArrayList postfix) {// 后缀表达式的运算顺序就是操作符出现的先后顺序
        System.out.println("calculate");
        int i, res = 0, size = postfix.size();
        Stack<Integer> stackNum = new Stack<Integer>();
        for (i = 0; i < size; i++) {
            if (postfix.get(i).getClass() == Integer.class) {// 判断如果是操作数
                stackNum.push((Integer) postfix.get(i));//入栈
                System.out.println("push" + " " + (Integer) postfix.get(i));
            } else {// 如果是操作符
                System.out.println((Character) postfix.get(i));
                int a = stackNum.pop();// 出栈后一个操作数
                int b = stackNum.pop();// 出栈前一个操作数
                switch ((Character) postfix.get(i)) {
                    case '+':
                        res = b + a;
                        System.out.println("+ " + a + " " + b);
                        break;
                    case '-':
                        res = b - a;
                        System.out.println("- " + a + " " + b);
                        break;
                    case '*':
                        res = b * a;
                        System.out.println("* " + a + " " + b);
                        break;
                    case '/':
                        res = b / a;
                        System.out.println("/ " + a + " " + b);
                        break;
                }
                stackNum.push(res);//操作后的结果入栈
                System.out.println("push" + " " + res);
            }
        }
        res = stackNum.pop();//结果
        System.out.println("结果: " + " " + res);
        return res;
    }
}

image.gif

栈与递归

栈:限定仅在表尾进行插入和删除操作的线性表。

递归:直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。

函数的递归调用和普通函数调用是一样的。当程序执行到某个函数时,将这个函数进行入栈操作,在入栈之前,通常需要完成三件事。

1、将所有的实参、返回地址等信息传递给被调函数保存。

2、为被调函数的局部变量分配存储区。

3、将控制转移到北调函数入口。

当一个函数完成之后会进行出栈操作,出栈之前同样要完成三件事。

1、保存被调函数的计算结果。

2、释放被调函数的数据区。

3、依照被调函数保存的返回地址将控制转移到调用函数。

上述操作必须通过栈来实现,即将整个程序的运行空间安排在一个栈中。每当运行一个函数时,就在栈顶分配空间,函数退出后,释放这块空间。所以当前运行的函数一定在栈顶。

(注:摘自严蔚敏等人的数据结构c语言版)

JVM内存栈

JVM:在java编译器和os平台之间的虚拟处理器,JVM会递归调用为例,一个栈帧包括局部变量表、操作数栈、栈数据区

image.gif编辑

image.gif编辑


相关文章
|
29天前
|
负载均衡 NoSQL 算法
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
这篇文章是关于Java面试中Redis相关问题的笔记,包括Redis事务实现、集群方案、主从复制原理、CAP和BASE理论以及负载均衡算法和类型。
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
|
1月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
30天前
|
存储 设计模式 Java
Unity精华☀️ 面试“堆、栈”误区!这样做可能反而会降低吸引力
Unity精华☀️ 面试“堆、栈”误区!这样做可能反而会降低吸引力
|
1月前
|
机器学习/深度学习 算法 数据中心
【机器学习】面试问答:PCA算法介绍?PCA算法过程?PCA为什么要中心化处理?PCA为什么要做正交变化?PCA与线性判别分析LDA降维的区别?
本文介绍了主成分分析(PCA)算法,包括PCA的基本概念、算法过程、中心化处理的必要性、正交变换的目的,以及PCA与线性判别分析(LDA)在降维上的区别。
41 4
|
1月前
|
机器学习/深度学习 算法
【机器学习】解释对偶的概念及SVM中的对偶算法?(面试回答)
解释了对偶的概念,指出对偶性在优化问题中的重要性,尤其是在强对偶性成立时可以提供主问题的最优下界,并且详细阐述了支持向量机(SVM)中对偶算法的应用,包括如何将原始的最大间隔优化问题转换为对偶问题来求解。
44 2
|
1月前
|
机器学习/深度学习 算法 数据挖掘
|
23天前
|
消息中间件 存储 算法
这些年背过的面试题——实战算法篇
本文是技术人面试系列实战算法篇,面试中关于实战算法都需要了解哪些内容?一文带你详细了解,欢迎收藏!
|
29天前
|
算法
【算法】栈算法——逆波兰表达式求值
【算法】栈算法——逆波兰表达式求值
|
29天前
|
存储 算法
【算法】栈算法——最小栈
【算法】栈算法——最小栈
|
29天前
|
算法
【算法】栈算法——栈的压入、弹出序列
【算法】栈算法——栈的压入、弹出序列