java-数据结构与算法-02-数据结构-05-栈

1. 栈

1. 概述

计算机科学中,stack 是一种线性的数据结构,只能在其一端添加数据和移除数据。习惯来说,这一端称之为栈顶,另一端不能操作数据的称之为栈底,就如同生活中的一摞书

栈是一种特殊的线性表,只能在一端进行操作

  • 往栈中添加元素的操作,一般叫做 push,入栈
  • 从栈中移除元素的操作,一般叫做 pop,出栈(只能移除栈顶元素,也叫做:弹出栈顶元素)
  • 后进先出的原则,Last In First Out,LIFO
    在这里插入图片描述

先提供一个栈接口

public interface Stack<E> {
    /**
     * 向栈顶压入元素
     * @param value 待压入值
     * @return 压入成功返回 true, 否则返回 false
     */
    boolean push(E value);

    /**
     * 从栈顶弹出元素
     * @return 栈非空返回栈顶元素, 栈为空返回 null
     */
    E pop();

    /**
     * 返回栈顶元素, 不弹出
     * @return 栈非空返回栈顶元素, 栈为空返回 null
     */
    E peek();

    /**
     * 判断栈是否为空
     * @return 空返回 true, 否则返回 false
     */
    boolean isEmpty();

    /**
     * 判断栈是否已满
     * @return 满返回 true, 否则返回 false
     */
    boolean isFull();
}

栈的应用
浏览器的前进和后退

在这里插入图片描述

2. 链表实现

package com.itheima.datastructure.stack;

import java.util.Iterator;
import java.util.StringJoiner;

/**
 * 链表实现的栈。
 * 该类实现了Stack接口和Iterable接口,允许对栈中的元素进行迭代。
 *
 * @param <E> 栈中元素的类型。
 */
public class LinkedListStack<E> implements Stack<E>, Iterable<E> {

    /**
     * 栈的容量,默认为Integer.MAX_VALUE,表示不限制容量。
     */
    private int capacity = Integer.MAX_VALUE;
    
    /**
     * 栈中元素的数量。
     */
    private int size;
    
    /**
     * 链表的头节点,用于简化插入和删除操作。
     */
    private final Node<E> head = new Node<>(null, null);

    /**
     * 默认构造函数。
     */
    public LinkedListStack() {
    }

    /**
     * 带容量限制的构造函数。
     *
     * @param capacity 栈的容量限制。
     */
    public LinkedListStack(int capacity) {
        this.capacity = capacity;
    }

    /**
     * 将元素压入栈顶。
     *
     * @param value 要压入栈的元素。
     * @return 如果栈未满,则返回true;否则返回false。
     */
    /*
        head -> 2 -> 1 -> null
     */
    @Override
    public boolean push(E value) {
        if (isFull()) {
            return false;
        }
        head.next = new Node<>(value, head.next);
        size++;
        return true;
    }

    /**
     * 从栈顶弹出一个元素。
     *
     * @return 如果栈不为空,则返回栈顶元素;否则返回null。
     */
    /*
        head -> 2 -> 1 -> null
     */
    @Override
    public E pop() {
        if (isEmpty()) {
            return null;
        }
        Node<E> first = head.next;
        head.next = first.next;
        size--;
        return first.value;
    }

    /**
     * 查看栈顶元素。
     *
     * @return 如果栈不为空,则返回栈顶元素;否则返回null。
     */
    @Override
    public E peek() {
        if (isEmpty()) {
            return null;
        }
        return head.next.value;
    }

    /**
     * 检查栈是否为空。
     *
     * @return 如果栈为空,则返回true;否则返回false。
     */
    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 检查栈是否已满。
     *
     * @return 如果栈已满,则返回true;否则返回false。
     */
    @Override
    public boolean isFull() {
        return size == capacity;
    }

    /**
     * 创建一个迭代器,用于遍历栈中的元素。
     *
     * @return栈的元素迭代器。
     */
    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            Node<E> p = head.next;

            @Override
            public boolean hasNext() {
                return p != null;
            }

            @Override
            public E next() {
                E value = p.value;
                p = p.next;
                return value;
            }
        };
    }

    /**
     * 链表节点类,用于存储栈中的元素。
     *
     * @param <E> 节点中存储的元素类型。
     */
    static class Node<E> {
        E value;
        Node<E> next;

        public Node(E value, Node<E> next) {
            this.value = value;
            this.next = next;
        }
    }

    /**
     * 将栈中的元素转换为字符串表示。
     *
     * @return 栈的字符串表示,元素之间用逗号分隔。
     */
    @Override
    public String toString() {
        StringJoiner sj = new StringJoiner(",");
        for (E e : this) {
            sj.add(e.toString());
        }
        return sj.toString();
    }
}


3. 数组实现

/**
 * 数组实现的栈类,支持泛型元素。
 * @param <E> 栈中元素的类型。
 */
package com.itheima.datastructure.stack;

import java.util.Iterator;

public class ArrayStack<E> implements Stack<E>, Iterable<E> {

    /**
     * 存储栈元素的数组。
     */
    private final E[] array;
    
    /**
     * 栈顶指针,指示当前栈的顶部元素的位置。
     */
    private int top; // 栈顶指针

    /**
     * 构造一个指定容量的栈。
     * @param capacity 栈的初始容量。
     */
    @SuppressWarnings("all")
    public ArrayStack(int capacity) {
        this.array = (E[]) new Object[capacity];
    }

    /**
     * 将元素压入栈顶。
     * @param value 要压入栈的元素。
     * @return 如果栈未满,则返回true;否则返回false。
     */
    @Override
    public boolean push(E value) {
        if (isFull()) {
            return false;
        }
        array[top++] = value;
        return true;
    }

    /**
     * 弹出栈顶元素。
     * @return 栈顶元素,如果栈为空,则返回null。
     */
    @Override
    public E pop() {
        if (isEmpty()) {
            return null;
        }
        E e = array[--top];
        array[top] = null; // help GC
        return e;
    }

    /**
     * 查看栈顶元素。
     * @return 栈顶元素,如果栈为空,则返回null。
     */
    @Override
    public E peek() {
        if (isEmpty()) {
            return null;
        }
        return array[top - 1];
    }

    /**
     * 检查栈是否为空。
     * @return 如果栈为空,则返回true;否则返回false。
     */
    @Override
    public boolean isEmpty() {
        return top == 0;
    }

    /**
     * 检查栈是否已满。
     * @return 如果栈已满,则返回true;否则返回false。
     */
    @Override
    public boolean isFull() {
        return top == array.length;
    }

    /**
     * 返回栈元素的迭代器,用于遍历栈。
     * @return栈元素的迭代器。
     */
    /*
        底          顶
        0   1   2   3
        a   b   c   d
                        p
     */
    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            int p = top;

            @Override
            public boolean hasNext() {
                return p > 0;
            }

            @Override
            public E next() {
                return array[--p];
            }
        };
    }
}

4. 应用

模拟如下方法调用

public static void main(String[] args) {
    System.out.println("main1");
    System.out.println("main2");
    method1();
    method2();
    System.out.println("main3");
}

public static void method1() {
    System.out.println("method1");
    method3();
}

public static void method2() {
    System.out.println("method2");
}

public static void method3() {
    System.out.println("method3");
}

模拟代码

package com.itheima.datastructure.stack;

/**
 * CPU模拟类,使用栈来模拟方法的调用与返回。
 */
public class CPU {
    /**
     * 方法帧类,用于存储方法的退出点。
     */
    static class Frame {
        int exit;

        /**
         * 构造方法,初始化方法帧的退出点。
         * 
         * @param exit 方法的退出点值。
         */
        public Frame(int exit) {
            this.exit = exit;
        }
    }

    /**
     * 程序计数器,用于指示当前执行的指令位置。
     */
    static int pc = 1;
    /**
     * 方法调用栈,用于模拟方法的调用与返回过程。
     */
    static ArrayStack<Frame> stack = new ArrayStack<>(100);

    /**
     * 程序入口点。
     * 
     * @param args 命令行参数。
     */
    public static void main(String[] args) {
        // 初始化方法调用栈,压入一个表示main方法开始的帧
        stack.push(new Frame(-1));
        // 当栈不为空时,循环执行指令
        while (!stack.isEmpty()) {
            // 根据程序计数器的值执行相应的操作
            switch (pc) {
                case 1:
                    // 执行main方法的第一段代码
                    System.out.println("main1");
                    pc++;
                    break;
                case 2:
                    // 执行main方法的第二段代码
                    System.out.println("main2");
                    pc++;
                    break;
                case 3:
                    // 调用method1方法
                    stack.push(new Frame(pc + 1));
                    pc = 100;
                    break;
                case 4:
                    // 调用method2方法
                    stack.push(new Frame(pc + 1));
                    pc = 200;
                    break;
                case 5:
                    // 方法返回,从栈中弹出方法帧,并跳转到退出点
                    System.out.println("main3");
                    pc = stack.pop().exit;
                    break;
                case 100:
                    // method1方法的代码段
                    System.out.println("method1");
                    stack.push(new Frame(pc + 1));
                    pc = 300;
                    break;
                case 101:
                    // method1方法返回
                    pc = stack.pop().exit;
                    break;
                case 200:
                    // method2方法的代码段
                    System.out.println("method2");
                    pc = stack.pop().exit;
                    break;
                case 300:
                    // method3方法的代码段
                    System.out.println("method3");
                    pc = stack.pop().exit;
                    break;
            }
        }
    }
}

2. 习题

E01. 有效的括号-Leetcode 20

一个字符串中可能出现 [] () 和 {} 三种括号,判断该括号是否有效
有效的例子

()[]{}

([{}])

()

无效的例子

[)

([)]

([]

思路

  • 遇到左括号, 把要配对的右括号放入栈顶
  • 遇到右括号, 若此时栈为空, 返回 false,否则把它与栈顶元素对比
    • 若相等, 栈顶元素弹出, 继续对比下一组
    • 若不等, 无效括号直接返回 false
  • 循环结束
    • 若栈为空, 表示所有括号都配上对, 返回 true
    • 若栈不为空, 表示右没配对的括号, 应返回 false

答案(用到了课堂案例中的 ArrayStack 类)

public boolean isValid(String s) {
    ArrayStack<Character> stack = new ArrayStack<>(s.length() / 2 + 1);
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        if (c == '(') {
            stack.push(')');
        } else if (c == '[') {
            stack.push(']');
        } else if (c == '{') {
            stack.push('}');
        } else {
            if (!stack.isEmpty() && stack.peek() == c) {
                stack.pop();
            } else {
                return false;
            }
        }
    }
    return stack.isEmpty();
}

E02. 后缀表达式求值-Leetcode 120

后缀表达式也称为逆波兰表达式,即运算符写在后面

  • 从左向右进行计算
  • 不必考虑运算符优先级,即不用包含括号

示例

输入:tokens = ["2","1","+","3","*"]
输出:9
即:(2 + 1) * 3

输入:tokens = ["4","13","5","/","+"]
输出:6
即:4 + (13 / 5)

题目假设

  • 数字都视为整数
  • 数字和运算符个数给定正确,不会有除零发生

代码

public int evalRPN(String[] tokens) {
    LinkedList<Integer> numbers = new LinkedList<>();
    for (String t : tokens) {
        switch (t) {
            case "+" -> {
                Integer b = numbers.pop();
                Integer a = numbers.pop();
                numbers.push(a + b);
            }
            case "-" -> {
                Integer b = numbers.pop();
                Integer a = numbers.pop();
                numbers.push(a - b);
            }
            case "*" -> {
                Integer b = numbers.pop();
                Integer a = numbers.pop();
                numbers.push(a * b);
            }
            case "/" -> {
                Integer b = numbers.pop();
                Integer a = numbers.pop();
                numbers.push(a / b);
            }
            default -> numbers.push(Integer.parseInt(t));
        }
    }
    return numbers.pop();
}

E03. 中缀表达式转后缀

public class E03InfixToSuffix {
    /*
        思路
        1. 遇到数字, 拼串
        2. 遇到 + - * /
            - 优先级高于栈顶运算符 入栈
            - 否则将栈中高级或平级运算符出栈拼串, 本运算符入栈
        3. 遍历完成, 栈中剩余运算符出栈拼串
            - 先出栈,意味着优先运算
        4. 带 ()
            - 左括号直接入栈
            - 右括号要将栈中直至左括号为止的运算符出栈拼串

        |   |
        |   |
        |   |
        _____

        a+b
        a+b-c
        a+b*c
        a*b+c
        (a+b)*c

     */
    public static void main(String[] args) {
        System.out.println(infixToSuffix("a+b"));
        System.out.println(infixToSuffix("a+b-c"));
        System.out.println(infixToSuffix("a+b*c"));
        System.out.println(infixToSuffix("a*b-c"));
        System.out.println(infixToSuffix("(a+b)*c"));
        System.out.println(infixToSuffix("a+b*c+(d*e+f)*g"));
    }

       /**
     * 将中缀表达式转换为后缀表达式。
     * 后缀表达式也称为逆波兰表达式,它使用栈的操作来实现运算符的优先级处理,有效地简化了计算过程。
     * 
     * @param exp 中缀表达式字符串,包含数字、运算符和括号。
     * @return 后缀表达式字符串。
     */
    static String infixToSuffix(String exp) {
        // 使用链表作为栈来存储运算符
        LinkedList<Character> stack = new LinkedList<>();
        // 使用StringBuilder来构建后缀表达式
        StringBuilder sb = new StringBuilder(exp.length());
        
        // 遍历中缀表达式的每个字符
        for (int i = 0; i < exp.length(); i++) {
            char c = exp.charAt(i);
            
            // 根据字符的不同类型进行处理
            switch (c) {
                case '*', '/', '+', '-' -> {
                    // 处理运算符
                    if (stack.isEmpty()) {
                        stack.push(c);
                    } else {
                        if (priority(c) > priority(stack.peek())) {
                            stack.push(c);
                        } else {
                            // 当当前运算符的优先级不高于栈顶运算符时,将栈顶运算符弹出到后缀表达式中
                            while (!stack.isEmpty() && priority(stack.peek()) >= priority(c)) {
                                sb.append(stack.pop());
                            }
                            stack.push(c);
                        }
                    }
                }
                case '(' -> {
                    // 遇到左括号直接入栈
                    stack.push(c);
                }
                case ')' -> {
                    // 遇到右括号,将栈中的运算符依次弹出到后缀表达式中,直到遇到左括号
                    while (!stack.isEmpty() && stack.peek() != '(') {
                        sb.append(stack.pop());
                    }
                    // 弹出左括号,不加入到后缀表达式中
                    stack.pop();
                }
                default -> {
                    // 遇到数字直接加入到后缀表达式中
                    sb.append(c);
                }
            }
        }
        // 将栈中剩余的运算符依次弹出到后缀表达式中
        while (!stack.isEmpty()) {
            sb.append(stack.pop());
        }
        // 返回构建好的后缀表达式
        return sb.toString();
    }

/**
 * 计算运算符的优先级。
 * 
 * @param c 运算符
 * @return 运算符的优先级
 * @throws IllegalArgumentException 如果运算符不合法,则抛出此异常
 */
static int priority(char c) {
    // 使用switch表达式来根据运算符的类型返回对应的优先级
    return switch (c) {
        case '*' -> 2; // 乘法和除法具有相同的优先级
        case '/' -> 2;
        case '+' -> 1; // 加法和减法具有相同的优先级
        case '-' -> 1;
        case '(' -> 0; // 左括号具有最低优先级
        default -> throw new IllegalArgumentException("不合法的运算符:" + c);
    };
}

}

返回结果

ab+c*
abc*+d-e*
abc+*

E04. 双栈模拟队列-Leetcode 232

给力扣题目用的自实现栈,可以定义为静态内部类

class ArrayStack<E> {

    private E[] array;
    private int top; // 栈顶指针

    @SuppressWarnings("all")
    public ArrayStack(int capacity) {
        this.array = (E[]) new Object[capacity];
    }

    public boolean push(E value) {
        if (isFull()) {
            return false;
        }
        array[top++] = value;
        return true;
    }

    public E pop() {
        if (isEmpty()) {
            return null;
        }
        return array[--top];
    }

    public E peek() {
        if (isEmpty()) {
            return null;
        }
        return array[top - 1];
    }

    public boolean isEmpty() {
        return top == 0;
    }

    public boolean isFull() {
        return top == array.length;
    }
}

参考解答,注意:题目已说明

  • 调用 push、pop 等方法的次数最多 100
package com.itheima.datastructure.stack;

/**
 * 双栈模拟队列
 *
 * <ul>
 *     <li>调用 push、pop 等方法的次数最多 100</li>
 * </ul>
 */
public class E04Leetcode232 {

    /*
        队列头        队列尾
        b
        顶   底     底   顶
        s1              s2

        队列尾添加
            s2.push(a)
            s2.push(b)

        队列头移除
            先把 s2 的所有元素移动到 s1
            s1.pop()

     */
    ArrayStack<Integer> s1 = new ArrayStack<>(100);
    ArrayStack<Integer> s2 = new ArrayStack<>(100);

    public void push(int x) { //向队列尾添加
        s2.push(x);
    }

    public int pop() { // 从对列头移除
        if (s1.isEmpty()) {
            while (!s2.isEmpty()) {
                s1.push(s2.pop());
            }
        }
        return s1.pop();
    }

    public int peek() { // 从对列头获取
        if (s1.isEmpty()) {
            while (!s2.isEmpty()) {
                s1.push(s2.pop());
            }
        }
        return s1.peek();
    }

    public boolean empty() {
        return s1.isEmpty() && s2.isEmpty();
    }

}

E05. 单队列模拟栈-Leetcode 225

给力扣题目用的自实现队列,可以定义为静态内部类

public class ArrayQueue3<E> {

    private final E[] array;
    int head = 0;
    int tail = 0;

    @SuppressWarnings("all")
    public ArrayQueue3(int c) {
        c -= 1;
        c |= c >> 1;
        c |= c >> 2;
        c |= c >> 4;
        c |= c >> 8;
        c |= c >> 16;
        c += 1;
        array = (E[]) new Object[c];
    }
    
    public boolean offer(E value) {
        if (isFull()) {
            return false;
        }        
        array[tail & (array.length - 1)] = value;
        tail++;
        return true;
    }

    public E poll() {
        if (isEmpty()) {
            return null;
        }
        E value = array[head & (array.length - 1)];
        head++;
        return value;
    }

    public E peek() {
        if (isEmpty()) {
            return null;
        }
        return array[head & (array.length - 1)];
    }

    public boolean isEmpty() {
        return head == tail;
    }

    public boolean isFull() {
        return tail - head == array.length;
    }
}

参考解答,注意:题目已说明

  • 调用 push、pop 等方法的次数最多 100
  • 每次调用 pop 和 top 都能保证栈不为空
public class E05Leetcode225 {
    /*
        队列头     队列尾
        cba
        顶           底

        queue.offer(a)
        queue.offer(b)
        queue.offer(c)
     */
    ArrayQueue3<Integer> queue = new ArrayQueue3<>(100);
    int size = 0;
    public void push(int x) {
        queue.offer(x);
        for (int i = 0; i < size; i++) {
            queue.offer(queue.poll());
        }
        size++;
    }

    public int pop() {
        size--;
        return queue.poll();
    }

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

    public boolean empty() {
        return queue.isEmpty();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值