前言
- 學習棧
- 瞭解棧的特點。
先進後出,後進先出
。 - 使用數組模擬棧、鏈表模擬棧。(相比於雙鏈表和單向環形鏈表來說是相對簡單的)
- 棧的應用實例:
用棧+中綴表達式 實現綜合計算器
。 - 2020.4.21、22 日學習
- 相對於前面的雙鏈表和單向環形鏈表,這裏的數組模擬棧、鏈表模擬棧是比較簡單的。
比較重要的是 棧的應用
:棧+中綴表達式 實現綜合計算器。關於中綴在下一節博客學習、講解。
一、棧
1.1 棧的介紹
- 棧的英文爲(stack)
- 棧是一個
先入後出
(FILO-First In Last Out)的有序列表。 - 棧(stack)是限制線性表中元素的插入和刪除
只能在線性表的同一端
進行的一種特殊線性表。允許插入和刪除的一端,爲變化的一端,稱爲棧頂
(Top),另一端爲固定的一端,稱爲棧底
(Bottom)。 - 根據棧的定義可知,最先放入棧中元素在棧底,最後放入的元素在棧頂,而刪除元素剛好相反,最後放入的元素最先刪除,最先放入的元素最後刪除。
- 棧的基礎方法爲
入棧、出棧、顯示棧數據
。
1.2 棧的應用實例【重點】
請輸入一個表達式
計算式:[722-5+1-5+3-3] 點擊計算【如下圖】
請問: 計算機底層是如何運算得到結果的? 注意不是簡單的把算式列出運算,因爲我們看這個算式 7 * 2 * 2 - 5
, 但是計算機怎麼理解這個算式的(對計算機而言,它接收到的就是一個 字符串 ),我們討論的是這個問題。-> 棧
1.3 棧的應用場景
- 子程序的調用:在跳往子程序前,會先將下個指令的地址存到堆棧中,直到子程序執行完後再將地址取出,以回到原來的程序中。
- 處理遞歸調用:和子程序的調用類似,只是除了儲存下一個指令的地址外,也將參數、區域變量等數據存入堆棧中。
- 表達式的轉換:[中綴表達式轉後綴表達式]與求值(實際解決)。
- 二叉樹的遍歷。
- 圖形的深度優先(depth一first)搜索法。
1.4 入棧與出棧
直接上圖,看圖理解:
二、數據模擬棧
2.1 思路分析
- 由於棧是一種有序列表,當然可以使用數組的結構來儲存棧的數據內容,下面我們就用
數組模擬棧的出棧,入棧等操作
- 實現思路分析,並畫出示意圖
- 使用數組來模擬棧
定義一個 top 來表示棧頂,初始化 爲 -1
入棧
的操作,當有數據加入到棧時,top++; stack[top] = data;
出棧
的操作,int value = stack[top]; top--, return value
2.2 代碼結構
ArrayStack: 數組模擬棧的 類
ArrayStackMain:測試類
2.3 ArrayStack 棧類
package com.feng.ch06_stack.s1_arraystack;
/*
* 定義一個 ArrayStack 表示棧,數組模擬 棧
* */
public class ArrayStack {
private int maxSize; // 棧的大小
private int[] stack; // 數組,數組模擬棧,數據就放在數組中。
private int top = -1; // top 表示棧頂,初始化爲-1
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
this.stack = new int[this.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("棧已空,無法獲取數據\n");
}
int num = stack[top];
top--;
return num;
}
// 顯示棧的情況【遍歷棧】,遍歷時,需要從棧頂開始顯示數據
public void list(){
if (isEmpty()){
System.out.printf("棧空,無數據\n");
}
for (int i = top; i>=0; i--){
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
}
2.4 ArrayStackMain 測試類
package com.feng.ch06_stack.s1_arraystack;
import java.util.Scanner;
public abstract class ArrayStackMain {
public static void main(String[] args) {
// 測試 ArrayStack 是否正確
// 先創建一個 ArrayStack 對象 -》棧
ArrayStack stack = new ArrayStack(4);
String key = "";
boolean loop = true;
Scanner scanner = new Scanner(System.in);
while (loop) {
System.out.println("show:表示顯示棧");
System.out.println("exit:退出程序");
System.out.println("push:表示添加數據到棧(入棧)");
System.out.println("pop:表示從棧取數據(出棧)");
System.out.println("請輸入您的選擇");
key = scanner.next();
switch (key) {
case "show":
stack.list();
break;
case "push":
System.out.println("請輸入一個數:");
int value = scanner.nextInt();
stack.push(value);
break;
case "pop":
try {
int res = stack.pop();
System.out.printf("出棧的數據是 %d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "exit":
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.printf("程序退出成功~~~");
}
}
2.5 測試結果
簡單運行測試一下,運行即可
三、單鏈表模擬棧
3.1 方法分析
棧的基礎方法爲入棧(push)、出棧(pop)、顯示棧數據(list)
- 先創建頭節點head。
- push():第一次添加時,直接添加到頭結點的後面:
head.setNext(newNode);
第二次往後,先將頭結點後面的節點掛載到newNode上,再將newNode掛載到head 上。即:
newNode.setNext(head.getNext())
;
head.setNext(newNode);
- pop():直接輸出頭結點head後的一個節點即可,再將後一個節點的後一個節點掛載到 頭節點即可:
StackNode next = head.getNext()
;
head.setNext(next.getNext())
;
return next;
- list():設置輔助指針。循環遍歷輸出即可。
3.2 代碼結構
StackNode: 鏈表節點
LinkedStack: 棧方法
LinkedStackMain: 測試方法
3.3 StackNode 鏈表節點
package com.feng.ch06_stack.s2_linkedstack;
public class StackNode {
private int no;
private StackNode next;
public StackNode(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public StackNode getNext() {
return next;
}
public void setNext(StackNode next) {
this.next = next;
}
@Override
public String toString() {
return "StackNode{" +
"no=" + no +
'}';
}
}
3.4 LinkedStack 棧方法
package com.feng.ch06_stack.s2_linkedstack;
public class LinkedStack {
private StackNode head = new StackNode(0);
public StackNode getHead() {
return head;
}
/*
* 入棧
* */
public void push(StackNode newNode) {
if (head.getNext() == null) { // 第一次天機
head.setNext(newNode);
} else {
newNode.setNext(head.getNext());
head.setNext(newNode);
}
}
/*
* 出棧
* */
public StackNode pop(){
if (head.getNext() == null){
throw new RuntimeException("棧爲空,無法出棧");
}
/*
* 兩種返回方式
* */
// int value = head.getNext().getNo();
// head.setNext(head.getNext().getNext());
// return value;
StackNode next = head.getNext();
head.setNext(next.getNext());
return next;
}
/*
* 遍歷
* */
public void list(){
if (head.getNext() == null){
System.out.println("棧爲空,無法遍歷");
return;
}
StackNode temporary = head.getNext();
while (true){
if (temporary == null){
break;
}
System.out.printf("節點編號%d\n",temporary.getNo());
temporary = temporary.getNext();
}
}
}
3.5 LinkedStackMain 測試方法
package com.feng.ch06_stack.s2_linkedstack;
public class LinkedStackMain {
public static void main(String[] args) {
LinkedStack stack = new LinkedStack();
StackNode node1 = new StackNode(1);
StackNode node2 = new StackNode(2);
StackNode node3 = new StackNode(3);
StackNode node4 = new StackNode(4);
StackNode node5 = new StackNode(5);
stack.push(node1);
stack.push(node2);
stack.push(node3);
stack.push(node4);
stack.push(node5);
System.out.println("初始化的棧:");
stack.list();
// 測試出棧
System.out.println();
System.out.println("出棧的數據:"+stack.pop());
System.out.println();
System.out.println("出棧後的棧:");
stack.list();
}
}
3.5 測試結果
四、棧+中綴表達式 實現綜合計算器
4.1 需求描述
給定一個表達式字符串:【3+2*6-2】, 進行計算。
中綴表達式 就是 3+2*6-2 這種我們熟知的計算表達式。關於中綴表達式在下一節詳細講解
4.2 思路分析
- 使用上面開發的數組模擬棧,
新建兩個棧,一個數棧、一個符號棧
。 - 通過一個 index 值(索引),來遍歷我們的表達式
- 如果我們
發現是一個數字
, 就直接入數棧 - 如果發現
掃描到是一個符號
, 就分如下情況
4.1 如果發現當前的符號棧爲 空,就直接入棧
4.2 如果符號棧有操作符,就進行比較,如果 當前的操作符的優先級小於或者等於棧中的操作符
, 就需要從數棧中pop出兩個數,在從符號棧中pop出一個符號,進行運算,將得到結果,入數棧,然後將當前的操作符入符號棧,如果當前的操作符的優先級大於棧中的操作符, 就直接入符號棧
。 - 當表達式掃描完畢,就順序的從 數棧和符號棧中pop出相應的數和符號,並運行.
- 最後在數棧只有一個數字,就是表達式的結果
驗證: 3+2*6-2 = 13
4.3 代碼實現
- 先實現一位數的運算。
- 擴展到多位數的運算, 擴展多位的運算,主要改變的地方:
- 增加了一個變量
keepNum
用戶多位數的拼接, 然後在掃描時,遇到的字符 爲數字時,再往後多看一位,如果是數字,則拼接,如果不是數組,則到此結束。
4.3.1 Calculator.java 計算類
package com.feng.ch06_stack.s3_calculator;
public class Calculator {
public static void main(String[] args) {
// 根據前面思路,完成表達式的運算
String expression = "3+2*6-2"; // 3+2*6-2=13
// 思考:如果處理多位數的問題? 30+2*6-2=40 。添加 keepNum 用於拼接多位數。
// 創建兩個棧,數棧、一個符號棧
ArrayStack numStack = new ArrayStack(10);
ArrayStack operStack = new ArrayStack(10);
// 定義需要的相關變量
int index = 0; // 用於掃描
int num1 = 0;
int num2 = 0;
int oper = 0;
int res = 0;
char ch = ' '; // 將每次掃描得到插入保存到 ch
String keepNum = ""; // 用於拼接多位數
// 開始循環的掃描 expression
while (true) {
// 依次得到expression 的每一個字符
ch = expression.substring(index, index + 1).charAt(0);
/*
* 判斷 ch 是什麼,然後做相應的處理
* 如果是運算符
* */
if (operStack.isOperation(ch)) {
// 判斷當前的符號棧是否爲空
if (!operStack.isEmpty()) {
// 如果符號棧有操作符,就進行比較,如果當前的操作符的優先級小於或者等於棧中的操作符,就需要從數棧中 pop 出兩個數,
// 在符號棧中 pop 出一個符號,進行運算,將得到結果,入數棧,然後將當前的操作符入符號棧。
if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
oper = operStack.pop();
num1 = numStack.pop();
num2 = numStack.pop();
res = numStack.calculation(num1, num2, oper);
// 把運算的結果入數棧
numStack.push(res);
// 然後把當前的操作符入符號棧
operStack.push(ch);
} else {
// 如果當前的操作符的優先級大於棧中的操作符,就直接入符號棧
operStack.push(ch);
}
} else {
// 如果爲空,直接入棧。。
operStack.push(ch);
}
} else {// 如果是數
/*
* 如果是數,就直接入數棧, 這樣不能滿足 多位數。。。
* // ? "1+3" '1' => 1, ch 爲字符,轉換爲 數字要 減48。
* 如果是個位數,用字符 char 來表示,則要減48,如果是多位數,下邊變成了字符串,就直接使用包裝類的方法轉換成 整型
* */
//numStack.push(ch-48);
/*
* 解決多位數的 分析思路:
* 1、當處理多位數時,不能發現是一個數就立即入棧,因爲他可能是多位數
* 2、在處理數時,需要向expression 的表達式的index 後在多看一位,如果是數就立刻掃描,如果是符號在入棧
* 3、因此定義一個 變量 字符串,用於拼接
* */
// 處理多位數
keepNum += ch;
// 如果 ch 已經是expression 的最後一位,就直接入棧
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(keepNum));
} else {
//判斷下一個字符是不是數字,如果是數字,就繼續掃描,如果是運算符,則入棧
// 注意是看後一位,不是 index++
if (operStack.isOperation(expression.substring(index + 1, index + 2).charAt(0))) {
// 如果後一位是運算符,則入棧, keepNum = "1" 或者 "123"
numStack.push(Integer.parseInt(keepNum));
keepNum = "";
}
}
}
// 讓index +1 ,並判斷 是否掃描到 expression 最後
index++;
if (index >= expression.length()) {
break;
}
}
/*
* 當表達式掃描完畢,就順序的從數棧 和符號棧 中 pop 出相應的數和符號,並運行
* */
while (true) {
// 如果符號棧爲空,則計算到最後的結果,數棧中只有一個數字【結果】
if (operStack.isEmpty()) {
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.calculation(num1, num2, oper);
numStack.push(res);
}
// 將數棧的最後數,pop出,就是結果
int result = numStack.pop();
System.out.printf("表達式 %s = %d", expression, result);
}
}
4.3.2 ArrayStack.java 使用的棧(自定義,數組模擬棧)
對於之前的數組模擬棧、鏈表模擬棧,這裏需要增加幾個方法:
peek(): 查看棧頂數據,但不是出棧。
priority(int oper):返回運算符的優先級,優先級是程序員來確定,優先級使用數字表示。
calculation(int num1, int num2, int oper):計算方法
package com.feng.ch06_stack.s3_calculator;
/*
* 先創建一個棧,直接使用前面創建好的,
* 需要擴展功能:增加
* 定義一個 ArrayStack 表示棧
* */
public class ArrayStack {
private int maxSize; // 棧的大小
private int[] stack; // 數組,數組模擬棧,數據就放在數組中。
private int top = -1; // top 表示棧頂,初始化爲-1
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
this.stack = new int[this.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("棧已空,無法獲取數據\n");
}
int num = stack[top];
top--;
return num;
}
// 顯示棧的情況【遍歷棧】,遍歷時,需要從棧頂開始顯示數據
public void list() {
if (isEmpty()) {
System.out.printf("棧空,無數據\n");
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
/*
* 查看棧頂的 數據, 不是真正出棧
* */
public int peek(){
return stack[top];
}
/*
* 返回運算符的優先級,優先級是程序員來確定,優先級使用數字表示
* 數字越大,則優先級越高。
* */
public int priority(int oper) {
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '+' || oper == '-') {
return 0;
} else {
return -1; // 假定目前的表達式只有 + - * /
}
}
/*
* 判斷是不是一個雲算法
* */
public boolean isOperation(char value) {
return value == '+' || value == '-' || value == '*' || value == '/';
}
/*
* 計算方法
* @param num1
* @param num2
* @param oper
* @return
* */
public int calculation(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 = num1/num2;
break;
default:
break;
}
return res;
}
}