棧
可變長數組實現
鏈表實現
數組與鏈表的對比
隊列
鏈表實現
棧
下壓棧(簡稱棧)是一種基於后進后出(LIFO)策略的集合類型。這里學習分別用數組和鏈表這兩種基礎數據結構來實現棧。
棧支持的基本操作有push,pop。
可變長數組實現
要用數組實現棧,可以聲明一個int型的標記,這個標記指向的位置即為棧頂,push操作時,將值放在數據中標記的位置,同時將標記+1,pop時,返回數組在標記位置的值,同時標記-1。
但java中的數組在聲明的時候其長度就已經固定了,所以棧使用的空間只能是這個數組最大容量的一部分。為了能容納更多的數據而聲明一個特別大的數組會非常浪費空間,那如何解決這個問題,達到既不會浪費數組空間也不會超出數組范圍呢?
可以采用動態調整數組大小的方式,在push操作導致棧已滿時,重新創建一個數組,其容量為原數組的兩倍。同理在pop操作使數組的閑置空間達到一定程度時,重新創建一個容量更小的數組。但閑置的判斷標准不能為一半1/2,否則會造成在1/2的臨界點處push和pop操作時發生“抖動”,即頻繁地進行數組擴容、縮容操作,這會極大地降低棧的性能。所以通常的做法是在減少到數組容量的1/4時縮容為1/2。
實現可變長數組的代碼如下:
public class StackResizeArray<Item> { private Item[] a; // array of items private int n; // number of elements on stack public StackResizeArray() { a = (Item[]) new Object[2]; n = 0; } public boolean isEmpty() { return n == 0; } public int size() { return n; } private void resize(int capacity) { assert capacity >= n; Item[] temp = (Item[]) new Object[capacity]; for (int i = 0; i < n; i++) { temp[i] = a[i]; } a = temp; } public void push(Item item) { if (n == a.length) resize(2 * a.length); // double size of array if necessary a[n++] = item; // add item } public Item pop() { if (isEmpty()) throw new NoSuchElementException("Stack underflow"); Item item = a[n - 1]; a[n - 1] = null; // to avoid loitering n--; // shrink size of array if necessary if (n > 0 && n == a.length / 4) resize(a.length / 2); return item; } }
pop方法中,a[n - 1] = null將數組中已經出棧的位置設為null,是為了避免對象游離,否則這個位置的對象雖然已經不在棧中,但還被數組引用着,導致GC無法對其回收。
鏈表實現
棧的另外一種實現方式是采用鏈表,鏈表是一種遞歸的數據結構,它或者為空,或者指向一個結點的引用,該結點含有一個泛型的元素和一個指向另一條鏈表的引用。 構成鏈表的基本結點可以為:
private static class Node<Item> { public Item item; public Node next; }
其中泛型的item變量存放數據,同樣是Node類型的next變量用來指向下一個結點。將鏈表的頭部作為棧頂,push相當於在表頭插入元素,pop是從表頭刪除元素。 代碼如下:
public class StackLinkedList<Item> { private static class Node<Item> { public Item item; public Node next; } private Node<Item> first; private int N; private 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; } }
數組與鏈表的對比
數組與鏈表的區別決定了兩種棧實現的區別:
- 存取方式上,數組可以順序存取或者隨機存取,而鏈表只能順序存取;
- 存儲位置上,數組邏輯上相鄰的元素在物理存儲位置上也相鄰,而鏈表不一定;
- 存儲空間上,鏈表由於帶有指針域,存儲密度不如數組大;
- 按序號查找時,數組可以隨機訪問,需要的時間是常數,而鏈表不支持隨機訪問,需要的時間與數據規模成線性關系。
- 按值查找時,若數組無序,數組和鏈表時間復雜度均為O(n),但是當數組有序時,可以采用折半查找將時間復雜度降為O(logn);
- 插入和刪除時,數組平均需要移動n/2個元素,而鏈表只需修改指針即可;
- 空間分配方面: 雖然可變長數組可以擴充,但需要移動大量元素,導致操作效率降低,而且如果內存中沒有更大塊連續存儲空間將導致分配失敗; 鏈表存儲的節點空間只在需要的時候申請分配,只要內存中有空間就可以分配,操作比較靈活高效;
隊列
鏈表實現
先進先出隊列(簡稱隊列)是一種基於先進先出(FIFO)策略的集合類型。這里只關注隊列的鏈表實現。與鏈表實現的棧類似,用結點包含泛型的item變量和next變量,前者存放數據,后者用來指向下一個結點。不同之處在於鏈表的頭部、尾部都會被操作,從鏈表的一端入隊(enqueue),從另一端出隊(dequeue) 代碼:
public class Queue<Item> { private static class Node<Item> { public Item item; public Node next; } private Node<Item> first; private Node<Item> last; private int N; public void enqueue(Item item) { Node<Item> oldLast = last; last = new Node<Item>(); last.item = item; if (isEmpty()) { first = last; } else { oldLast.next = last; } N++; } public Item dequeue() { if (isEmpty()) { return null; } Item result = first.item; first = first.next; if (isEmpty()) { last = null; } N--; return result; } public boolean isEmpty() { return N == 0; } public int size() { return N; } }