引言: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),,它是一種運算受限的線性表。 其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱爲棧頂,相對地,把另一端稱爲棧底(這句話是性質)。和彈夾的進出順序是一樣的。
進棧:入棧或壓棧,將新元素放到棧頂元素的上面,使之成爲新的棧頂元素。
出棧:退棧,將棧頂元素刪除掉,使得與其相鄰的元素成爲新的棧頂元素。
棧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語言版)》嚴蔚敏、吳偉民著)
棧的順序,鏈接實現
棧的數組(順序)實現
/**
* 棧的數組實現--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());
}
}
}
棧的鏈表(鏈接)實現
/**
* 棧的鏈表實現--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());
}
}
}
棧的應用
括號匹配的檢驗-有效括號問題
// 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 == '}');
}
}
棧的經典應用-逆波蘭表達式法
中綴表達式轉爲後綴表達式:
設置一個堆棧,初始時將堆棧頂設置爲#
順序讀入中綴表達式,到讀到的單詞爲數字時將其輸出,接着讀下一個單詞;
令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;
}
}
棧與遞歸
棧:限定僅在表尾進行插入和刪除操作的線性表。
遞歸:直接調用自己或通過一系列的調用語句間接地調用自己的函數,稱做遞歸函數。
函數的遞歸調用和普通函數調用是一樣的。當程序執行到某個函數時,將這個函數進行入棧操作,在入棧之前,通常需要完成三件事。
1、將所有的實參、返回地址等信息傳遞給被調函數保存。
2、爲被調函數的局部變量分配存儲區。
3、將控制轉移到北調函數入口。
當一個函數完成之後會進行出棧操作,出棧之前同樣要完成三件事。
1、保存被調函數的計算結果。
2、釋放被調函數的數據區。
3、依照被調函數保存的返回地址將控制轉移到調用函數。
上述操作必須通過棧來實現,即將整個程序的運行空間安排在一個棧中。每當運行一個函數時,就在棧頂分配空間,函數退出後,釋放這塊空間。所以當前運行的函數一定在棧頂。
(注:摘自嚴蔚敏等人的數據結構c語言版)
JVM內存棧
JVM:在java編譯器和os平臺之間的虛擬處理器,JVM會遞歸調用爲例,一個棧幀包括局部變量表、操作數棧、棧數據區