前言:許多基礎數據類型都和對象的集合有關。具體來說,數據類型的值就是一組對象的集合,所有操作都是關於添加、刪除或是訪問集合中的對象。而且有很多高級數據結構都是以這樣的結構為基石創造出來的,在本文中,我們將了解學習三種這樣的數據類型,分別是背包(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); } }
核心代碼為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()); } } }
核心方法為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()); } } }
核心方法為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版》