《演算法》- 佇列和棧

DaviZhong發表於2020-06-06

1、佇列: 先進先出(FIFO),例如超市的收銀臺、排隊買票的顧客。在Java中,它和List的區別在於,List可以在任意位置新增和刪除元素,而Queue只有兩個操作:

  • 把元素新增到佇列末尾;
  • 從佇列頭部取出元素。

2、棧: 下壓棧,後進先出(LIFO),例如你辦公桌上的一疊信件,新信件來時將它們放在最上面(push方法),當閱讀時從上到下取件(pop方法)。

3、雙棧算術表示式求值:
例如計算(1+((2+3)*(4*5)))的值:用兩個棧,一個儲存運算子(運算子棧),一個儲存數字(運算元棧)。

從左到右逐個將實體送入棧處理:
1、遇到數字時,將數字壓入運算元棧,遇到運演算法時,壓入運算子棧;
2、遇到左括號時忽略;
3、遇到右括號,彈出一個運算子,彈出所需數量的數字,並將運算子和數字的運算結果壓入運算元棧。

演算法演示

雙棧算術表示式求值演算法(為了程式碼簡潔未考慮異常):

import java.util.Stack;

public class EvaluateTest {
    public static void main(String[] args) {
        // 需要計算的表示式
        String str = "(1+((2+3)*(4*5)))".trim();

        // 運算子棧和運算元棧
        Stack<String> ops = new Stack<>();
        Stack<Double> vals = new Stack<>();

        for(int i=0;i < str.length();i++) {
            String s = String.valueOf(str.charAt(i));
            if(s.equals("(")){
                // 左括號時忽略
            }else if(s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/") || s.equals("sqrt")){
                // 運算子時壓運算子棧
                ops.push(s);
            }else if(s.equals(")")){
                // 右括號時,將兩個棧都pop,再計算、壓棧
                String op = ops.pop();
                Double v = vals.pop();
                if(op.equals("+")){
                    v = vals.pop() + v;
                }else if(op.equals("-")){
                    v = vals.pop() - v;
                }if(op.equals("*")){
                    v = vals.pop() * v;
                }if(op.equals("/")){
                    v = vals.pop() / v;
                }if(op.equals("sqrt")){
                    v = Math.sqrt(v);
                }
                vals.push(v);
            }else{
                // 最後是數字時,轉為double壓入運算元棧
                vals.push(Double.valueOf(s));
            }
        }
        System.out.println("最終運算結果:" + vals.pop());
    }
}

4、連結串列
連結串列是一種遞迴的資料結構,它可以為空(null),可以是指向一個節點(node)的引用。該節點包含一個元素(資料域,儲存節點含有的資訊)和一個指向另一條連結串列或節點的引用(引用域)。

連結串列的特點:

  • 插入和刪除元素方便;
  • 查詢資料時效率低,訪問某個位置的資料要從第一個節點開始訪問,根據第一個節點儲存的下一個節點的地址找到第二個節點,以此類推;

可以看完下面的流程再來理解它的特點。這裡主要介紹單向連結串列:

// 一個節點
public class Node {
    public Object item;
    public Node next;
}

虛擬碼構造一條連結串列:

建立好連結串列後,在表頭插入一個節點很容易,如下圖,如果在表頭插入字串"not",先將first儲存在oldfirst中,然後將一個新節點賦給first,並將它的item元素設為not,next設為oldfirst。

在表頭刪除一個節點,將first指向first.next即可。曾經的第一個節點物件變成了一個孤兒,Java的記憶體管理最終將回收它所佔用的記憶體:

請注意:當連結串列中只有一個節點時,它既是首節點又是尾節點,另外注意連結串列為空的情況。

那麼在表尾插入節點可以表示為:

以前連結串列操作只需要幾行賦值程式碼,所需時間和連結串列的長度無關。但如果需要刪除表尾節點,就要遍歷整條連結串列並找出指向last節點的節點,這樣所需的時間和連結串列的長度成正比。
要想實現任意插入和刪除操作,可以使用雙向連結串列,這裡不作介紹。

5、遍歷連結串列 我們知道遍歷陣列可以用for(int i = 0; i < N; i++){...};那麼遍歷連結串列也有一個對應方式:

for (Node x = first; x != null; x = x.next) {
   // 處理 x.item
}

6、堆疊的連結串列實現
直接上程式碼:

import java.util.Iterator;
import java.util.NoSuchElementException;

public class Stack<Item> implements Iterable<Item> {
    private Node<Item> first;     // 棧頂(最近新增的元素)
    private int n;                // 元素數量

    private static class Node<Item> {
        private Item item;
        private Node<Item> next;// 定義了節點的巢狀類
    }

    /**
     * Initializes an empty stack.
     */
    public Stack() {
        first = null;
        n = 0;
    }

    /**
     * 當first==null或者n==0時,棧是空的
     */
    public boolean isEmpty() {
        return first == null;
    }
    
    public int size() {
        return n;
    }

    /**
     * 向棧頂新增元素
     */
    public void push(Item item) {
        Node<Item> oldfirst = first;
        first = new Node<Item>();
        first.item = item;
        first.next = oldfirst;
        n++;
    }

    /**
     * 從棧頂刪除元素
     */
    public Item pop() {
        if (isEmpty()) throw new NoSuchElementException("Stack underflow");
        Item item = first.item;
        first = first.next;
        n--;
        return item;
    }


    /**
     * 只取值,不刪除
     */
    public Item peek() {
        if (isEmpty()) throw new NoSuchElementException("Stack underflow");
        return first.item;
    }

    /**
     * 按照LIFO的順序,返回一個迭代器可以迭代此類
     */
    public Iterator<Item> iterator() {
        return new LinkedIterator(first);
    }
    
    private class LinkedIterator implements Iterator<Item> {
        private Node<Item> current;

        public LinkedIterator(Node<Item> first) {
            current = first;
        }
        public boolean hasNext() {
            return current != null;
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
        public Item next() {
            if (!hasNext()) throw new NoSuchElementException();
            Item item = current.item;
            current = current.next;
            return item;
        }
    }
}

7、佇列的連結串列實現
Queue的實現使用的資料結構和Stack類相同,都是連結串列,但它實現了不同的新增和刪除演算法,這也是FIFO和LIFO的區別所在。

import java.util.Iterator;
import java.util.NoSuchElementException;

public class Queue<Item> implements Iterable<Item> {
    private Node<Item> first;    // 指向最早新增的節點的引用
    private Node<Item> last;     // 隊尾,最近新增
    private int n;

    private static class Node<Item> {
        private Item item;
        private Node<Item> next;
    }

    public Queue() {
        first = null;
        last  = null;
        n = 0;
    }

    public boolean isEmpty() {
        return first == null;
    }

    public int size() {
        return n;
    }

    /**
     * 向表尾新增元素
     */
    public void enqueue(Item item) {
        Node<Item> oldlast = last;
        last = new Node<Item>();
        last.item = item;
        last.next = null;
        if (isEmpty()){
            first = last;
        }else{
            oldlast.next = last;
        }
        n++;
    }

    /**
     * 從表頭刪除元素
     */
    public Item dequeue() {
        if (isEmpty()) throw new NoSuchElementException("Queue underflow");
        Item item = first.item;
        first = first.next;
        n--;
        if (isEmpty()) last = null;
        return item;
    }

    /**
     * 從表頭獲取元素,不刪除
     */
    public Item peek() {
        if (isEmpty()) throw new NoSuchElementException("Queue underflow");
        return first.item;
    }

    public Iterator<Item> iterator()  {
        return new LinkedIterator(first);
    }

    private class LinkedIterator implements Iterator<Item> {
        private Node<Item> current;

        public LinkedIterator(Node<Item> first) {
            current = first;
        }
        public boolean hasNext()  { return current != null;                     }
        public void remove()      { throw new UnsupportedOperationException();  }

        public Item next() {
            if (!hasNext()) throw new NoSuchElementException();
            Item item = current.item;
            current = current.next;
            return item;
        }
    }
}

相關文獻:
Bags, Queues, and Stacks

相關文章