棧模型##
棧(Stack)是限制插入和刪除只能在一個位子上進行的表,該位子是表的末端,叫做棧的頂(top)。對棧的基本操作有進棧(push)和出棧(pop),相當于表插入和刪除最后一個元素。
棧的實現##
由于棧可以看作是一個表,因此任何實現表的方法都能實現棧。顯然,在Java集合中,ArrayList和LinkedList都支持棧的操作,由于棧的操作都是棧頂元素,所以對于數組實現的ArrayList和鏈表實現的LinkedList來說都花費常數時間,因此他們并沒有太大區別。但是對于代碼實現來說,數組實現更加簡潔,更易于理解,下面將給出棧的簡單實現。
public class Stack<E> {
//棧的默認大小為10
private static final int DEFAULT_VALUE_SIZE = 10;
//用一個數組來存儲棧的元素
private E[] elementData;
//棧中元素的實際個數
private int size;
//構造器,調用doClear方法初始化
public Stack() {
doClear();
}
//進棧操作
public E push(E element) {
//存放棧元素的數組滿了,則需要對elementData擴容
if (elementData.length == size) {
//擴容操作
ensureCapacity(size + 1);
}
elementData[size++] = element;
return element;
}
//出棧操作,若棧為空時出棧,則拋出數組越界異常
public E pop() {
if (size == 0) {
throw new IndexOutOfBoundsException();
}
return elementData[--size];
}
//清空棧的操作
public void clear() {
doClear();
}
//清空棧的操作
private void doClear() {
size = 0;
ensureCapacity(DEFAULT_VALUE_SIZE);
}
//保證在進棧的,存儲棧元素的elementData數組有足夠的空間
private void ensureCapacity(int minCapacity) {
if (size > minCapacity) {
return;
}
E[] old = elementData;
elementData = (E[]) new Object[minCapacity];
for (int i = 0; i < size; i++) {
elementData[i] = old[i];
}
}
//返回棧的實際元素個數
public int size() {
return size;
}
}
上面的代碼實現了棧的基本操作,push/pop/clear。
棧的應用##
應用1 —— 平衡符號####
編譯器檢查程序的語法錯誤,但是常常由于缺少一個符號(如遺漏一個花括號)引起編譯不能通過。在這種情況下,一個有用的工具就是檢測是成對出現的東西。于是,一個右花括號、右方括號及右括號必然對其相應的左括號。比如[()]序列是合法的,[(])是不合法的。
通過棧,可以實現上述程序,思路如下:
做一個空棧,讀取字符直到末尾。如果字符是一個開放符號(左邊的符號),則將其推入棧內。如果字符是一個封閉符號(右邊的符號),則當空棧是報錯。否則,將棧元素彈出。如果彈出的開放符號不是當前封閉符號所對應的符號,則報錯。在字符串末尾,若棧非空則報錯。
代碼實現######
public class StackApplication {
// 申明一個棧,用來存放開放符號
Stack<Character> lefts = new Stack<Character>();
// 申明一個數組,用來存放封閉符號
ArrayList<Character> rights = new ArrayList<Character>();
// 用來存放從控制臺讀取的字符數組
char[] characters;
// 從控制臺讀取字符串,并轉換成字符串
private void readCharsFromConsole() {
Scanner scanner = new Scanner(System.in);
if (scanner.hasNext()) {
// 給characters數組
characters = scanner.next().toCharArray();
}
}
// 檢查開放符號和封閉符號是否匹配
public boolean check() {
readCharsFromConsole();
// 開放符號進棧lefts,封閉符號添加到數組rights
for (int i = 0; i < characters.length; i++) {
char ch = characters[i];
if (ch == '(') {
lefts.push(ch);
}
if (ch == ')') {
rights.add(ch);
}
}
// 如果開放符號的個數不等于封閉符號的個數則報錯
if (lefts.size() != rights.size()) {
return false;
}
// 遍歷封閉符號,如果棧lefts彈出的元素不是當前封閉元素所對應的,則返回false
for (int i = 0; i < rights.size(); i++) {
if (rights.get(i).charValue() == ')') {
if (lefts.pop().charValue() != '(') {
return false;
}
}
}
// 最后返回true
return true;
}
}
測試代碼######
public static void main(String[] args) {
StackApplication stackApplication = new StackApplication();
System.out.println(stackApplication.check());
}
測試結果######
在控制臺輸入((())),回車,結果如下圖所示,返回true。
在控制臺輸入((()),回車,結果如下圖所示,返回false。
應用二 —— 后綴表達式(逆波蘭表達式)####
假設我們需要一個計算器來計算我們購物的花費,你的計算公式可能是這樣的:2 * 2 + 3 * 3
,如果你手上的計算器的普通計算器的話,答案為21。但是現在大多數時候我們希望使用科學計算器,它可以判定運算的優先級,科學計算器的結果為13。
我們簡單地分析一下科學計算器計算上面例子的過程,首先判斷優先級,乘法有限,所以依次計算2*2和#3*3并將其結果分別存儲到兩個臨時變量A1和A2中,最后計算加法,將A1和A2求和。我們可以將這種操作寫為:2 2 * 3 3 * +
。這就是逆波蘭表達式。
計算細節如下:
首先假設有一個空棧stack,遍歷后綴表達式,將數字壓入棧中,直到遇到一個操作符,這個操作符的操作數為連續兩次出棧的結果。
- 第一步:stack=[2,2],讀到一個操作符為*,于是操作數連續兩次出棧的結果2和2。所以計算結果為2*2=4,將結果壓入棧中,stack=[4]。
- 第二步:繼續讀入數字,直到一個操作符。stack=[4,3,3],操作符為*,操作數為連續兩次出棧的結果3和3。所以計算結果為3*3=9,將結果壓入棧中,stack=[4,9]。
- 第三部:繼續讀入,下一個元素為+操作數,所以執行加法操作,操作數為連續兩次出棧的結果4和9,所以計算結果為4+9=13。壓棧,stack=13。到此遍歷完了整個后綴表達式,最后結果就為棧頂元素13。
代碼######
public class StackApplication2 {
// 字符數組,用來保存后綴表達式的每一個字符
private char[] postfix;
// 操作數棧
Stack<Integer> numberStack = new Stack<Integer>();
// 構造器,傳入一個后綴表達式字符串
public StackApplication2(String postfix) {
this.postfix = postfix.toCharArray();
}
public StackApplication2() {
}
// 判斷后綴表達式中字符是否為數字
private boolean isNumeric(char ch) {
return (ch >= '0' && ch <= '9') ? true : false;
}
// 科學計算器的實現方法
public int scientificCaculator() {
// 兩個操作數
int num1;
int num2;
// 遍歷后綴表達式
for (int i = 0; i < postfix.length; i++) {
char temp = postfix[i];
// 如果是數字就壓棧
if (isNumeric(temp)) {
numberStack.push(temp - '0');// char轉int
} else {
// 如果是操作符就從棧中彈出操作數并執行相關運算
num1 = numberStack.pop();
num2 = numberStack.pop();
if (temp == '+') {
numberStack.push(num1 + num2);
} else if (temp == '-') {
numberStack.push(num1 - num2);
} else if (temp == '*') {
numberStack.push(num1 * num2);
} else if (temp == '/') {
numberStack.push(num1 / num2);
}
}
}
// 返回最后的棧頂元素,即結果
return numberStack.pop();
}
}
測試代碼#######
public static void main(String[] args) {
StackApplication2 stackApplication2 = new StackApplication2("6523+8*+3+*");
System.out.println(stackApplication2.scientificCaculator());
}
結果######
計算一個后綴表示花費的時間是O(N),該算法是一個十分簡單的算法。注意,當一個表達式以后綴表示給出時,我們就不用知道運算的優先級,這是一個明顯的優點。
中綴到后綴的轉換####
棧不僅僅可以用來計算后綴表達式的值,還可以用來用來講一個標準形式的表達式(中綴表達式)轉換成后綴表達式。 我們假設只有運算+,*,(,),并且表達式合法以及使用普通的優先級法則,即括號>乘>加。
假設中綴表示1+2*3+(4*5+6)*7
,則轉換后的后綴表達式為:123*+45*6+7*+
。
轉換方法######
我們需要兩個數據結構,一個用來存放操作符的棧operatorStack,一個用來存放后綴表達式的字符數組postfix。遍歷中綴表達式,如果讀到的是一個操作數,則立即添加到postfix數組中,如果是一個操作符則相對麻煩。為了說明方便,我們將operatorStack棧中的棧頂操作符稱為top,當前遍歷的操作符為temp。
操作符的處理規則如下:
- 如果棧為空,則直接將temp壓入operatorStack棧中。
- 如果棧不為空并且temp= * 或 + 或 (,然后將temp的優先級和top比較,如果temp的優先級大于top優先級,則將temp壓棧;否則,依次將棧中優先級大于或等于temp的操作符彈出,添加到postfix,直到top優先級高于temp,然后將temp壓棧,但是有一個情況例外,即top=(,temp的優先級低于(時,這種情況,top不會出棧,而是將temp直接壓棧。
- 如果讀到的),則依次彈出操作符,加入postfix的末尾,直到(。
現在我們對剛才提到的例子1+2*3+(4*5+6)*7
采用上面的算法依次計算:
- 讀取到操作數1,直接添加到postfix數組中。此時,operatorStack為空,postfix=[1]。
- 讀取到操作符+,因為此時棧為空,所以直接壓棧。此時,operatorStack={+},postfix=[1]。
- 讀取到操作數2,直接添加到postfix數組中。此時,operatorStack={+},postfix=[1,2]。
- 讀取到操作符*,此時temp = *,top = +,優先級temp > top,所以temp壓棧。此時,operatorStack={+,*},postfix=[1,2]。
- 讀取到操作數3,直接添加到postfix數組中。此時,operatorStack={+,*},postfix=[1,2,3]。
- 讀取到操作符+,此時temp = +,top = *,優先級top>=temp,所以top出棧并添加到postfix數組。此時operatorStack={+},postfix=[1,2,3,*],top=+,top>=temp,所以top出棧并添加到postfix數組。此時operatorStack={},postfix=[1,2,3,*,+}。此時operatorStack為空,所以temp壓棧。所以此時operatorStack={+},postfix=[1,2,3,*,+}。
- 讀取操作符(,此時temp = (,top = +,優先級temp > top,所以temp直接壓棧。此時,operatorStack={+,(},postfix=[1,2,3,*,+]。
- 讀取操作數4,直接添加到postfix數組中。此時operatorStack={+,(},postfix=[1,2,3*,+,4]。
- 讀取操作符*,此時temp = *,top = (,雖然優先級top >= temp,但是對于(特殊處理,不出棧。所以temp直接壓棧。此時operatorStack={+,(,*},postfix=[1,2,3,*,+,4]。
- 讀取操作數5,直接添加到postfix數組中。此時operatorStack={+,(,*},postfix=[1,2,3,*,+,4,5]。
- 讀取操作符+,此時temp = +,top = *,優先級top >= temp,所以top出棧并且添加到postfix數組,此時operatorStack={+,(},postfix=[1,2,3,*,+,4,5,*],top = (,優先級temp < top,temp直接壓棧。此時operatorStack={+,(,+},postfix=[1,2,3,*,+,4,5,*]。
- 讀取操作數6,直接添加到postfix數組中。此時operatorStack={+,(,+},postfix=[1,2,3,*,+,4,5,*,6]。
- 讀取操作符),依次彈出棧頂操作符,直到(。此時operatorStack={+},postfix=[1,2,3,*,+,4,5,*,6,+]。
- 讀取操作符*,此時temp = *,top = +,優先級temp > top,所以temp壓棧。此時operatorStack={+,*},postfix=[1,2,3,*,+,4,5,*,6,+]。
- 讀取操作數7,直接添加到postfix數組中。此時operatorStack={+,*},postfix=[1,2,3,*,+,4,5,*,6,+,7]。
- 依次彈出棧中剩余操作符,最終operatorStack={},postfix=[1,2,3,*,+,4,5,*,6,+,7,*,+]。
代碼實現######
public class InfixToPostfix {
// 存放操作符的棧
private Stack<Character> operatorStack = new Stack<Character>();
// 存放后綴表達式的字符數組
private char[] postfix;
// 存放中綴表達式的字符數組
private char[] infix;
// 定義操作符
private static final char multiply = '*';
private static final char divide = '/';
private static final char add = '+';
private static final char substract = '-';
private static final char leftParenthesis = '(';
private static final char rightParenthesis = ')';
// 構造器
public InfixToPostfix(String infix) {
this.infix = infix.toCharArray();
postfix = new char[infix.length()];
}
public InfixToPostfix() {
}
// 判斷一個字符是否為數字
private boolean isNumeric(char ch) {
return (ch >= '0' && ch <= '9') ? true : false;
}
private String infixToPostfix() {
// 插入后綴的表達式下標
int insertIndex = 0;
// 操作符棧的大小
int size;
// 返回結果,后綴表達式字符串
String result;
for (int i = 0; i < infix.length; i++) {
if (isNumeric(infix[i])) {
postfix[insertIndex++] = infix[i];
} else {
char temp = infix[i];
size = operatorStack.size();
// 如果棧為空,就直接壓棧
if (size == 0) {
operatorStack.push(temp);
} else { // 棧不為空時
// 彈出棧頂元素
char top = operatorStack.pop();
if (temp == rightParenthesis) {
while (top != leftParenthesis) {
postfix[insertIndex++] = top;
top = operatorStack.pop();
}
} else {
// 如果當前操作符的優先級大于棧頂操作符的優先級,則當前操作符進棧
if (operatorPriorityCompare(temp, top)) {
/*
* 因為上一行為了比較當前操作符和棧頂元素的操作符,
* 上面彈出了棧頂元素,
* 所以這里要先將彈出的操作符壓棧
*/
operatorStack.push(top);
}
/*
* 如果當前操作符的優先級低于或等于棧頂操作符的優先級, 則棧頂操作符出棧,然后一直比較,
* 直到棧頂操作符優先級大于當前操作符或棧為空,然后當前操作符進棧。
*/
while (!operatorPriorityCompare(temp, top)) {
if (top == leftParenthesis) {
operatorStack.push(top);
break;
} else {
postfix[insertIndex++] = top;
size = operatorStack.size();
if (size == 0) {
break;
}
top = operatorStack.pop();
}
}
// 當前操作符進棧
operatorStack.push(temp);
}
}
}
}
// 遍歷完畢,一次彈出棧中剩余操作符
size = operatorStack.size();
if (size != 0) {
for (int i = 0; i < size; i++) {
postfix[insertIndex++] = operatorStack.pop();
}
}
result = String.valueOf(postfix).trim();
return result;
}
// 判斷操作符的優先級:括號>乘除>加減
// true: temp>top false: top >= temp
private boolean operatorPriorityCompare(char current, char top) {
if (((current == multiply || current == divide) && (top == substract || top == add))
|| (current == leftParenthesis
&& (top == multiply || top == divide || top == substract || top == add))) {
return true;
} else {
return false;
}
}
}
測試代碼######
public static void main(String[] args) {
InfixToPostfix infixToPostfix = new InfixToPostfix("1+2*3+(4*5+6)*7");
String result = infixToPostfix.infixToPostfix();
System.out.println(result);
}
測試結果######
與上面相同,這種轉換需要O(N)時間。可以通過制定減法和加法有相同優先級以及乘法和除法有相同優先級二將減法和乘法添加到指令中去。需要注意的是,表達式a-b-c應該轉化為ab-c-,而不是abc--。