幾種基本數據結構--棧、隊列、雙向鏈表、有根樹的分析和簡單實現

    本文介紹幾種基本數據結構--棧、隊列、雙向鏈表、有根樹。

一、棧

    棧不用多說了,一種LIFO(後進先出)的數據結構,我們使用Java實現其入棧(PUSH),出棧(POP)的基本操作:

public class Stack<E> {
    public final static int DEFAULTSIZE = 10;
    private final E[] elements;
    private int top;//下一次入棧的位置
    public Stack(){
        this(DEFAULTSIZE);
    }
    @SuppressWarnings("unchecked")
    public Stack(int size){
        elements = (E[]) new Object[size];
        top = 0;
    }
    
    public void push(E data){
        if(top >= elements.length)
            throw new IllegalArgumentException("Stack Overflow");
        elements[top++] = data;
    }
    
    public E pop(){
        if(top <= 0)
            throw new IllegalArgumentException("Stack Underflow");
        E data = elements[--top];
        elements[top] = null;//help GC
        return data;
    }
}

    棧是一種很簡單也很基礎的數據結構,但是這裏使用Java實現的時候也要注意幾點:

    1、注意棧的上溢出(overflow,也就是棧滿了還進行push),以及棧的下溢出(underflow,也就是棧空還進行pop)的處理。

    2、首先爲了保證每種棧可以存儲不同數據類型我們使用了泛型,然後在創建盛裝元素的數組的時候不能直接new一個泛型數組,因爲泛型僅僅存在於編譯期間,在運行期間會擦出,而內存分配是在運行期間做的,這矛盾了。因此不能直接new一個泛型數組,而是new一個Object數組,然後做一個強制類型轉化,轉化爲對應泛型類型的數組。

    3、Java是一門有GC機制的語言,對於沒有引用的堆對象會被GC回收。然而如果我們不需要再訪問這個堆對象了,卻一直保持着它的引用,這片內存就不會被回收。對於我們在pop的時候,我們就需要注意這點,如果我們只是return elements[top--]; 那麼就會內存泄漏,因爲我們彈出的元素內存仍然被elements數組維護着,而不會被GC。因此需要手動element[top] = null;  讓應該被彈出的元素不再被引用。


二、隊列

    隊列也是一種很簡單很基礎的FIFO(先進先出)的數據結構,我們簡單實現其入隊(enqueue)和出隊(dequeue)操作:

public class Queue<E> {
    private int head;
    private int tail;//下次元素放的位置
    public static final int DEFAULT_SIZE = 10;
    private final E[] elements;
    @SuppressWarnings("unchecked")
    public Queue(int size){
        elements = (E[]) new Object[size+1];
        head = 0;
        tail = 0;
    }
    public Queue(){
        this(DEFAULT_SIZE);
    }
    public void enqueue(E x){
        if(head == tail+1)
            throw new IllegalArgumentException("Queue Overflow");
        elements[tail] = x;
        tail = (++tail)%elements.length;
    }
    public E dequeue(){
        if(head == tail)
            throw new IllegalArgumentException("Queue Underflow");
        E temp = elements[head];
        elements[head] = null;//help GC
        head = (++head)%elements.length;
        return temp;
    }
}

    同樣,我們需要注意以下幾點:

    1、我們使用size+1容量的數組來存儲size容量隊列,使用head表示隊列頭,tail表示隊列尾。因爲我們需要區分隊列空和隊列滿,如果使用size容量來存儲size容量隊列就會發現隊列滿和隊列空都是滿足head = tail。然而使用size+1容量來存儲的時候隊列滿時滿足head = tail,此時如果繼續enqueue就會queue overflow,隊列空時滿足head + 1 = tail,此時如果繼續dequeue就會queue underflow。

    2、和棧一樣,注意泛型的使用GC問題,不要試圖創建泛型數組,注意堆中不使用對象的引用清除。


三、雙向鏈表

    鏈表是一種各對象按照線性順序排列的數據結構,鏈表的順序由個各對象裏的後繼指針next決定。而雙向鏈表每個元素則有兩個指針,一個前驅指針prev,一個後繼指針next。下面是實現代碼,實現雙向鏈表的刪除、插入、查找,我們使用了空頭結點,這個頭結點不包含數據,只是起到頭的作用,也就是我們說的哨兵

public class DoublyLinkedList<E> {
    private static class LinkedNode<E>{
        private E data;
        private LinkedNode<E> prev;
        private LinkedNode<E> next;
        public LinkedNode(E data){
            this.data = data;
        }
        public LinkedNode(){
            this.data = null;//頭結點
        }
    }
    //空的頭指針
    private LinkedNode<E> head;
    public DoublyLinkedList(){
        head = new LinkedNode<E>();
    }
    //將元素x插入到索引index的位置
    //插入到index應該找到index位置的前一個結點
    public void insert(int index, E x){
        //從頭結點的第一個結點計數
        LinkedNode<E> node = head;
        int i = 0;
        for( i = 0; i<index&&node.next!=null ; i++)
            node = node.next;
        //結束循環時i == index 或者 node.next == null
        //找到了索引前一個結點node並且node是最後一個結點 進行插入
        if(i != index)//沒到索引就結束了 說明索引超出了鏈表範圍
            throw new IllegalArgumentException("Illegal Argument : index "+index);
        LinkedNode<E> newNode = new LinkedNode<E>(x);
        LinkedNode<E> oldTail = node.next;
        node.next = newNode;
        if(oldTail != null){
            newNode.next = oldTail;
            oldTail.prev = newNode;
        }
        newNode.prev = node;
    }
    public LinkedNode<E> search(E x){
        LinkedNode<E> node = head.next;
        while( node!=null && node.data!=x ){
            node = node.next;
        }
        return node;
    }
    public boolean delete(E x){
        LinkedNode<E> node = search(x);
        if(node==null)
            return false;
        else{
            //node爲頭結點的下一個結點
            if(node.prev!=head)
                node.prev.next = node.next;
            else
                head.next = node.next;
            if(node.next!=null)
                node.next.prev = node.prev;
            return true;
        }
    }
}

    鏈表作爲一種很基礎的數據結構沒什麼可說的,但是涉及到大量的指針操作,請注意邊界判斷,注意代碼的魯棒性


四、有根樹

    有根樹就是一個相對比較複雜的基礎數據結構了,有根樹有一個根結點,根節點可能會存在子結點,子結點又可能會存在其他子結點,但是這些結點之間不可能構成圖。

    我們首先討論一下樹的一些常用的表示方法。

    1、k孩子表示法

    顯而易見的,我們會想到對於一個結點有子結點,我們就可以使用child1,child2,...,childk來表示k個子結點。但是這種方法會存在一個問題,就是我們有時候不確定要分配多少個屬性來表示孩子。如果我們不使用屬性,而是使用數組來表示孩子們,也會存在一個問題,就是我們不確定結點的孩子個數,所以我們總是需要開闢最大可能的數組,這就導致了空間的大量浪費。我們使用最簡單的二叉樹來實現這種表示方法:

public class BinaryTreeWithChildRep<E> {
    private TreeNode<E> root;
    public static class TreeNode<E>{
        private E data;
        private TreeNode<E> leftChild;
        private TreeNode<E> rightChild;
        public TreeNode(E data){
            this.data = data;
        }
        public TreeNode(TreeNode<E> leftChild, E data, TreeNode<E> rightChild){
            this.data = data;
            this.leftChild = leftChild;
            this.rightChild = rightChild;
        }
    }
    public BinaryTreeWithChildRep(TreeNode<E> root){
        this.root = root;
    }
    public static void main(String[] args) {
        TreeNode<Integer> node4 = new TreeNode<Integer>(4);
        TreeNode<Integer> node5 = new TreeNode<Integer>(5);
        TreeNode<Integer> node2 = new TreeNode<Integer>(node4, 3, node5);
        TreeNode<Integer> node3 = new TreeNode<Integer>(3);
        TreeNode<Integer> node1 = new TreeNode<Integer>(node2,1,node3);
        BinaryTreeWithChildRep<Integer> tree = new BinaryTreeWithChildRep<>(node1);
    }
}

    對於上面的代碼,我們就創建了這樣一個二叉樹:



    2、左孩子右兄弟表示法

    對於上面k孩子表示法中出現的問題,我們可以用左孩子右兄弟表示法來解決。對於樹的每個結點只存在兩個指針,一個leftChild表示最左邊的孩子,一個rightSibling表示該結點右側的兄弟結點。我們使用最簡單的二叉樹來實現這個表示方法:

public class BinaryTreeWithSiblingRep<E> {
    public static class TreeNode<E> {
        private E data;
        private TreeNode<E> leftChild;
        private TreeNode<E> rightSibling;
        public TreeNode(E data, TreeNode<E> leftChild, TreeNode<E> rightSibling){
            this.leftChild = leftChild;
            this.data = data;
            this.rightSibling = rightSibling;
        }
    }
    private TreeNode<E> root;
    public BinaryTreeWithSiblingRep( TreeNode<E> root) {
        this.root = root;
    }
    public static void main(String[] args) {
        TreeNode<Integer> node5 = new TreeNode<Integer>(5,null,null);
        TreeNode<Integer> node4 = new TreeNode<Integer>(4,null,node5);
        TreeNode<Integer> node3 = new TreeNode<Integer>(3,null,null);
        TreeNode<Integer> node2 = new TreeNode<Integer>(2,node4,node3);
        TreeNode<Integer> node1 = new TreeNode<Integer>(1,node2,node3);
        BinaryTreeWithSiblingRep<Integer> tree = new BinaryTreeWithSiblingRep<Integer>(node1); 
    }
}

    對於這段代碼,我們創建的二叉樹表示如下:



    3、數組表示法

    一棵n層高的樹(根算第一層)總共最多可以有2^n-1個結點,因此對於一個n層高的樹我們可以開闢一個2^n-1,其中第2^(i-1)-1到2^i個元素(從0開始計數)爲第i層的元素對應序列。對於第i個結點(從0開始計數),如果有的話,其左結點爲第2i+1個元素,右結點爲2(i+1)個元素。這種表示方法很簡單,而且訪問某個元素的複雜度爲O(1),但是存在大量空間浪費。我們來實現一下二叉樹中這種表示方式的左右子結點和父結點的獲取:

public class BinaryTreeWithArrayRep<E> {
    private final E[] elements;
    
    @SuppressWarnings("unchecked")
    public BinaryTreeWithArrayRep(E[] array){
        if(Integer.toBinaryString(array.length+1).lastIndexOf('1')!=0)
            throw new IllegalArgumentException("The length - 1 of the array is not the power of 2");
        elements = (E[]) new Object[array.length];
        System.arraycopy(array, 0, elements, 0, array.length);
    }
    public E getLeftChild(int index){
        int result = (index<<1)+1;
        if(result<elements.length)
            return elements[result];
        else
            return null;
    }
    public E getRightChild(int index){
        int result = (index+1)<<1;
        if(result<elements.length)
            return elements[result];
        else
            return null;
    }
    public E getParent(int index){
        if(index%2==0&&(index>>1)-1>=0)
            return elements[(index>>1)-1];
        else if(index%2!=0&&index-1>>1>=0)
            return elements[index-1>>1];
        else
            return null;
    }
}

    可以看到這種表示方法十分簡單,效率也很高。這裏有一個小細節,我們構造參數傳入數組的時候需要判斷這個數組是不是2的整數次冪減一,我們可以把數組長度加一,轉換成二進制,然後判斷最後一個一的位置,以此來判斷是不是2的整數次冪。

    當樹的結點密度比較大或者沒有對空間上的要求的時候就比較合適採用這種方法,否則這種方法會產生大量的空間浪費。


    以上樹的三種表示方法只是常用的三種表示方法,每種方法都有其優點,具體用哪一種看具體需要。


    這就是本文介紹的所有的基本數據結構,如果有錯誤歡迎提出。

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