數據結構梳理(3) - 棧

前言

上次梳理了數組和鏈表,這次我們再來看看棧,棧也是常用的數據結構之一,我們這次除了瞭解它的特性之外,主要是手動來實現它,平常我們可能都是直接使用的java api裏的stack類,我們很少會去關注它的實現原理,如果這時候來了個任務,讓自己實現一個輕量級的棧呢,對吧,所以自己動手實現纔是最可靠的,也是非常有必要的,我主要是分爲兩種,一種是基於數組實現,一種是基於鏈表實現,好了,我們開始吧!

目錄

1、棧的特性與適用場景
2、基於數組實現棧
3、基於鏈表實現棧
4、Java api中的棧(stack)實現

正文

1、棧的特性與適用場景

棧是一個非常好用的數據結構,關於棧的特性這裏,其實就是四個字,後進先出,我們正常的思維是誰先來就誰先走,類似排隊的效果,但是往往一些場景需要的效果正好是反的,需要誰後來就誰先走這樣的效果,例如回溯遞歸效果,迷宮求解路徑效果等,在遇到相對應的實際問題時,要能使用棧合理的解決問題。

由於比較簡單,我就不贅述了,放一張圖加深下對棧的理解。
在這裏插入圖片描述

2、基於數組實現棧

下面我們來手動實現一個輕量級的棧,一般有兩種方式,基於數組或者基於鏈表,我們首先基於數組來實現,既然是基於數組,所以第一步就是先聲明一個數組類型的成員變量,然後爲了方便,一般還需聲明當前元素個數,棧頂指針,以及默認數組長度等,如下

private  int[] arr;
private int topIndex;
private static final int length_default=10;//棧大小的默認值
private int size;//元素個數

然後就是初始化操作,這裏也就是初始化數組,由於int類型默認爲0,所以符合要求,初始化代碼如下

public ArrayStack() {
	this(length_default);
}

public ArrayStack(int length) {
	arr=new int[length];
	topIndex=-1;
}

有了這些成員變量和初始化操作之後,接下來就是最核心的兩個方法,入棧和出棧,我們要結合棧的特性來思考這兩個方法該怎麼實現,其核心就是這個棧頂指針的調整,每當我們添加一個元素的時候,就將棧頂指針+1,刪除一個元素的時候,就將棧頂指針減一,當我們要彈出一個元素的時候,就是直接返回棧頂指針代表的下標元素,然後棧頂指針減一即可,好了,我們現在已經有了思路。

然後爲了程序的健壯性,入棧的時候一定要記得判斷棧是否滿,出棧的時候也一樣要判斷棧是否爲空,由於生命了棧頂指針,在這兩個操作之後,一定要記得更新棧頂指針的位置,入棧出棧的代碼如下

public void push(int data){
	if(isFull()){
		throw new IndexOutOfBoundsException("棧滿啦");
	}
	arr[++topIndex]=data;
	size++;
}

public int pop(){
	if(isEmpty()){
		throw new NullPointerException("棧爲空");
	}
	size--;
	return arr[topIndex--];
}

然後爲了我們這個數據結構使用的方便,我們可以爲其添加獲取棧頂元素、判空、判滿、打印等方法,如下

public int peek(){
	if(isEmpty()){
		throw new NullPointerException("空指針啦");
	}
	return arr[topIndex];
}

public boolean isEmpty(){
	return topIndex==-1;
}

public boolean isFull(){
	return size==arr.length;
}

//獲取棧中元素的個數
public int getSize(){
	return size;
}

public void clear(){
	topIndex=-1;
}

@Override
public String toString() {
	if(isEmpty()){
		return "[]";
	}
	String str = "[ ";  
       for (int i = 0; i <= topIndex; i++) {  
           str += arr[i] + ", ";  
       }  
       str = str.substring(0, str.length()-2) + " ]";  
       return str;  
}

然後還有一個比較常用的功能沒有實現,就是擴容,這個根據需求,自己設定擴容方法,一般每次擴大之前的兩倍即可。

到這裏就基本差不多了,一個基於數組的輕量級棧實現完畢,怎麼樣,是不是覺得很簡單,我們現在可以爲它寫一寫測試代碼,驗證一下它,如果實際業務還需要其它功能的話,就可以再在這個基礎上添磚加瓦,完成更復雜的功能。

3、基於鏈表實現棧

剛纔實現了基於數組的棧,現在我們再換種實現方式,實現一個基於鏈表的棧。

首先,既然是基於鏈表,那麼我們先啥也不說,定義好我們的節點類,如下

public class Node{
	private int data;
	private Node next;
	
	public Node() {
		
	}
	
	public Node(int data,Node next){
		this.data=data;
		this.next=next;
	}
	
	public int getData(){
		return data;
	}
}

然後我們思考它應該有哪些成員變量呢,首先想到的就是棧頂指針,由於是鏈表,所以棧頂指針是一個Node對象,然後就是棧中元素的數量size,然後我們在初始化的時候,要分別給這兩個對象賦值,如下

private Node topNode;
private int size;

public LinkedStack(){
	topNode=null;
	size=0;
}

接下來就是核心操作,壓棧和彈出操作的實現,我們有了上面基於數組的實現方式的啓發之後,會發現棧頂指針就是用來指向最新壓棧的元素的,那麼只要抓住這個核心即可,對鏈表來說,怎麼讓棧頂指針指向棧中最新的壓棧元素呢,答案就是鏈表的頭插法,然後更新插入節點爲棧頂指針即可,然後彈出的時候,只需要刪除頭結點,也就是棧頂指針指向的節點,然後更新棧頂指針即可。ok,有了思路之後,我們就來實現這兩個方法吧,如下

public void push(int data){
	Node node=new Node(data, topNode);
	topNode=node;
	size++;
}

public int pop(){
	if(topNode==null){
		throw new NullPointerException("棧爲空");
	}
	int data=topNode.getData();
	topNode=topNode.next;
	size--;
	return data;
}

由於是鏈表,沒有長度的限制,所以我沒有做棧判滿的處理,當然實際使用的話,最好還是手動設置一個長度限制,以免發生數據無限壓棧的情況發生。

同樣的,我們也可爲其添加獲取棧頂元素、判空、清空、打印等方法,如下

public int peek(){
	if(topNode==null){
		throw new NullPointerException("棧爲空");
	}
	int data=topNode.getData();
	return data;
}

public boolean isEmpty(){
	return size==0;
}

public void clear(){
	topNode=null;
	size=0;
}

public int getSize(){
	return size;
}

@Override
public String toString() {
	if (isEmpty()) {
		return "null";
	} else {
		StringBuilder sb = new StringBuilder("");
		for (Node current = topNode; current != null; current = current.next)// 從head開始遍歷
		{
			sb.append(current.data + "-");
		}
		int len = sb.length();
		return sb.delete(len - 1, len).append("").toString();// 刪除最後一個 -
	}
}

寫完之後,我們爲了正確性,一定要寫一些測試用例來檢驗這段代碼,主要是一些易出錯的邊界情況的測試。

4、Java api中的棧(stack)實現

好了,上面已經手動實現了一個輕量級的棧,可能功能還比較簡單,但是實現了基礎之後,再有其它需求,我們只需要在這個打好的“地基”上接着建造即可,所以這個就贅述了。

這時候,如果你有心的話,可能會去翻一番jdk中提供的棧的實現,我們現在來看一下人家實現的一個功能完善的棧是什麼樣子,和我們的有什麼區別,有哪些地方是值得我們學習的。

這時候,我們點開源碼,發現,咦,怎麼代碼這麼少,一看,原來是偷懶了,hhhh,,繼承了Vector這個類。jdk源代碼去掉註釋後,代碼如下

public class Stack<E> extends Vector<E> {

    public Stack() {
    }
    
    public E push(E item) {
        addElement(item);
        return item;
    }

    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }

    public boolean empty() {
        return size() == 0;
    }

    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = 1224463164541339165L;
}

我們可以明顯發現很多工作,都是這個Vertor類做的,這個類是幹嘛的呢,如果jdk用的不熟的話,可能沒有用過這個類,這個其實非常簡單,就是一個線程安全版本的ArrayList,然後我們發現Stack類中的部分方法加了synchronized關鍵字,加了這個關鍵字之後的效果就是同一時間只能單線程訪問,然後我們可以看到push方法雖然沒加synchronized關鍵字,但是它內部調用的是Vector的addElement方法,而Vertor類是線程安全的,所以這個方法也是線程安全的,最終push方法也就是線程安全的了。那說了這麼多,相信你也明白了,jdk源碼提供的這個Stack類是線程安全的。這是我們應該學習的,我們上面在自己實現的時候,沒有考慮這個問題,所以我們可以改進我們上面的實現,再提供一個線程安全的版本。

然後我們再看看具體的方法實現,發現都很簡單,和我們的思路差不多,如下我們打開push方法中調用的addElement方法,如下

public synchronized void addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);//擴容判斷
    elementData[elementCount++] = obj;
}

和我們實現的相比,只是多了一箇中間的擴容操作,然後我們再看pop方法,pop方法中是調用的Vector中的removeElementAt方法,我們打開這個方法,如下

public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                 elementCount);
    }
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}

首先是越界的處理,我們發現作者非常的細心,它將越界又分爲了兩種情況,一種是上越界,一種是下越界(<0越界),這個是我們可以優化的,其次就是它是做了元素的移動,調用的系統函數,其它的就沒啥了。

然後就是我們在使用的時候,有一個最大的區別就是jdk提供的Stack使用了泛型,這樣可以兼容所有的類型,而回過頭來發現我們實現的,都是隻能容納一種指定的數據類型, 這樣肯定是不優雅的,因爲棧這個數據結構畢竟是一個容器,我們不可能實現一個int版本的棧,再實現一個String版本的棧,換句話說,也就是我們要實現通用效果,這時候,就可以藉助系統實現的方式,使用泛型來完成這一個功能。有關泛型的使用不是本篇的重點,所以不作贅述,其實也不難,就是一些固定的語法,完全可以參展系統的實現代碼將我們自己的實現替換爲泛型實現方式,這裏就不貼代碼啦。

結語

好了,本篇到此結束,雖然只是實現一個棧,但是我們會發現在這個過程中還是會遇到很多很多的問題,然後我們再查看jdk源碼中棧的實現時,又可以發現很多我們自己設計的容器存在的問題,以及別人優秀的設計思想,以後我們自己再遇到這樣的問題的話,就要考慮到這些問題,然後吸收別人優秀的思想,將它運用到實際中去。

按照計劃,下一篇是隊列的梳理,這兩天精力充沛,嘻嘻嘻,進度很快!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章