棧
- 棧(Stack)是一種限制插入和刪除只能在一個位置上進行的表,改位置是表的末端,叫做棧頂top。棧的基本操作有push (進棧)pop(出棧)
- 棧又叫做LIFO(後進先出)表,下圖展示普通push輸入,而pop和top是輸出操作。普通的清空棧,判斷是否空棧操作都是基本操作指令,但是我們對棧的具體修改操作基本也就是push和pop。下圖模型中表示若干操作後,一個抽象的棧只有棧頂元素是可見的,其他不可見。
棧的實現
- 由於棧是一個表,因此任何實現表的方法都能夠實現棧,顯然,ArrayList, LinkedList都支援這些操作,99%的時間他們都是最合理的選擇,偶爾設計一種特殊目的的實現可能會更快。因為棧操作是常數時間操作,所以除非在非常特殊的環境下,一般都是這兩種方式,這兩種一種是鏈式結構,一種是陣列結構,二者都簡化了在ArrayList和LinkedList中的邏輯,
棧的連結串列實現
- 棧的第一種實現是單連結串列,通過在表的頂端插入實現push,通過刪除表頂端實現pop。top操作只是查詢表頂端元素並返回他的值。
棧的陣列實現
- 另一種實現方法避免了鏈而且可能是最流行的解決方法。模擬ArrayList的Add操作,因此現有的實現方法非常簡單,與每個棧相關聯的參是push,pop,都是對陣列末尾的資料進行操作,對空棧他是-1。
- 將元素X推入棧,top 增加1,並且array[last]=X
- 將元素彈出棧,返回array[top],並且top減1
- 以上測操作都是以常數時間執行,非常快速的只有一個操作,以下我們用陣列實現自己的一個棧:
public class MyStack<E> {
private Object[] elementData;
private Integer top;
private Integer capacity;
private Integer position;
public MyStack() {
elementData = new Object[10];
top = -1;
capacity = elementData.length - 1;
position = 0;
}
public void reSize() {
if (position == capacity) {
Integer newCapacity = capacity + (capacity >> 1);
elementData = Arrays.copyOf(elementData, newCapacity);
capacity = newCapacity;
}
}
public Object push(E e) {
reSize();
top = position;
elementData[position++] = e;
return e;
}
public E pop(){
if(top == -1){
return null;
}
E e = (E)elementData[top--];
position --;
return e;
}
public E getTop(){
return (E)elementData[top];
}
public boolean isEmpty(){
return top == -1;
}
public int size(){
return position;
}
public void printMyStack(){
Integer temp = top;
while (top != -1){
System.out.print(pop());
}
top = temp;
}
public static void main(String[] args) {
MyStack<Integer> myStack = new MyStack<Integer>();
for (int i = 0; i < 100; i++) {
myStack.push(i);
}
Integer result = 0;
while (result != null){
result = myStack.pop();
System.out.println(result);
}
}
}
應用
- 我們以上示例中將操作限制在一個表上進行,你們這些操作會執行很快,然而這樣操作是非常重要的,維護了棧中的資料關係。在棧的應用中給出了三個例子,實現了其中一個作為參考。
平衡符號
- 編譯器檢查程式的語法錯誤,經常會因為缺少一個符號,比如花括號,引起編譯器上百行的錯誤提示,而真正的錯誤並沒有找出來。
- 這種情況,有一個有用的工具就是檢驗是否每一件事情都成對出現。
- 每一個右括號,右方括號,右花括號都比如對應左邊括號。序列{()} 是合法的,但是{(]}是非法的。
- 事實上檢測這些事情是很容易的,為簡單起見,我們直接利用棧就可以做一個簡單實現,流程如下:
- 做一個空棧,讀入字元,直到檔案結尾。
- 如果字元是一個開放括號,推入棧
- 如果字元是一個封閉括號,則當棧空時候,報錯,否則將棧元素彈出。
- 如果彈出的符合不是對應的開放符號的括號,報錯
- 檔案結尾,如果棧非空,報錯
字尾表示式
- 假設我們有一個便攜計算器用於計算,為此,將一系列資料進行±*/之後得到結果,如果購物單價是4.99,5.99,6.99,數量是1,1,2,那麼輸入這些資料的如下:
4.99 + 5.99 + 6.99 * 2
- 普通計算器並不能識別先加減後乘除,得到的結果是:35.94,但是我們期望得到的結果是 24.96
- 只有科學計算器才支援複雜計算,科學計算器,實現中,將6.99*2 結果儲存A1,將5.99與A1 相加得到A2,再將4.99與A2 相加得到組中的結果,我們將這種操作順序書寫如下
4.99 5.99 + 6.99 2 * +
- 以上操作符記錄方式叫做字尾(postfix)或者逆波蘭(reverse polish)表示式,求值過程恰好就是上面描述的過程,計算這個問題最容易的方法就是使用棧。當看到一個數時候,將他推入棧,遇到運算子就將棧中頭兩個資料彈出,計算,接入推入棧中。例如如下字尾表示式計算過程動圖如下:
- 表示式:1 2+3-4 5*+
- 如上動圖展示全流程,計算一個字尾表示式花費的世界是O(N),因為對於輸入中的每個元素的處理都是有一些棧操作組成從而花費常數的時間。演算法簡單。注意:當一個表示式以字尾的方式給出的時候,沒有必要知道任何的優先規則,因為在中序,轉字尾表示式的時候已經考慮過,這個是一個明顯的優點。
中綴表示式轉字尾表示式
- 上面講到字尾表示式的計算,那我們如何將中綴表示式轉字尾,也是可以用棧來完成這個操作,我們用簡單的±*/(,這幾個操作符,我們用如下案例來說明:
a+b*c+(d*e+f)*g
1 2 3 * 4 5 * 6 + 7 * + +
演算法分析
- 操作符優先順序,‘)’右括號 > */ 乘除 > ±加減 > ‘(’ 左括號
- 依次讀取運算元,將數字推入number 棧,將操作符推入action棧
- 右括號‘)’,依次將action中操作符彈出,直到遇見左括號‘(’
- 如果見到任何其他符號(+,-,*,/,(),如果當前棧頂元素X優先順序高於當前元素Y,那麼彈出棧頂元素X,將X加入number棧,並將當前元素Y加入到action棧
- 如果讀到末尾,我們需要將action棧元素依次彈出加入到number棧
動圖展示
- 與前面的計算相同,這種轉換同樣也只要O(N)時間,並經過一次輸入後就完成工作。以下程式碼通過自己實現的棧,實現中綴表示式轉字尾表示式,並且計算字尾表示式得到對應結果
public class Evaluate {
public static MyStack<String> middlePreTOAfterPre(String mathStr) {
MyStack<String> number = new MyStack<>();
MyStack<String> action = new MyStack<>();
String[] chars = mathStr.split(" ");
for (int i = 0; i < chars.length; i++) {
String s = String.valueOf(chars[i]);
switch (flag(s, action)) {
case 1:
number.push(s);
break;
case 2:
action.push(s);
break;
case 3:
action.push(s);
break;
case 4:
number.push(action.pop());
action.push(s);
break;
case 5:
action.push(s);
break;
case 6:
while (!action.isEmpty()) {
String temp = action.pop();
if (temp.matches("\\(")) {
break;
} else {
number.push(temp);
}
}
break;
default:
break;
}
}
while (!action.isEmpty()) {
number.push(action.pop());
}
MyStack<String> temp = new MyStack<>();
while (!number.isEmpty()) {
temp.push(number.pop());
}
return temp;
}
public static int flag(String s, MyStack<String> action) {
if (s.matches("([1-9]\\d*\\.?\\d*)|(0\\.\\d*[1-9])")) {
return 1;
}
if (s.matches("(\\*)|(\\/)|(\\+)|(\\-)")) {
if (action.isEmpty()) {
return 2;
} else if (prior(s, action.getTop())) {
return 3;
} else {
return 4;
}
}
if (s.matches("\\(")) {
return 5;
}
if (s.matches("\\)")) {
return 6;
}
return 0;
}
public static boolean prior(String s, String top) {
if (top.matches("\\(")) {
return true;
}
if (s.matches("(\\*)|(\\/)") && top.matches("(\\+)|(\\-)")) {
return true;
}
return false;
}
public static Double evalutePostfix(MyStack<String> myStack){
MyStack<String> newStack = new MyStack<>();
while (!myStack.isEmpty()){
String temp = myStack.pop();
if(temp.matches("\\d+")){
newStack.push(temp);
}else if(temp.matches("(\\*)|(\\/)|(\\+)|(\\-)")){
if(newStack.size() < 2){
break;
}
double a = Double.valueOf(newStack.pop());
double b = Double.valueOf(newStack.pop());
switch (temp){
case "+":
newStack.push(String.valueOf(b+a));
break;
case "-":
newStack.push(String.valueOf(b-a));
break;
case "*":
newStack.push(String.valueOf(b*a));
break;
case "/":
newStack.push(String.valueOf(b/a));
break;
default:
break;
}
}
}
return Double.valueOf(newStack.pop());
}
public static void main(String[] args) {
MyStack<String> middlerStack = middlePreTOAfterPre("67 - 8 + 9 * ( 1 + 4 - 5 / 3 ) + ( 8 - 4 * 2 ) + 8 - 21");
middlerStack.printMyStack();
System.out.println();
System.out.println(evalutePostfix(middlerStack));
}
}
- 注意:以上帶有減法,除法, 在具體計算時候,應該是 (第二個彈出值- 第一個彈出值),除法也是類似,只有這個順序區別,其他的和以上描述一致。
- 其實中綴轉字尾唯一的一個重要規則就是:高優先順序操作符入棧,需要保證低下的操作符優先順序比我更低或相等,否則就彈出頂部操作符,也就是演算法分析中的第四點:
- 如果見到任何其他符號(+,-,*,/,(),如果當前棧頂元素X優先順序高於當前元素Y,那麼彈出棧頂元素X,將X加入number棧,並將當前元素Y加入到action棧
應用三
上一篇:資料結構與演算法–排序演算法總結(動圖演示)