接上次博客:数据结构初阶(4)(OJ练习【判断链表中是否有环、返回链表入口点、删除链表中的所有重复出现的元素】、双向链表LinkedList【注意事项、构造方法、常用方法、模拟实现、遍历方法、顺序表和链表的区别)_di-Dora的博客-CSDN博客
目录
栈(Stack)的概念
栈的模拟实现
栈的应用及练习
1. 改变元素的序列 :
2. 将递归转化为循环
3、括号匹配(出现概率高)
4、逆波兰表达式求值
5、出栈入栈次序匹配
6、最小栈
链栈和顺序栈
栈、虚拟机栈、栈帧的区别
栈(Stack)是一种常见的数据结构,它遵循后进先出(Last-In-First-Out,LIFO)的原则。它是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈可以想象成一堆盘子,你只能从最上面放入盘子或者取出最上面的盘子,无法直接访问或操作其他位置的盘子。
栈的两个基本操作是压栈(Push)和出栈(Pop):
1、压栈(Push):将一个元素添加到栈的顶部。新的元素成为栈的新顶部,原来的顶部元素在它下方。也可以说是将元素放入栈顶。
2、出栈(Pop):从栈的顶部移除一个元素,并返回该元素的值。栈的新顶部将是原来顶部下方的元素。也可以说是从栈顶取出元素。
除了压栈和出栈操作外,栈还具有以下特点:
package java.util;
public
class Stack extends Vector {
public Stack() {
}
public E push(E item) {
addElement(item);
return item;
}
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
public boolean empty() {
return size() == 0;
}
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
private static final long serialVersionUID = 1224463164541339165L;
}
我们可以简单使用一下:
public static void main(String[] args) {
Stack stack = new Stack<>();
stack.push(12);
stack.push(23);
stack.push(34);
//删除
Integer x = stack.pop();
System.out.println(x);
//获取栈顶元素但是不删除
int ret = stack.peek();
System.out.println(ret);
ret = stack.peek(); //还是原来的那个元素
System.out.println(ret);
System.out.println(stack.size());
}
我们发现,栈的源代码里面好像不包含成员变量?
Vector是一种动态数组结构,在Java中被实现为一个类。它与普通的数组相似,但具有动态增长的能力。Vector可以根据需要自动调整其大小,以容纳任意数量的元素。
Vector内部使用一个对象数组来存储元素,并通过索引访问这些元素。与普通数组相比,
Vector具有以下特点:
- 动态大小:Vector的大小可以根据需要进行动态调整。当元素数量超过当前容量时,Vector会自动增加其容量,以容纳更多元素。这使得Vector非常适合在需要经常进行插入和删除操作的情况下使用。
- 线程安全:Vector是线程安全的,这意味着它可以在多线程环境中使用而无需额外的同步措施。Vector的方法在执行时会进行同步,以确保线程安全。
Vector类提供了一系列方法来操作和访问其中的元素,包括添加元素、删除元素、访问元素、搜索元素等。一些常用的方法包括:
- add(element): 在Vector的末尾添加一个元素。
- remove(element): 删除Vector中的指定元素。
- get(index): 获取指定索引位置的元素。
- size(): 返回Vector中元素的数量。
- isEmpty(): 检查Vector是否为空。
需要注意的是,从Java 1.2版本开始,推荐使用更为高级的ArrayList类替代Vector,因为ArrayList在大多数情况下具有更好的性能。但如果需要在多线程环境中使用,或者需要与旧版本的Java代码兼容,仍然可以使用Vector。
现在我们开始实现栈:
import java.util.Arrays;
public class MyStack {
private int[] elem;
private int usedSize;//不初始化,默认为0 可以代表下标
private static final int DEFAULT_CAPACITY = 10;
public MyStack() {
this.elem = new int[DEFAULT_CAPACITY];
}
public void push(int val) {
if(isFull()) {
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
this.usedSize++;
}
public boolean isFull() {
return this.usedSize == this.elem.length;
}
public int pop() {
if(isEmpty()) {
throw new EmptyException();
}
int oldVal = elem[usedSize-1];
this.usedSize--;
return oldVal;
}
public int peek() {
if(isEmpty()) {
throw new EmptyException();
}
return elem[usedSize-1];
}
public boolean isEmpty() {
return this.usedSize == 0;
}
}
使用一下看看:
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.push(3);
System.out.println(myStack.peek());//3
System.out.println(myStack.pop());//3
System.out.println(myStack.pop());//2
System.out.println(myStack.pop());//1
System.out.println(myStack.isEmpty());//true
System.out.println(myStack.pop());//异常
}
大家应该发现了,栈的实现不难,但是题很多。
废话少说,现在开始:
1. 若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是(C)
A: 1,4,3,2 例如:进来1,出1,进来2、3、4,再依次出栈,✔
B: 2,3,4,1
C: 3,1,4,2 要出来1,前面必要有2。
D: 3,4,2,1
2.一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( B )。
A: 12345ABCDE
B: EDCBA54321
C: ABCDE12345
D: 54321EDCBA
比如:逆序打印链表
// 递归方式
void printList(Node head){
if(null != head){
printList(head.next);
System.out.print(head.val + " ");
}
}
所以现在可以给我们之前的链表加上一个包含了栈的新的方法:逆序打印:
// 循环方式
public void reversePrintList(Node head){
if(null == head){
return;
}
Stack stack = new Stack<>();//给一个栈放节点
Node cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
while (!stack.isEmpty()) {
Node top = stack.pop();
System.out.print(top.val+" ");
}
System.out.println();
}
力扣:20. 有效的括号 - 力扣(Leetcode)
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
这道题为什么用栈比较好?
栈的插入和删除的时间复杂度是O(1)。
大致思路如下:
只要是左括号就入栈;遇到右括号就开始匹配;
当且仅当字符串遍历完,并且栈为空的时候才匹配。
public boolean isValid(String s) {
Stack stack = new Stack<>();
for(int i = 0;i < s.length();i++) {
char ch = s.charAt(i);//肯定是括号!
if(ch == '(' || ch == '{' || ch == '[') {
//左括号
stack.push(ch);
}else {
//右括号
if(stack.empty()) {
return false;//右括号不匹配
}
char top = stack.peek();
//此时top是左括号 ch是右括号
if(ch == ')' && top == '(' || ch == '}' && top == '{' || ch == ']' && top == '[') {
stack.pop();
}else{
return false;//不匹配
}
}
}
if(!stack.empty()) {
return false;//左括号不匹配
}
return true;
}
或者:
public class ValidParentheses {
public boolean isValid(String s) {
Stack stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '{' || c == '[') {
stack.push(c);
} else if (c == ')' || c == '}' || c == ']') {
if (stack.isEmpty()) {
return false; // 当前右括号没有匹配的左括号
}
char top = stack.pop();
if ((c == ')' && top != '(') || (c == '}' && top != '{') || (c == ']' && top != '[')) {
return false; // 当前右括号与栈顶的左括号类型不匹配
}
}
}
return stack.isEmpty(); // 所有左括号都匹配完了,栈应该为空
}
我们可以看一下力扣官方的题解:
class Solution {
public boolean isValid(String s) {
int n = s.length();
if (n % 2 == 1) {
return false;
}
// 使用一个映射来存储左右括号的对应关系
Map pairs = new HashMap() {{
put(')', '(');
put(']', '[');
put('}', '{');
}};
Deque stack = new LinkedList();
for (int i = 0; i < n; i++) {
char ch = s.charAt(i);
if (pairs.containsKey(ch)) {
if (stack.isEmpty() || stack.peek() != pairs.get(ch)) {
return false;
}
stack.pop();
} else {
stack.push(ch);
}
}
return stack.isEmpty();
}
}
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
力扣:150. 逆波兰表达式求值 - 力扣(Leetcode)
要做这个题,我们需要先了解几个概念:
中缀表达式和后缀表达式(逆波兰表达式):
1、中缀表达式是我们通常使用的表达式表示方法,其中运算符位于操作数的中间。
例如,2 + 3 * 4 就是一个中缀表达式。中缀表达式通常需要通过运算符优先级和括号—— ( 1 + 4 ) * 5 - 7 来确定运算的顺序。
2、后缀表达式,也称为逆波兰表达式(Reverse Polish Notation,RPN),是一种将运算符放置在操作数之后的表达式表示方法。在后缀表达式中,运算的顺序可以直接由表达式本身的结构决定,而无需括号或运算符优先级的考虑。例如,中缀表达式 2 + 3 * 4 可以转换为后缀表达式 2 3 4 * +。
为什么要有后缀表达式?
后缀表达式的计算可以通过使用栈来实现。遍历后缀表达式,遇到操作数时将其入栈,遇到运算符时从栈中取出相应数量的操作数进行计算(先弹出来的作为右操作数,后出来的作为左操作数),并将计算结果入栈。最后栈中剩下的元素就是最终的计算结果。
后缀表达式的优点是可以消除运算符优先级和括号带来的歧义,计算过程简单明了。它常用于计算机科学领域中的编译器、解释器和计算器等场景。
相应的,前缀表达式,也称为波兰前缀表达式,其中运算符位于操作数之前。例如,对于表达式 "2 + 3 * 4",在前缀表达式中可以表示为 "+ 2 * 3 4"。
public int evalRPN(String[] tokens) {
Stack stack = new Stack<>();
for(String s : tokens) {
if(!isOperation(s)) {
//数字字符
stack.push(Integer.parseInt(s));
}else {
// 有可能是加减乘除 当中的一个运算符
int num2 = stack.pop();
int num1 = stack.pop();
switch(s) {
case "+":
stack.push(num1 + num2);
break;
case "-":
stack.push(num1 - num2);
break;
case "*":
stack.push(num1 * num2);
break;
case "/":
stack.push(num1 / num2);
break;
}
}
}
return stack.pop();
}
private boolean isOperation(String s) {
if(s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")) {
return true;
}
return false;
}
或者:
import java.util.Stack;
class Solution {
public int evalRPN(String[] tokens) {
Stack stack = new Stack<>();
for (String token : tokens) {
if (isOperator(token)) {
int num2 = stack.pop();
int num1 = stack.pop();
int result = calculate(num1, num2, token);
stack.push(result);
} else {
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}
private boolean isOperator(String token) {
return token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/");
}
private int calculate(int num1, int num2, String operator) {
switch (operator) {
case "+":
return num1 + num2;
case "-":
return num1 - num2;
case "*":
return num1 * num2;
case "/":
return num1 / num2;
default:
return 0;
}
}
}
这是力扣的官方题解,用数组实现:
class Solution {
public int evalRPN(String[] tokens) {
int n = tokens.length;
int[] stack = new int[(n + 1) / 2];
int index = -1;
for (int i = 0; i < n; i++) {
String token = tokens[i];
switch (token) {
case "+":
index--;
stack[index] += stack[index + 1];
break;
case "-":
index--;
stack[index] -= stack[index + 1];
break;
case "*":
index--;
stack[index] *= stack[index + 1];
break;
case "/":
index--;
stack[index] /= stack[index + 1];
break;
default:
index++;
stack[index] = Integer.parseInt(token);
}
}
return stack[index];
}
}
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
牛客网链接:栈的压入、弹出序列_牛客题霸_牛客网 (nowcoder.com)
这个题熟不熟悉?是不是就类似于我们刚刚做的选择题的代码实现?
来挑战一下吧!
代码的大致实现思路如下:
import java.util.Stack;
public class Solution {
public boolean IsPopOrder(int[] pushA, int[] popA) {
Stack stack = new Stack<>();
int pushIndex = 0;
int popIndex = 0;
while (popIndex < popA.length) {
// 如果栈顶元素和当前弹出序列元素相同,则直接弹出
if (!stack.isEmpty() && stack.peek() == popA[popIndex]) {
stack.pop();
popIndex++;
}
// 如果栈为空或者栈顶元素和当前弹出序列元素不同
else {
// 如果还有元素可以压入栈,则将元素压入栈
if (pushIndex < pushA.length) {
stack.push(pushA[pushIndex]);
pushIndex++;
}
// 否则说明无法满足当前的弹出序列,返回false
else {
return false;
}
}
}
// 如果所有的弹出序列元素都被处理完,且栈为空,则说明弹出序列是可能的
return stack.isEmpty();
}
}
或者稍微变一下:
public class Solution {
public boolean IsPopOrder(int[] pushA, int[] popA) {
Stack stack = new Stack<>();
int j=0;
for(int i=0;i
这样好像更清晰明了,对吧?
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
import java.util.Stack;
class MinStack {
private Stack stack;
private Stack minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
if(!stack.empty()) {
if (stack.peek().equals(minStack.peek())) {
minStack.pop();
}
stack.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
在MinStack类中,我们使用两个栈,一个用于存储数据的普通栈(stack),另一个用于存储最小元素的栈(minStack)。每当push一个新元素时,我们将其压入普通栈,并检查是否需要将其压入最小栈。如果最小栈为空或者新元素小于等于最小栈的栈顶元素,则将其压入最小栈。在pop操作时,如果普通栈的栈顶元素和最小栈的栈顶元素相等,则同时将它们出栈。getMin操作只需要返回最小栈的栈顶元素即可。
这样设计,无论是push、pop、top还是getMin操作,都可以在常数时间内完成。
我们可以再把上面的部分代码稍微展开,写得更明白一些:
class MinStack {
private Stack stack;
private Stack minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
if(minStack.empty()) {
minStack.push(val);
}else {
if(val <= minStack.peek()) {
minStack.push(val);
}
}
}
public void pop() {
if(!stack.empty()) {
int ret = stack.pop();
if(minStack.peek() == ret) {
minStack.pop();
}
}
}
//获取正常栈顶元素
public int top() {
if(stack.empty()) {
return -1;
}
return stack.peek();
}
//获取最小栈顶元素
public int getMin() {
if(minStack.empty()) {
return -1;
}
return minStack.peek();
}
}
栈不止可以基于数组来实现,还可以基于链表实现:
链栈和顺序栈都是栈的实现方式,它们的主要区别在于数据的存储方式和操作的实现方式。
顺序栈(Sequential Stack):
顺序栈使用数组来实现,是一种连续存储的栈结构。栈顶指针指向数组的最后一个元素,当有新的元素入栈时,栈顶指针向后移动,指向新的栈顶元素。当元素出栈时,栈顶指针向前移动,指向新的栈顶元素。顺序栈的特点是访问元素的时间复杂度为O(1),但是在栈满时需要进行扩容操作。
链栈(Linked Stack):
链栈使用链表来实现,每个节点包含一个数据元素和一个指向下一个节点的指针。链栈的栈顶即链表的头节点,新元素入栈时,将其作为新的头节点。元素出栈时,将头节点移除,并将指针指向下一个节点。链栈的特点是不需要进行扩容操作,可以动态地分配内存,但是访问元素的时间复杂度为O(n),其中n是栈的长度。
顺序栈适用于知道栈的最大容量或者容量变化不大的情况,可以实现高效的元素访问。链栈适用于容量变化较大或者不确定的情况,可以动态地分配内存,但是在访问元素时需要遍历链表,效率较低。选择使用哪种栈实现取决于具体的应用场景和需求。
在计算机科学中,栈(Stack)、虚拟机栈(Virtual Machine Stack)和栈帧(Stack Frame)是相关概念,它们在不同的层次和上下文中有着不同的含义和功能。
栈(Stack):
栈是一种数据结构,用于存储和管理数据的一种方式。它遵循后进先出(LIFO)的原则,即最后进入栈的元素将首先被移除。
在计算机中,栈通常被用于管理函数调用和返回,以及保存临时数据等。
栈可以在内存中的任何位置实现,通常具有固定的大小。
虚拟机栈(Virtual Machine Stack):
虚拟机栈是在虚拟机(如Java虚拟机)中的一种数据结构,用于支持程序的执行。
每个线程在虚拟机中都有自己的虚拟机栈,用于存储方法调用的信息。
虚拟机栈的大小可以在虚拟机启动时预先定义,或者根据需要动态调整。
栈帧(Stack Frame):
栈帧是在函数调用过程中,用于存储有关调用函数的信息和局部变量的数据结构。
每个函数调用都会创建一个新的栈帧,它包含了函数的参数、局部变量、返回地址等。
栈帧通常由一些特定的字段组成,如局部变量表、操作数栈、动态链接等,用于支持函数的执行和返回。
简而言之,栈是一种通用的数据结构,虚拟机栈是在虚拟机中为每个线程维护的数据结构,而栈帧是在函数调用中用于存储函数执行信息的数据结构。栈帧是虚拟机栈中的一个元素,用于支持函数调用和执行。