前言
上一篇:算法分析
下一篇:基本排序
本篇內容主要是棧,隊列 (和包)的基本數據類型和數據結構
在很多應用中,我們需要維護多個對象的集合,而對這個集合的操作也很簡單
基本數據類型
- 對象的集合
-
操作:
- insert -- 向集合中添加新的對象
- remove -- 去掉集合中的某個元素
- iterate -- 遍歷集合中的元素並對他們執行某種操作
- test if empty -- 檢查集合是否爲空
- 做插入和刪除操作時我們要明確以什麼樣的形式去添加元素,或我們要刪除集合中的哪個元素。
處理這類問題有兩個經典的基礎數據結構:棧(stack) 和隊列(queue)
兩者的區別在於去除元素的方式:
-
棧:去除最近加入的元素,遵循後進先出原則(LIFO: last in first out)。
- 插入元素對應的術語是入棧 -- push;去掉最近加入的元素叫出棧 -- pop
-
隊列:去除最開始加入的元素,遵循先進先出原則(FIFO: first in first out)。
- 關注最開始加入隊列的元素,爲了和棧的操作區分,隊列加入元素的操作叫做入隊 -- enqueue;去除元素的操作叫出隊 -- dequeue
此篇隱含的主題是模塊式編程,也是平時開發需要遵守的原則
模塊化編程
這一原則的思想是將接口與實現完全分離。比如我們精確定義了一些數據類型和數據結構(如棧,隊列等),我們想要的是把實現這些數據結構的細節完全與客戶端分離。客戶端可以選擇數據結構不同的實現方式,但是客戶端代碼只能執行基本操作。
實現的部分無法知道客戶端需求的細節,它所要做的只是實現這些操作,這樣,很多不同的客戶端都可以使用同一個實現,這使得我們能夠用模塊式可複用的算法與數據結構庫來構建更復雜的算法和數據結構,並在必要的時候更關注算法的效率。
Separate client and implementation via API.
API:描述數據類型特徵的操作
Client:使用API操作的客戶端程序。
Implementation:實現API操作的代碼。
下面具體看下這兩種數據結構的實現
棧
棧 API
假設我們有一個字符串集合,我們想要實現字符串集合的儲存,定期取出並且返回最後加入的字符串,並檢查集合是否爲空。我們需要先寫一個客戶端然後再看它的實現。
字符串數據類型的棧
性能要求:所有操作都花費常數時間
客戶端:從標準輸入讀取逆序的字符串序列
測試客戶端
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public static void main(String[] args)
{
StackOfStrings stack = new StackOfStrings();
while (!StdIn.isEmpty())
{
//從標準輸入獲取一些字符串
String s = StdIn.readString();
//如果字符串爲"-",則客戶端將棧頂的字符串出棧,並打印出棧的字符串
if (s.equals("-")) StdOut.print(stack.pop());
//否則將字符串入棧到棧頂
else stack.push(s);
}
}
客戶端輸入輸出:
棧的實現:鏈表
鏈表(linked-list)連接待添加...
我們想保存一個有節點組成的,用來儲存字符串的鏈表。節點包含指向鏈表中下一個元素的引用(first).
維持指針 first 指向鏈表中的第一個節點
- Push:入棧,在鏈表頭插入一個新的節點
- Pop:出棧,去掉鏈表頭處第一個節點
Java 實現
public class LinkedStackOfStrings
{
//棧中唯一的實例變量是鏈表中的第一個節點的引用
private Node first = null;
//內部類,節點對象,構成鏈表中的元素,由一個字符串和指向另一個節點的引用組成
private class Node
{
private String item;
private Node next;
}
public boolean isEmpty()
{ return first == null; }
//
public void push(String item)
{
//將指向鏈表頭的指針先保存
Node oldfirst = first;
//創建新節點:我們將要插入表頭的節點
first = new Node();
first.item = item;
//實例變量的next指針指向鏈表oldfirst元素,現在變成鏈表的第二個元素
first.next = oldfirst;
}
//出棧
public String pop()
{
//將鏈表中的第一個元素儲存在標量 item 中
String item = first.item;
//去掉第一個節點:將原先指向第一個元素的指針指向下一個元素,然後第一個節點就等着被垃圾回收處理
first = first.next;
//返回鏈表中原先保存的元素
return item;
}
}
圖示:
出棧:
入棧:
性能分析
通過分析提供給客戶算法和數據結構的性能信息,評估這個實現對以不同客戶端程序的資源使用量
Proposition 在最壞的情況下,每個操作只需要消耗常數時間(沒有循環)。
Proposition 具有n個元素的棧使用 ~40n 個字節內存
(沒有考慮字符串本身的內存,因爲這些空間的開銷在客戶端上)
棧的實現:數組
棧用鏈表是實現花費常數的時間,但是棧還有更快的實現
另一種實現棧的 natural way 是使用數組儲存棧上的元素
將棧中的N個元素保存在數組中,索引爲 n,n 對應的數組位置即爲棧頂的位置,即下一個元素加入的地方
- 使用數組 s[] 在棧上存儲n個元素。
- push():在 s[n] 處添加新元素。
- pop():從 s[n-1] 中刪除元素。
在改進前使用數組的一個缺點是必須聲明數組的大小,所以棧有確定的容量。如果棧上的元素個數比棧的容量多,我們就必須處理這個問題(調整數組)
Java 實現
public class FixedCapacityStackOfStrings
{
private String[] s;
//n 爲棧的大小,棧中下一個開放位置,也爲下一個元素的索引
private int n = 0;
//int capacity:看以下說明
public FixedCapacityStackOfStrings(int capacity)
{ s = new String[capacity]; }
public boolean isEmpty()
{ return n == 0; }
public void push(String item)
{
//將元素放在 n 索引的位置,然後 n+1
s[n++] = item;
}
public String pop()
{
//然後返回數組n-1的元素
return s[--n];
}
}
int capacity: 在構造函數中加入了容量的參數,破壞了API,需要客戶端提供棧的容量。不過實際上我們不會這麼做,因爲大多數情況下,客戶端也無法確定需要多大棧,而且客戶端也可能需要同時維護很多棧,這些棧又不同時間到達最大容量,同時還有其他因素的影響。這裏只是爲了簡化。在調整數組中會處理可變容量的問題,避免溢出
對於兩種實現的思考
上述的實現中我們暫時沒有處理的問題:
Overflow and underflow
- Underflow :客戶端從空棧中出棧我們沒有拋出異常
- Overflow :使用數組實現,當客戶端入棧超過容量發生棧溢出的問題
Null item:客戶端是否能像數據結構中插入空元素
Loitering 對象遊離:即在棧的數組中,我們有一個對象的引用,可是我們已經不再使用這個引用了
數組中當我們減小 n 時,在數組中仍然有我們已經出棧的對象的指針,儘管我們不再使用它,但是Java系統並不知道。所以爲了避免這個問題,有效地利用內存,最好將去除元素對應的項設爲 null,這樣就不會剩下舊元素的引用指針,接下來就等着垃圾回收機制去回收這些內存。這個問題比較細節化,但是卻很重要。
public String pop()
{
String item = s[--n];
s[n] = null;
return item;
}