什麼是棧(Stack)
下壓棧(FIFO queue),或者說棧(queue),是一種基於先進後出策略的集合模型。
使用場景
只要你留心,就會發現棧這種資料結構在生活中非常常見。
你在桌子上放了一摞檔案,放檔案和取檔案就是簡單的棧操作。
你開啟你的電子郵件賬戶,發現最新的郵件在最前面,如果這個時候有人給你發來新的郵件,你點選收信,發現新來的郵件又在你未讀郵件列表的最上面,這就是入棧;你從上到下依次點開郵件閱讀,這些唯未讀郵件也就是一一從你的未讀郵件列表移除了,這就是出棧操作。
你點開一個網頁,然後再點選網頁中的連結,這樣一直點選下去,直到你想回退到前面的網頁了,你開始點選回退按鈕,前面的網頁又一一出棧。同樣的,編輯器的回退功能,也是入棧出棧的例子。
Java實現
Stack
我們先定義棧的介面,一個完整的棧的介面,應該包含如下四個方法,即:
- 入棧
- 出棧
- 棧是否為空
- 棧中元素數量
下面是棧介面的定義:
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack;
public interface Stack<Item> {
/**
* add an item
*
* @param item
*/
void push(Item item);
/**
* remove the most recently added item
*
* @return
*/
Item pop();
/**
* is the stack empty?
*
* @return
*/
boolean isEmpty();
/**
* number of items in the stac
*/
int size();
}
複製程式碼
FixedCapacityStackOfStrings
首先我們實現一個最簡單的棧:定容棧,即容量固定的棧,棧的元素都為字串。
一個棧的實現需要有盛棧元素的地方,我們使用陣列。
還要有標記當前棧元素數量的變數N。
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.Consumer;
/**
* String定容棧:
* 固定容量的String型別棧
*/
public class FixedCapacityStackOfStrings {
private String[] a; // stack entries
private int N; // size
public FixedCapacityStackOfStrings(int cap) {
a = new String[cap];
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
public void push(String item) {
a[N++] = item;
}
public String pop() {
return a[--N];
}
}
複製程式碼
- 測試
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;
public class FixedCapacityStackOfStringsTests {
public static void main(String[] args){
FixedCapacityStackOfStrings fixedCapacityStackOfStrings=new FixedCapacityStackOfStrings(10);
System.out.println("fixedCapacityStackOfStrings : size="+fixedCapacityStackOfStrings.size()+",isEmpty="+fixedCapacityStackOfStrings.isEmpty());
fixedCapacityStackOfStrings.push("A");
fixedCapacityStackOfStrings.push("Aaha");
System.out.println("fixedCapacityStackOfStrings : size="+fixedCapacityStackOfStrings.size()+",isEmpty="+fixedCapacityStackOfStrings.isEmpty());
System.out.println("poped="+fixedCapacityStackOfStrings.pop());
System.out.println("fixedCapacityStackOfStrings : size="+fixedCapacityStackOfStrings.size()+",isEmpty="+fixedCapacityStackOfStrings.isEmpty());
}
}
複製程式碼
- 測試輸出
fixedCapacityStackOfStrings : size=0,isEmpty=true
fixedCapacityStackOfStrings : size=2,isEmpty=false
poped=Aaha
fixedCapacityStackOfStrings : size=1,isEmpty=false
複製程式碼
FixedCapacityStack
FixedCapacityStackOfStrings的缺點是隻能處理String物件,接著我們是使用泛型,讓我們的棧實現可以處理任意物件。
其中Item
就是我們泛型的型別引數。
由於歷史原因,Java的陣列一般情況下是不支援泛型的,因此我們用強轉的方式將Object型別的陣列轉為泛型中的陣列型別。
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;
/**
* 支援泛型的定容棧
* @param <Item>
*/
public class FixedCapacityStack<Item> implements Stack<Item> {
private Item[] a;
private int N;
public FixedCapacityStack(int cap){
a = (Item[]) new Object[cap];
}
@Override
public void push(Item item) {
a[N++] = item;
}
@Override
public Item pop() {
return a[--N];
}
@Override
public boolean isEmpty() {
return N == 0;
}
@Override
public int size() {
return N;
}
}
複製程式碼
- 測試
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;
public class FixedCapacityStackTests {
public static void main(String[] args){
FixedCapacityStack<Double> fixedCapacityStack=new FixedCapacityStack<>(10);
System.out.println("fixedCapacityStack : size="+fixedCapacityStack.size()+",isEmpty="+fixedCapacityStack.isEmpty());
fixedCapacityStack.push(new Double(10.01));
fixedCapacityStack.push(new Double(202.22));
System.out.println("fixedCapacityStack : size="+fixedCapacityStack.size()+",isEmpty="+fixedCapacityStack.isEmpty());
System.out.println("poped="+fixedCapacityStack.pop());
System.out.println("fixedCapacityStack : size="+fixedCapacityStack.size()+",isEmpty="+fixedCapacityStack.isEmpty());
}
}
複製程式碼
- 測試輸出
fixedCapacityStack : size=0,isEmpty=true
fixedCapacityStack : size=2,isEmpty=false
poped=202.22
fixedCapacityStack : size=1,isEmpty=false
複製程式碼
ResizingArrayStack
FixedCapacityStack的最大缺點就是容量固定,這就要求我們在使用棧之前必須估計棧的最大容量,很不方便。
下面我們就實現容量可變的棧。
我們用一個新的陣列來替換老的陣列,從而實現棧的容量擴充套件。這裡要注意如兩點:
- 當進行入棧操作的時候,如果棧滿,則將其容量增大一倍,保證接下來可以多次入棧。因為頻繁擴充套件容量也是很耗費記憶體的。
- 當進行出棧操作的時候,如果發現只用了棧容量的四分之一,則將棧的容量縮小一半。因為陣列如果空著不用,會白白耗費記憶體。
另外特別注意的是,出棧以後要將指定位置的元素賦值為null,以防止物件遊離。
Java的垃圾回收策略是回收所有無法被訪問的物件的記憶體,如果出棧以後,不將指定位置的元素賦值為null,那麼儲存這樣一個不需要的物件的引用,就稱為物件的遊離。
通過賦值已經出棧的位置為null,我們覆蓋了無效的引用,好讓GC回收這部分記憶體。
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;
/**
* 容量可變的棧
*
* @param <Item>
*/
public class ResizingArrayStack<Item> implements Stack<Item> {
private Item[] a = (Item[]) new Object[1];
private int N = 0;
/**
* 改變棧的容量大小
*
* @param max
*/
private void resize(int max) {
// Move stack to a new array of size max.
Item[] temp = (Item[]) new Object[max];
for (int i = 0; i < N; i++) {
temp[i] = a[i];
}
a = temp;
}
@Override
public void push(Item item) {
//如果棧滿,則將其容量增大一倍
if (N == a.length) {
resize(2 * a.length);
}
a[N++] = item;
}
@Override
public Item pop() {
Item item = a[--N];
// 防止物件遊離(loitering)
a[N] = null;
//如果棧中已用的容量只佔總容量的1/4,則將棧容量縮小一半
if (N > 0 && N == a.length / 4) {
resize(a.length / 2);
}
return item;
}
@Override
public boolean isEmpty() {
return N == 0;
}
@Override
public int size() {
return N;
}
}
複製程式碼
- 測試
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;
import java.math.BigDecimal;
public class ResizingArrayStackTests {
public static void main(String[] args){
ResizingArrayStack<BigDecimal> resizingArrayStack=new ResizingArrayStack<>();
System.out.println("resizingArrayStack : size="+resizingArrayStack.size()+",isEmpty="+resizingArrayStack.isEmpty());
resizingArrayStack.push(new BigDecimal(100.001));
resizingArrayStack.push(new BigDecimal(202.022));
System.out.println("resizingArrayStack : size="+resizingArrayStack.size()+",isEmpty="+resizingArrayStack.isEmpty());
System.out.println("poped="+resizingArrayStack.pop());
System.out.println("resizingArrayStack : size="+resizingArrayStack.size()+",isEmpty="+resizingArrayStack.isEmpty());
}
}
複製程式碼
- 測試輸出
resizingArrayStack : size=0,isEmpty=true
resizingArrayStack : size=2,isEmpty=false
poped=202.02199999999999135980033315718173980712890625
resizingArrayStack : size=1,isEmpty=false
複製程式碼
IterableResizingArrayStack
下面我們將為我們的棧實現增加迭代器的特性。
事實上,foreach
不僅僅是for
的簡寫形式語法糖這麼簡單,如下foreach
和while
迴圈是等效的:
for(String s:collection){
s ...
}
複製程式碼
while(collection.hasNext()){
collection.next();
...
}
複製程式碼
從上面例子可以看出,迭代器其實就是一個實現了hasNext()
和next()
方法的物件。
如果一個類可迭代,那麼第一步就要宣告實現Iterable介面。
然後我們通過一個內部類來實現Iterator的hasNext()
和next()
方法從而支援迭代操作。
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 可迭代的可變長的基於陣列儲存的棧實現
*
* @param <Item>
*/
public class IterableResizingArrayStack<Item> implements Stack<Item>, Iterable<Item> {
private Item[] a = (Item[]) new Object[1];
private int N = 0;
/**
* 改變棧的容量大小
*
* @param max
*/
private void resize(int max) {
// Move stack to a new array of size max.
Item[] temp = (Item[]) new Object[max];
for (int i = 0; i < N; i++) {
temp[i] = a[i];
}
a = temp;
}
@Override
public void push(Item item) {
//如果棧滿,則將其容量增大一倍
if (N == a.length) {
resize(2 * a.length);
}
a[N++] = item;
}
@Override
public Item pop() {
Item item = a[--N];
// 防止物件遊離(loitering)
a[N] = null;
//如果棧中已用的容量只佔總容量的1/4,則將棧容量縮小一半
if (N > 0 && N == a.length / 4) {
resize(a.length / 2);
}
return item;
}
@Override
public boolean isEmpty() {
return N == 0;
}
@Override
public int size() {
return N;
}
@Override
public Iterator<Item> iterator() {
return new ReverseArrayIterator();
}
//支援迭代方法,實現在內部類裡
private class ReverseArrayIterator implements Iterator<Item> {
// Support LIFO iteration.
private int i = N;
@Override
public boolean hasNext() {
return i > 0;
}
@Override
public Item next() {
if(i<=0){
throw new NoSuchElementException();
}
return a[--i];
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}
複製程式碼
- 測試
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;
import java.math.BigDecimal;
public class IterableResizingArrayStackTests {
public static void main(String[] args) {
IterableResizingArrayStack<Float> resizingArrayStack = new IterableResizingArrayStack<>();
System.out.println("resizingArrayStack : size=" + resizingArrayStack.size() + ",isEmpty=" + resizingArrayStack.isEmpty());
resizingArrayStack.push(new Float(100.001));
resizingArrayStack.push(new Float(202.022));
System.out.println("resizingArrayStack : size=" + resizingArrayStack.size() + ",isEmpty=" + resizingArrayStack.isEmpty());
System.out.println("resizingArrayStack all items:");
for (Float f:resizingArrayStack) {
System.out.println(f);
}
System.out.println("poped=" + resizingArrayStack.pop());
System.out.println("resizingArrayStack : size=" + resizingArrayStack.size() + ",isEmpty=" + resizingArrayStack.isEmpty());
}
}
複製程式碼
- 測試輸出
resizingArrayStack : size=0,isEmpty=true
resizingArrayStack : size=2,isEmpty=false
resizingArrayStack all items:
202.022
100.001
poped=202.022
resizingArrayStack : size=1,isEmpty=false
複製程式碼
應用示例
判斷括號是否為成對出現
要求一個字串中,如果有括號的話,所有括號,必須是成對出現的。
寫一個檢查器檢查指定字串是否符合上面的原則。
根據TDD(測試驅動開發)的開發方法,先把單元測試寫好:
package net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.evaluation;
import net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.checker.LegalParenthesesChecker;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* 括號必須成對出現,此程式基於棧結構,用於檢測常用括號是否為成對出現,並表示式輸出是否合法。
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class LegalParenthesesCheckerTests {
@Test
public void testChecker(){
Assert.assertFalse(LegalParenthesesChecker.check("1}"));
Assert.assertFalse(LegalParenthesesChecker.check("[1}"));
Assert.assertFalse(LegalParenthesesChecker.check("[1]}"));
Assert.assertFalse(LegalParenthesesChecker.check("(((1+1)+2)+3"));
Assert.assertFalse(LegalParenthesesChecker.check("<((1+1)+2)+3"));
Assert.assertTrue(LegalParenthesesChecker.check(""));
Assert.assertTrue(LegalParenthesesChecker.check(" "));
Assert.assertTrue(LegalParenthesesChecker.check("1"));
Assert.assertTrue(LegalParenthesesChecker.check("[]"));
Assert.assertTrue(LegalParenthesesChecker.check("[1]"));
Assert.assertTrue(LegalParenthesesChecker.check("{(『((<1+1>)+【2】)+』3)}"));
}
}
複製程式碼
下面開始寫實現。
package net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.checker;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl.IterableResizingArrayStack;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* 括號必須成對出現,此程式基於棧結構,用於檢測常用括號是否為成對出現,並表示式輸出是否合法。
*/
public class LegalParenthesesChecker {
private static final String BRACKET="<>()()〈〉‹›﹛﹜『』〖〗[]《》﹝﹞〔〕{}「」【】︵︶︷︸︿﹀︹︺︽︾﹁﹂﹃﹄︻︼";
public static boolean check(String expression) {
//如果傳入的表示式為空則不需要進行括號成對判斷
if (null == expression || expression.length() < 1) {
return true;
}
//將傳入的表示式拆分為char陣列
char[] expressionsChars = expression.toCharArray();
//將所有需要判斷的括號拆分為char陣列並進行sort,其中index為偶數的永遠是左半個括號,index為奇數的是右半個括號
char[] brackets = BRACKET.toCharArray();
//使用binarySearch之前,需要sort陣列
Arrays.sort(brackets);
//用於儲存棧容器和括號之間的對應關係
Map<Character, Stack> map = new HashMap<>();
//遍歷表示式的每個字元
for (char c : expressionsChars) {
//判斷該字元是否為括號
int index = Arrays.binarySearch(brackets, c);
//負數,不是括號,不需要處理
if (index < 0) {
continue;
}
//偶數,是左括號,則放入棧中
if (index % 2 == 0) {
//取出map中該左括號對應的棧容器
Stack<Character> stack = map.get(c);
//如果該左括號對應的key是第一次存入map,則建立一個棧容器
if (null == stack) {
stack = new IterableResizingArrayStack<>();
}
stack.push(c);
map.put(c, stack);
} else {
//奇數,是右括號,則先找到該右括號對應的左括號
char left = brackets[index - 1];
//取出左括號對應的棧容器中的值
Stack<Character> stack = map.get(left);
//如果該右括號沒有對應的左括號與之匹配,則表示此表示式中的括號不成對,不合法
if (null == stack || stack.size() < 1) {
return false;
} else {
stack.pop();
continue;
}
}
}
//如果map中還有左側括號,表示左側括號比右側多,則返回false
for (char k : map.keySet()) {
if(!map.get(k).isEmpty()){
return false;
}
}
return true;
}
}
複製程式碼
經過單元測試驗證,該檢查器滿足要求。
雙棧算術表示式求值演算法
Dijkstra的雙棧算術表示式求值演算法(Dijkstra's two-stack algorithm for expression evaluation)是由E.W.Dijkstra在上個世紀60年代發明的一個很簡單的演算法,用兩個棧:一個用來儲存運算子、一個用來儲存運算元,來完成對一個表示式的運算。
其實整個演算法思路很簡單:
- 無視左括號
- 將運算元壓入運算元棧
- 將運算子壓入運算子棧
- 在遇到右括號的時候,從運算子棧中彈出一個運算子,再從運算元棧中彈出所需的運算元,並且將運算結果壓入運算元棧中
package net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.evaluation;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl.IterableResizingArrayStack;
public class DijkstrasTwoStackAlgorithmForExpressionEvaluation {
public Double cal(String expression) {
String[] expressionArr = expression.split(" ");
Stack<String> ops = new IterableResizingArrayStack<String>();
Stack<Double> vals = new IterableResizingArrayStack<Double>();
for (String s : expressionArr) {
// Read token, push if operator.
if (s.equals("(")) {
;
} else if (s.equals("+")) {
ops.push(s);
} else if (s.equals("-")) {
ops.push(s);
} else if (s.equals("*")) {
ops.push(s);
} else if (s.equals("/")) {
ops.push(s);
} else if (s.equals("sqrt")) {
ops.push(s);
} else if (s.equals(")")) {
// Pop, evaluate, and push result if token is ")"
String op = ops.pop();
double v = vals.pop();
if (op.equals("+")) {
v = vals.pop() + v;
} else if (op.equals("-")) {
v = vals.pop() - v;
} else if (op.equals("*")) {
v = vals.pop() * v;
} else if (op.equals("/")) {
v = vals.pop() / v;
} else if (op.equals("sqrt")) {
v = Math.sqrt(v);
}
vals.push(v);
}
// Token not operator or paren: push double value.
else {
vals.push(Double.parseDouble(s));
}
}
return vals.pop();
}
}
複製程式碼
- 測試
package net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.evaluation;
public class DijkstrasTwoStackAlgorithmForExpressionEvaluationTests {
public static void main(String[] args){
DijkstrasTwoStackAlgorithmForExpressionEvaluation expressionEvaluation=new DijkstrasTwoStackAlgorithmForExpressionEvaluation();
System.out.println(expressionEvaluation.cal("( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )"));
}
}
複製程式碼
- 測試輸出:
101.0
複製程式碼