引言
在计算机科学和软件工程领域,数据结构是构建算法与程序设计的基础。栈(Stack)作为一种基础且实用的数据结构,遵循“后进先出”(Last In First Out, LIFO)原则,它的操作简单直观,广泛应用于各种编程场景。本文将深入剖析栈的基本概念、操作原理、实现方式以及实际应用场景。
一、栈的基本概念与原理
栈是一种线性数据结构,它仅允许在一端进行插入和删除操作,这一端称为栈顶(Top)。新元素总是被添加到栈顶,并且只有栈顶的元素才能被移除或访问,这种特性赋予了栈独特的逻辑特性——后进先出。
栈的主要操作包括:
- Push(入栈):将一个元素添加到栈顶。
- Pop(出栈):从栈顶移除并返回一个元素。
- Peek(查看栈顶元素):返回栈顶元素但不移除。
二、栈的实现方式
在Java等高级语言中,栈可以通过数组或链表来实现。下面分别简述两种实现方式:
1.基于数组的栈
- 基于数组的栈:使用固定大小的数组来存储元素,通过一个变量记录当前栈顶位置。当栈顶达到数组末尾时,若需要继续添加元素,则可能需要动态扩容,这会带来一定的性能开销。
class ArrayStack {
private int maxSize; //栈的大小
private int[] stack; //数组,数组模拟栈,数据就放在该数组
private int top = -1; //栈顶,初始化为-1
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[maxSize];
}
// 栈满
public boolean isFull() {
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty() {
return top == -1;
}
// 入栈
public void push(int value) {
if (isFull()) {
System.out.println("栈满");
return;
}
top++;
stack[top] = value;
}
// 出栈
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈空");
}
int value = stack[top];
top--;
return value;
}
// 得到栈顶值不弹出
public int peek() {
if (isEmpty()) {
throw new RuntimeException("栈空");
}
return stack[top];
}
// 显示栈
public void show() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
}
2.基于链表的栈
- 基于链表的栈: 可以使用单向链表的头节点作为栈顶,新增元素直接加在链表头部,删除元素则只需改变头节点引用。这样可以避免数组扩容的问题,但每个节点需要额外的空间存储指针信息。
class LinkedListStack {
private Hero head = new Hero(0, "");
// 栈空
public boolean isEmpty() {
return head.getNext() == null;
}
// 入栈
public void push(Hero heroNode) {
Hero temp = head;
while (true) {
if (temp.getNext() == null) {
break;
}
temp = temp.getNext();
}
temp.setNext(heroNode);
}
// 出栈
public Hero pop() {
Hero temp = head;
Hero value = null;
if (temp.getNext() == null) {
throw new RuntimeException("栈空");
}
while (true) {
if (temp.getNext().getNext() == null) {
break;
}
temp = temp.getNext();
}
value = temp.getNext();
temp.setNext(null);
return value;
}
// 得到栈顶值不弹出
public Hero peek() {
Hero temp = head;
if (temp.getNext() == null) {
throw new RuntimeException("栈空");
}
while (true) {
if (temp.getNext() == null) {
break;
}
temp = temp.getNext();
}
return temp;
}
// 显示栈
public void show() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
Hero reverseHead = new Hero(-1, "");
Hero next = null;
Hero cur = head.getNext();
while (cur != null) {
next = cur.getNext();
cur.setNext(reverseHead.getNext());
reverseHead.setNext(cur);
cur = next;
}
head.setNext(reverseHead.getNext());
Hero temp = head.getNext();
while (true) {
if (temp == null) {
break;
}
System.out.println(temp);
temp = temp.getNext();
}
}
}
class Hero {
private int no;
private String name;
private Hero next;
public Hero(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "Hero{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Hero getNext() {
return next;
}
public void setNext(Hero next) {
this.next = next;
}
}
三、栈的应用场景
栈由于其简洁高效的特性,在众多编程问题和系统设计中发挥着关键作用,如:
-
函数调用堆栈:在大多数编程语言中,函数调用过程都会形成一个隐式的调用栈,用于保存函数参数、局部变量及返回地址等信息。
-
表达式求值和计算:比如计算逆波兰表示法(Reverse Polish Notation, RPN)表达式时,可以利用栈存储运算数,并在遇到运算符时弹出相应数量的运算数进行计算。
-
深度优先搜索(DFS):在图论算法中,遍历图或树时常使用栈来辅助完成深度优先搜索。
-
浏览器的历史记录:浏览器的前进后退功能就是栈的一个典型应用实例,用户访问的网页顺序按照栈的规则进行存储和回溯。
-
括号匹配问题:在处理代码或者文本中的括号匹配问题时,可以用栈来检测括号是否正确配对。
四、引出栈的应用(计算器)
1. 使用栈完成表达式的计算
思路:
- 通过一个index值,遍历我们的表达式
- 发现是数字,就直接入数字栈
- 发现是字符符号,就如下二种情况
- 如果符号栈中有操作符,就将其进行比较,如果当前操作符的优先级小于等于栈中操作符的优先级,就从数字栈中pop出两个值,从符号栈中pop出一个值,进行运算,将运算结果入栈数字栈,再讲该符号入栈到符号栈
- 如果当前操作符的优先级大于栈中操作符的优先级,就直接入符号栈
- 当表达式扫描完毕后,就顺序从数字栈和符号栈中pop出对应值进行运算
- 数字栈中最后只剩一个值,就是该表达式结果
验证:
3 + 2 * 6 - 2 ?= 13
2.代码展示
便于进行计算和判断扫表的是数字还是符号这里增加对应解决方法
1.比较运算符优先级
// 返回运算符的优先级 优先级是程序员来定的,优先级使用数字表示,数字越大优先级越高
public int priority(int oper) {
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '+' || oper == '-') {
return 0;
} else {
return -1; //假定运算符只有+-*/
}
}
2.判断数字字符
// 判断是否为运算符
public boolean isOper(int val) {
return val == '+' || val == '-' || val == '*' || val == '/';
}
3.两数计算
// 计算方法
public int cal(int num1, int num2, int oper) {
int res = 0; //用于存放计算的结果
switch (oper) {
case '+':
res = num1 + num2;
break;
case '-':
res = num2 - num1; //注意顺序
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num2 / num1;
break;
default:
break;
}
return res;
}
4.计算器逻辑
public class Calculator {
public static void main(String[] args) {
//计算表达式
String expression = "71+2*6-4";
//创建存放数字栈
ArrayStack2 numStack = new ArrayStack2(10);
//创建存放符号栈
ArrayStack2 operStack = new ArrayStack2(10);
//定义需要的相关变量
int index = 0; //用于扫描栈的
int num1 = 0;
int num2 = 0;
int oper = 0;
int res = 0;
char ch = ' '; //将每次扫描的car保存到此
String keepNum = ""; //拼接多为数的
while (true) {
ch = expression.substring(index, index + 1).charAt(0);
// 判断ch是数字还是符号
if (numStack.isOper(ch)) { //符号
// 判断当前符号栈是否为空
if (!operStack.isEmpty()) {
// 不为空 处理
// 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,
// 就从符号栈中pop出一个符号,进行计算,将得到结果入数字栈,然后将当前的操作符入符号栈
if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.cal(num1, num2, oper);
numStack.push(res);
operStack.push(ch);
} else {
// 如果当前的符号优先级大于栈中顶部符号的优先级那么直接入栈
operStack.push(ch);
}
} else {
// 为空 直接入栈操作
operStack.push(ch);
}
} else { //数字
// 这里扫描的数字是字符类型
// 例如 '1' = 49 '2' = 50 所以需要 ch - 48
// 分析思路
// 1.当处理多位数时,不能发现是一个数就直接入栈,因为可能是多位数
// 2.在处理数时,需要向expression的表达式的index后看一位是不是数字还是字符
// 如果是数字就继续扫描,是符号就入栈
// 3.需要定义一个变量拼接
// 处理多位数
keepNum += ch;
// 如果ch已经是最后一位就无须往后看
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(keepNum));
} else {
// 往后看一位,不影响index值
if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) {
numStack.push(Integer.parseInt(keepNum));
// 重要!!!!!KeepNum清空
keepNum = "";
}
}
}
// 让index+1并判断是否扫描到最后
index++;
if (index >= expression.length()) {
break;
}
}
// 扫描完毕,就顺序从数组栈和符号栈pop相应的数和符号进行运算,
// 数字栈将会有最后一个数组,就是该表达式的结构
while (true) {
// 如果符号栈为空就结束
if (operStack.isEmpty()) {
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.cal(num1, num2, oper);
numStack.push(res);
}
System.out.printf("表达式%s的结果为%d", expression, numStack.pop());
}
}
五、总结
总之,栈作为最基础的数据结构之一,不仅在理论层面为我们提供了理解复杂问题的新视角,而且在实践中为许多算法和系统提供了高效可靠的解决方案。掌握栈的概念和运用技巧,对于提升编程能力有着不可忽视的价值。