深入淺出LinkedList與ArrayList(2)

引言

上一篇博文,我們瞭解了LinkedList與ArrayList的底層構造和效率問題。在這篇博文中,我自己寫了兩個自己的數據結構來感受效率問題,這些代碼的由來源於我在某易的師兄的提問。所以我做了以下整理,希望對大家有所啓發,其實我們自己也能寫底層的源碼。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290

需求

需要有一個固定長度的數據結構用於存儲數據,在數據插入到閥值的時候,移除最老的數據。使用數組與雙向鏈表實現。

基於數組實現

package com.brickworkers;
/**
 * 
 * @author Brickworker
 * Date:2017年4月18日上午11:35:01 
 * 關於類MyArrayList.java的描述:數組結構
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class MyArrayList<T> {

    private int size;//定義MyArrayList存儲數據的數量

    private Object[] table;//底層用數組存儲

    //構造函數,指定數組容量
    public MyArrayList(int capacity) {//構建一個固定容量的MyArrayList
        if(capacity >= 0)
            table = new Object[capacity];
        else
            throw new IllegalArgumentException("capacity is not illegal"+ capacity);
    }

    //新增,只插入到數組尾部
    public void add(T t){
        //如果數據量到閥值,那麼觸發移除
        if(size == table.length)
            removeOldest();
        table[size] = t;//數據放到數組最後面
        size ++;
    }


    //移除最老的
    public void removeOldest(){
        //數組整體前移,覆蓋最老的數據
        for (int i = 0; i < size - 1; i++) {
            table[i] = table[i+1];//把整個數組往前移動一位
        }
        size --;
    }
}

這代碼中我沒有繼承和實現任何接口,其實大家如果嘗試寫的話,可以繼承Iterable和實現AbstractList來實現,這樣的話,你就可以重寫集合方法,同時還可以用增強for循環來遍歷。不過如果要精簡還是向上面這段代碼一樣。

基於雙向鏈表實現

package com.brickworkers;
/**
 * 
 * @author Brickworker
 * Date:2017年4月18日上午11:35:19 
 * 關於類MyLinkedList.java的描述:雙向鏈表結構
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class MyLinkedList<T> {

    private int size;//MyLinkedList中真實存在的數據

    //避免麻煩,定義首節點和尾節點
    private Node<T> startNode; 

    private Node<T> endNode;

    private int MAX_SIZE;//最大容量

    //定義雙向節點
    private static class Node<T>{//靜態內部類
        public T date;

        public Node<T> prev;

        public Node<T> next;

        public Node(T t, Node<T> p, Node<T> n) {//節點構造函數
            this.date = t;
            this.prev = p;
            this.next = n;
        }
    }


    //構造函數
    public MyLinkedList(int capacity) {//指定容量,自定義兩個節點不計算容量
        if(capacity >=0 ){
            MAX_SIZE = capacity;
            startNode = new Node<T>(null, null, null);//起始節點
            endNode = new Node<T>(null, startNode, null);//尾節點
            startNode.next = endNode;//鏈接兩個節點
            size = 0;
        }else
            throw new IllegalArgumentException("capacity is not illegal"+ capacity);
    }


    //添加到鏈表結尾
    public void add(T t){
    //如果數據存儲達到閥值,那麼觸發移除操作
        if(size == MAX_SIZE)
            removeOldest();
        //新建包含t數據的節點,並把它插入到最後(注意我說的最後不包括自定義兩個節點)
        endNode.prev = endNode.prev.next = new Node<T>(t, endNode.prev, endNode);
        size ++;

    }


    //移除最老的節點
    private void removeOldest(){
    //避免惡意數據
        if(startNode.next == endNode){
            throw new IllegalArgumentException("can not remove Node, the size is 0");
        }
        //移除頭結點
        Node<T> p = startNode.next; //p就是頭結點(注意我說的頭結點不包括自定義的兩個節點)
        startNode.next = p.next;
        p.next.prev = startNode;
        size --;
    }

}

上面這段就是用雙向鏈表來實現,其中核心的就是一個Node的靜態內部類,配合一個插入和移除的方法。進行測試:

package com.brickworkers;

public class MyListTest {

    static void testList(int size, int forsize){
        long arrStartTime = System.currentTimeMillis();
        MyArrayList<Integer> myArrayList = new MyArrayList<Integer>(size);
        for (int i = 0; i < forsize; i++) {
            myArrayList.add(i);
        }
        System.out.println("數組結構耗時:"+(System.currentTimeMillis() - arrStartTime));

        long linkStartTime = System.currentTimeMillis();
        MyLinkedList<Integer> myLinkedList =new MyLinkedList<Integer>(size);
        for (int i = 0; i < forsize; i++) {
            myLinkedList.add(i);
        }
        System.out.println("雙向鏈表結構耗時:"+(System.currentTimeMillis() - linkStartTime));
    }

    public static void main(String[] args) {
        testList(10000, 1000000);//容量設置爲10000, 循環插入1000000次
    }

}

//輸出結果:
//數組結構耗時:6154
//雙向鏈表結構耗時:22

從上面的結果可以看出,數據的開銷非常巨大。我們考慮爲什麼MyArrayList開銷如此之大呢?核心問題其實是出在移除一個最老的數據後數組整體移動的原因,整個數組的移動開銷是非常大的。所以數組實現雖然可行,但是不合理。

我們考慮一下需求,容量一定的時候循環插入,當容量飽和的時候就需要開始移除數據。那麼我們可以考慮在數組飽和之後,把新增的數據覆蓋即將要移除的數據中。那麼其實就是不移動數組,而是移動了數據的下標,我修改了MyArrayList如下:

修改之後的MyArrayList

package com.brickworkers;
/**
 * 
 * @author Brickworker
 * Date:2017年4月18日上午11:35:01 
 * 關於類MyArrayList.java的描述:數組結構
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class MyArrayList<T> {

    private int size;

    private Object[] table;

    private int pointer; //數組引用

    //構造函數,指定數組容量
    public MyArrayList(int capacity) {
        if(capacity >= 0)
            table = new Object[capacity];
        else
            throw new IllegalArgumentException("capacity is not illegal"+ capacity);
    }

    //新增插入到引用位置
    public void add(T t){
        table[pointer] = t;//按指針指向的地方進行插入
        trimPointer();//指針使用之後需要進行指針調整
        if(size != table.length)//如果數據飽和,size不再增加
            size++;
    }


    //調整指針位置
    public void trimPointer(){
        //如果指針指向最後就回撥到最前
        if(pointer == table.length - 1){
            pointer = 0;//指針歸0
        }else{
            pointer++;//指針往前移動一位
        }

        }
}

修改之後的MyArrayList修改的核心是修改了remove的實現,用最新的數據去覆蓋最老的數據。測試數據量與上面相同的情況下,測試結果如下:

//數組結構耗時:12
//雙向鏈表結構耗時:22

發現這個時候數組的效率比雙向鏈表還要高,那麼我們雙向鏈表如果也和數組一樣實現會怎麼樣呢?以下是我修改之後的雙向鏈表實現:

修改之後的MyLinkedList

package com.brickworkers;

import javax.swing.tree.DefaultTreeCellEditor.EditorContainer;

/**
 * 
 * @author Brickworker
 * Date:2017年4月18日上午11:35:19 
 * 關於類MyLinkedList.java的描述:雙向鏈表結構
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class MyLinkedList<T> {

    private int size;

    //避免麻煩,定義首節點和尾節點
    private Node<T> startNode;

    private Node<T> endNode;

    private int MAX_SIZE;

    private Node<T> pointerNode;//目標指針

    //定義雙向節點
    private static class Node<T>{
        public T date;

        public Node<T> prev;

        public Node<T> next;

        public Node(T t, Node<T> p, Node<T> n) {
            this.date = t;
            this.prev = p;
            this.next = n;
        }
    }


    //構造函數
    public MyLinkedList(int capacity) {
        if(capacity >=0 ){
            MAX_SIZE = capacity;
            startNode = new Node<T>(null, null, null);//起始節點
            endNode = new Node<T>(null, startNode, null);//尾節點
            startNode.next = endNode;//鏈接兩個節點
            size = 0;
        }
        else
            throw new IllegalArgumentException("capacity is not illegal"+ capacity);
    }


    //添加到鏈表結尾
    public void add(T t){
    //如果雙向鏈表中存儲的數據達到閥值之後,就直接把頭節點移動到尾部進行值覆蓋
        if(size == MAX_SIZE){
            removeFirst2Last();
            pointerNode.date = t;
        }else{//如果沒有達到閥值的話,那麼就新增一個節點放置雙向鏈表最後
            endNode.prev = endNode.prev.next = new Node<T>(t, endNode.prev, endNode);
            size ++;
        }

    }


    //把頭結點移動到尾部
    private void removeFirst2Last(){
        if(startNode.next == endNode){
            throw new IllegalArgumentException("can not remove Node, the size is 0");
        }
        pointerNode = startNode.next;
        startNode.next = pointerNode.next;//解決最頭上節點
        pointerNode.next.prev = startNode;//解決指針節點的原本後節點
        pointerNode.next = endNode;//
        pointerNode.prev = endNode.prev;//解決指針節點
        endNode.prev.next = pointerNode;//解決尾節點之前的節點
        endNode.prev = pointerNode;//最後解決尾節點



    }

}

和數組的實現方式一樣,在雙向鏈表中當數據飽和之後就需要把最老的節點移動到最前面來,並進行值覆蓋。測試數據和原先還是一樣,以下是測試結果:

//數組結構耗時:12
//雙向鏈表結構耗時:20

效果不大,但是的確有一點點的優化。希望對大家有所幫助。

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