1. 程式人生 > >資料結構梳理(3)

資料結構梳理(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原始碼中棧的實現時,又可以發現很多我們自己設計的容器存在的問題,以及別人優秀的設計思想,以後我們自己再遇到這樣的問題的話,就要考慮到這些問題,然後吸收別人優秀的思想,將它運用到實際中去。

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