天天看點

《算法》筆記 1 - 棧和隊列

《算法》筆記 1 - 棧和隊列
    • 可變長數組實作
    • 連結清單實作
    • 數組與連結清單的對比
  • 隊列

下壓棧(簡稱棧)是一種基于後進後出(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;
    }
}