1. 程式人生 > >1008:棧

1008:棧

一、什麼是棧?

1.後進者先出,先進者後出,這就是典型的“棧”結構。

2.從棧的操作特性來看,是一種“操作受限”的線性表,只允許在端插入和刪除資料。

二、為什麼需要棧?

1.棧是一種操作受限的資料結構,其操作特性用陣列和連結串列均可實現。

2.但,任何資料結構都是對特定應用場景的抽象,陣列和連結串列雖然使用起來更加靈活,但卻暴露了幾乎所有的操作,難免會引發錯誤操作的風險。

3.所以,當某個資料集合只涉及在某端插入和刪除資料,且滿足後進者先出,先進者後出的操作特性時,我們應該首選棧這種資料結構。

三、如何實現棧?

1.棧的API

public class Stack<Item> {    
    public void push(Item item){}//壓棧
    public Item pop(){}//彈棧
    public boolean isEmpty(){} //是否為空
    public int size(){}//棧中資料的數量
    public Item peek(){} //返回棧中最近新增的元素而不刪除它
}

2.陣列實現(自動擴容)

時間複雜度分析:根據均攤複雜度的定義,可以得陣列實現(自動擴容)符合大多數情況是O(1)級別複雜度,個別情況是O(n)級別複雜度,比如自動擴容時,會進行完整資料的拷貝。

空間複雜度分析:在入棧和出棧的過程中,只需要一兩個臨時變數儲存空間,所以O(1)級別。我們說空間複雜度的時候,是指除了原本的資料儲存空間外,演算法執行還需要額外的儲存空間。

實現程式碼:(棧的陣列實現)

public class StackOfArray<Item> implements Iterable<Item>{
	//儲存資料的陣列
	Item[] a = (Item[])new Object[1];
	//記錄元素個數N
	int N = 0;
	//構造器
	public StackOfArray(){}
	//新增元素
	public void push(Item item){
		//自動擴容
		if (N == a.length ) resize(2*a.length );
		a[N++] = item;
	}
	//刪除元素
	public Item pop(){
		Item item = a[--N];
		a[N] = null;
		if (N > 0 && N == a.length / 4) resize(a.length / 2);
		return item;
	}
	//是否為空
	public boolean isEmpty(){
		return N == 0;
	}
	//元素個數
	public int size(){
		return N;
	}
	//改變陣列容量
	private void resize(int length) {
		Item[] temp = (Item[])new Object[length];
		for (int i = 0; i < N; i++) {
			temp[i] = a[i];
		}
		a = temp;
	}
	//返回棧中最近新增的元素而不刪除它
	public Item peek(){
		return a[N-1];
	}
	@Override
	public Iterator<Item> iterator() {
		return new ArrayIterator();
	}
        //內部類
	class ArrayIterator implements Iterator{
		//控制迭代數量
		int i = N;
		@Override
		public boolean hasNext() {
			return i > 0;
		}
		@Override
		public Item next() {
			return a[--i];
		}
	}
}

3.連結串列實現

時間複雜度分析:壓棧和彈棧的時間複雜度均為O(1)級別,因為只需更改單個節點的索引即可。

空間複雜度分析:在入棧和出棧的過程中,只需要一兩個臨時變數儲存空間,所以O(1)級別。我們說空間複雜度的時候,是指除了原本的資料儲存空間外,演算法執行還需要額外的儲存空間。

實現程式碼:(棧的連結串列實現)

public class StackOfLinked<Item> implements Iterable<Item> {
	//定義一個內部類,就可以直接使用型別引數
	private class Node{
		Item item;
		Node next;
	}
	private Node first;
	private int N;
	//構造器
	public StackOfLinked(){}
	//新增
	public void push(Item item){
		Node oldfirst = first;
		first = new Node();
		first.item = item;
		first.next = oldfirst;
		N++;
	}
	//刪除
	public Item pop(){
		Item item = first.item;
		first = first.next;
		N--;
		return item;
	}
	//是否為空
	public boolean isEmpty(){
		return N == 0;
	}
	//元素數量
	public int size(){
		return N;
	}
	//返回棧中最近新增的元素而不刪除它
	public Item peek(){
		return first.item;
	}
	@Override
	public Iterator<Item> iterator() {
		return new LinkedIterator();
	}
	//內部類:迭代器
	class LinkedIterator implements Iterator{
		int i = N;
		Node t = first;
		@Override
		public boolean hasNext() {
			return i > 0;
		}
		@Override
		public Item next() {
			Item item = (Item) t.item;
			t = t.next;
			i--;
			return item;
		}	
	}
}

四、棧的應用

1.棧在函式呼叫中的應用

作業系統給每個執行緒分配了一塊獨立的記憶體空間,這塊記憶體被組織成“棧”這種結構,用來儲存函式呼叫時的臨時變數。每進入一個函式,就會將其中的臨時變數作為棧幀入棧,當被呼叫函式執行完成,返回之後,將這個函式對應的棧幀出棧。

2.棧在表示式求值中的應用(比如:34+13*9+44-12/3)

利用兩個棧,其中一個用來儲存運算元,另一個用來儲存運算子。我們從左向右遍歷表示式,當遇到數字,我們就直接壓入運算元棧;當遇到運算子,就與運算子棧的棧頂元素進行比較,若比運算子棧頂元素優先順序高,就將當前運算子壓入棧,若比運算子棧頂元素的優先順序低或者相同,從運算子棧中取出棧頂運算子,從運算元棧頂取出2個運算元,然後進行計算,把計算完的結果壓入運算元棧,繼續比較。

3.棧在括號匹配中的應用(比如:{}{[()]()})

用棧儲存為匹配的左括號,從左到右一次掃描字串,當掃描到左括號時,則將其壓入棧中;當掃描到右括號時,從棧頂取出一個左括號,如果能匹配上,則繼續掃描剩下的字串。如果掃描過程中,遇到不能配對的右括號,或者棧中沒有資料,則說明為非法格式。

當所有的括號都掃描完成之後,如果棧為空,則說明字串為合法格式;否則,說明未匹配的左括號為非法格式。

4.如何實現瀏覽器的前進後退功能?

我們使用兩個棧X和Y,我們把首次瀏覽的頁面依次壓如棧X,當點選後退按鈕時,再依次從棧X中出棧,並將出棧的資料一次放入Y棧。當點選前進按鈕時,我們依次從棧Y中取出資料,放入棧X中。當棧X中沒有資料時,說明沒有頁面可以繼續後退瀏覽了。當Y棧沒有資料,那就說明沒有頁面可以點選前進瀏覽了。

五、思考

1. 我們在講棧的應用時,講到用函式呼叫棧來儲存臨時變數,為什麼函式呼叫要用“棧”來儲存臨時變數呢?用其他資料結構不行嗎?

答:因為函式呼叫的執行順序符合後進者先出,先進者後出的特點。比如函式中的區域性變數的生命週期的長短是先定義的生命週期長,後定義的生命週期短;還有函式中呼叫函式也是這樣,先開始執行的函式只有等到內部呼叫的其他函式執行完畢,該函式才能執行結束。

正是由於函式呼叫的這些特點,根據資料結構是特定應用場景的抽象的原則,我們優先考慮棧結構。

2.我們都知道,JVM 記憶體管理中有個“堆疊”的概念。棧記憶體用來儲存區域性變數和方法呼叫,堆記憶體用來儲存 Java 中的物件。那 JVM 裡面的“棧”跟我們這裡說的“棧”是不是一回事呢?如果不是,那它為什麼又叫作“棧”呢?

答:JVM裡面的棧和我們這裡說的是一回事,被稱為方法棧。和前面函式呼叫的作用是一致的,用來儲存方法中的區域性變數。