1. 程式人生 > >資料結構與演算法(六)-揹包、棧和佇列

資料結構與演算法(六)-揹包、棧和佇列

  前言:許多基礎資料型別都和物件的集合有關。具體來說,資料型別的值就是一組物件的集合,所有操作都是關於新增、刪除或是訪問集合中的物件。而且有很多高階資料結構都是以這樣的結構為基石創造出來的,在本文中,我們將瞭解學習三種這樣的資料型別,分別是揹包(Bag)、棧(Stack)和佇列(Queue)

一、學習感悟

  對於資料結構的學習可以用以下步驟來學習:
  • 首先先對該結構的場景操作進行API化;
  • 然後再對資料型別的值的所有可能的表示方法以及各種操作的實現;
  • 總結特點和比較;
  接下來就對這三種資料型別進行介紹。

二、API

  這三種資料型別都是依賴於之前介紹過的線性表的鏈式儲存結構的,所以理解並掌握鏈式結構是學習各種演算法和資料結構的第一步,若還不是很清楚,可以看一下前面關於線性表的鏈式儲存結構的文章(本文主要是對鏈式儲存結構的進行介紹,如想要對順序儲存結構瞭解的話,可根據其特性和API進行編寫程式碼,歡迎在評論區留言討論)。

2.1、揹包(Bag)

  揹包是一種不支援從中刪除元素的集合資料型別——它的目的就是幫助用例收集元素並迭代遍歷所有收集到的元素(用例也可以檢查揹包是否為空或者獲取揹包中元素的數量)。   要理解揹包的概念,可以想象一個喜歡收集彈珠球的人。他將所有的彈珠球都放在一個揹包裡,一次一個,並且會不時在所有的彈珠球中尋找某一顆;   

2.1.1 揹包API

  根據以上的需求,可以寫出揹包的API:
public class Bag<Item> implements Iterable<Item>
  Bag()                    建立一個空揹包
  void add(Item item) 新增一個元素   boolean isEmpty() 揹包是否為空   int size() 揹包中的元素數量
  使用Bag的API,用例可以將元素新增進揹包並根據需要隨時使用foreach語句訪問所有的元素。用例也可以使用棧或是佇列,但是用Bag可以說明元素的處理順序不重要,比如在計算一堆Double值的平均值時,無需關注揹包元素相加的順序,只需要在得到所有值的和後除以Bag中元素的數量即可。

2.1.2 揹包實現

  根據2.1.1的API寫出具體的實現,其中關鍵方法add使用了頭插法:
public class Bag<T> implements Iterable<T> {

    private Node<T> first;

    private Integer size;

    Bag() {
        first = new Node<>();
        first.next = null;
        size = 0;
    }

    //由於Bag型別不需要考慮元素的相對順序,所以這裡我們可以使用頭插法來進行插入,提高效率
    public void add(T t) {
        Node<T> newNode = new Node<>();
        newNode.t = t;
        newNode.next = first.next;
        first.next = newNode;
        size++;
    }

    public Boolean isEmpty() {
        return size < 1;
    }

    public Integer size() {
        return size;
    }

    class Node<T> {

        T t;

        Node<T> next;

    }

    @Override
    public Iterator<T> iterator() {
        return new ListIterator();
    }

    class ListIterator implements Iterator<T> {

        private Node<T> current = first.next;

        @Override
        public boolean hasNext() {
            return current!=null;
        }

        @Override
        public T next() {
            T t = current.t;
            current = current.next;
            return t;
        }
    }

    public static void main(String[] args) {
        Bag<Integer> bag = new Bag<>();
        for (int i = 1; i <= 100; i++) {
            bag.add(i);
        }
        double sum = 0;
        Iterator<Integer> iterator = bag.iterator();
        while (iterator.hasNext()) {
            sum = sum + iterator.next();
        }
        System.out.println("和:"+sum);
        double size = bag.size();
        String format = new DecimalFormat("0.00").format(sum / size);
        System.out.println("平均值:"+format);
    }
}
Bag.java

  核心程式碼為add(),使用了頭插法::

    //由於Bag型別不需要考慮元素的相對順序,所以這裡我們可以使用頭插法來進行插入,提高效率
    public void add(T t) {
        Node<T> newNode = new Node<>();
        newNode.t = t;
        newNode.next = first.next;
        first.next = newNode;
        size++;
    }

2.1.3 總結

  上面就是關於Bag資料型別的實現,從中可以看出Bag是一種不支援刪除元素的、無序的、專注於取和存的集合型別。

2.2、棧(Stack)

  下壓棧(或簡稱棧)是一種基於後進先出(LIFO)策略的集合型別。比如在桌子上對方一疊書,我們拿書時,一般都是從最上面開始取的,這樣的操作就類似棧。      棧管理資料的兩種操作如下:
  • 寫入資料(堆積)操作稱作入棧(PUSH);
  • 讀取資料操作稱作出棧(POP);
  棧型別的模型結構在生活中的應用也不少,比如瀏覽器的回退功能,在一個瀏覽器tag頁上開啟的網頁,通過回退功能可以一次回退到歷史最近的瀏覽記錄。還有電腦軟體撤銷功能,也是這樣的策略模型。   棧是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一段被稱為棧頂,相對的,把另一端稱為棧底。想一個棧插入新元素又稱作進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之稱為新的棧頂元素;從一個棧刪除元素又稱作出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成為新的棧頂元素。   另外,像棧這樣,最後寫入的資料被最先讀取的資料管理方式被稱作LIFO(last in,first out),或者FILO(first in,last out)

2.2.1 棧API

  根據對以上理解寫出揹包的API:
public class Stack<Item> implements Iterable<Item>
  Stack()                     建立一個空棧
  void push(Item item)        新增一個元素
  Item pop()                  刪除最近新增的元素
  boolean isEmpty()           棧是否為空
  int size()                  棧中的元素數量

2.2.2 棧實現

  根據上面的棧API實現其方法,還是使用頭插法來實現:
public class Stack<T> implements Iterable<T> {

    private Node<T> head;

    private Integer size;

    Stack() {
        head = new Node<>();
        head.next = null;
        size = 0;
    }

    //頭插法
    public void push(T t) {
        Node<T> first = head.next;
        head.next = new Node<>();
        head.next.t = t;
        head.next.next = first;
        size++;
    }

    //取的時候從最上面開始取,也就是最近插入的元素
    public T pop() {
        Node<T> first = head.next;
        head.next = first.next;
        size--;
        return first.t;
    }

    public Boolean isEmpty() {
        return size < 1;
    }

    public Integer size() {
        return size;
    }

    class Node<T> {
        T t;
        Node<T> next;
    }

    @Override
    public Iterator<T> iterator() {
        return new ListIterator<T>();
    }

    class ListIterator<T> implements Iterator<T> {

        private Node<T> current = (Node<T>) head.next;

        @Override
        public boolean hasNext() {
            return current!=null;
        }

        @Override
        public T next() {
            T t = current.t;
            current = current.next;
            return t;
        }
    }

    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < 10; i++) {
            stack.push(i);
            System.out.println("push   -->   "+i);
        }
        Iterator<Integer> iterator = stack.iterator();
        while (iterator.hasNext()) {
            System.out.println("pop   -->   "+iterator.next());
        }
    }
}
Stack.java   核心方法為push()和pop():
    //頭插法
    public void push(T t) {
        Node<T> first = head.next;
        head.next = new Node<>();
        head.next.t = t;
        head.next.next = first;
        size++;
    }

    //取的時候從最上面開始取,也就是最近插入的元素
    public T pop() {
        Node<T> first = head.next;
        head.next = first.next;
        size--;
        return first.t;
    }
  執行結果:

2.2.3 總結

  它可以處理任意型別的資料,所需的空間總是和集合的大小成正比,操作所需的時間總是和集合的大小無關。
  • 先進後出;
  • 具有記憶功能,棧的特點是先進棧的後出棧,後進棧的先出棧,所以你對一個棧進行出棧操作,出來的元素肯定是你最後存入棧中的元素,所以棧有記憶功能;
  • 對棧的插入與刪除操作中,不需要改變棧底指標;
  • 棧可以使用順序儲存也可以使用鏈式儲存,棧也是線性表,因此線性表的儲存結構對棧也適用線性表可以鏈式儲存;

2.3、佇列(Queue)

  先進先出佇列(或簡稱佇列)是一種基於先進先出(FIFO)策略的集合型別。在生活中這種模型結構的示例有很多,比如說排隊上公交、排隊買火車票、排隊過安檢等都是先進先出的策略模型。      佇列是一種特殊的線性表,特殊之處在於它只允許在表的前端進行刪除操作,而在表的後端進行插入操作,和棧一樣,佇列是一種操作受限制的線性表進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。   像排隊一樣,一定是從最先的資料開始序按順處理資料的資料結構,就成為“佇列”,而像這類模型策略,被稱為FIFO(first in,first out)或者LILO(last in,last out)。   佇列在通訊時的電文傳送和接收中得到了應用。把接收到的電文一個一個放到了佇列中,在時間寬裕的時候再取出和處理。   當用例使用foreach語句迭代訪問佇列中的元素時,元素的處理順序就是他們被新增到佇列中的順序,而在程式中使用它的原因是在用集合儲存元素的同時儲存它們的相對順序:使它們入列順序和出列順序相同。

2.3.1 佇列API

  綜上所述,佇列的API為:
 public class Queue<Item> implements Iterable<Item>
  Queue()                     建立一個空佇列
  void enqueue(Item item)     新增一個元素
  Item dequeue()              刪除最近新增的元素
  boolean isEmpty()           佇列是否為空
  int size()                  佇列中的元素數量

2.3.2 佇列實現

  根據2.3.1的API編寫佇列的實現:
public class Queue<T> implements Iterable<T> {

    private Node<T> head;

    private Node<T> tail;

    private Integer size;


    Queue() {
        head = new Node<>();
        tail = null;
        head.next = tail;
        tail = head;
        size = 0;
    }

    //從佇列的尾部插入資料
    public void enqueue(T t) {
        Node<T> oldNode = tail;
        tail = new Node<>();
        tail.t = t;
        tail.next = null;
        if (isEmpty())
            head.next = tail;
        else
            oldNode.next = tail;
        size++;
    }

    //從佇列的頭部取資料
    public T dequeue() {
        Node<T> first = head.next;
        head.next = first.next;
        return first.t;
    }

    public Boolean isEmpty() {
        return size < 1;
    }

    public Integer size() {
        return size;
    }

    class Node<T> {
        T t;
        Node<T> next;
    }

    @Override
    public Iterator<T> iterator() {
        return new ListIterator();
    }

    class ListIterator implements Iterator<T> {

        private Node<T> current = head.next;

        @Override
        public boolean hasNext() {
            return current!=null;
        }

        @Override
        public T next() {
            T t = current.t;
            current = current.next;
            return t;
        }
    }

    public static void main(String[] args) {
        Queue<Integer> queue = new Queue<>();
        for (int i = 0; i < 10; i++) {
            queue.enqueue(i);
            System.out.println("enqueue   -->   "+i);
        }
        Iterator<Integer> iterator = queue.iterator();
        while (iterator.hasNext()) {
            System.out.println("dequeue   -->   "+iterator.next());
        }
    }
}
Queue.java   核心方法為enqueue()和dequeue():
    //從佇列的尾部插入資料
    public void enqueue(T t) {
        Node<T> oldNode = tail;
        tail = new Node<>();
        tail.t = t;
        tail.next = null;
        if (isEmpty())
            head.next = tail;
        else
            oldNode.next = tail;
        size++;
    }

    //從佇列的頭部取資料
    public T dequeue() {
        Node<T> first = head.next;
        head.next = first.next;
        return first.t;
    }
  執行結果:

2.3.3 總結

佇列做的事情有很多,包括我們常用的一些MQ工具,也是有佇列的影子。
  • 先進先出;
  • 特殊的線性結構;
  • 關注於元素的順序,公平性;

三、揹包、棧和佇列的比較

  揹包:不關注元素的順序,不支援刪除操作的集合型別;   棧:先進後出,具有記憶性,多應用於需要記憶功能的業務;   佇列:先進先出,可以應用於緩衝;

 本系列參考書籍:

  《寫給大家看的演算法書》

  《圖靈程式設計叢書 演算法 第4版》