1. 程式人生 > >Java學習筆記-ArrayList(2)和LinkedList

Java學習筆記-ArrayList(2)和LinkedList

上一篇中我們大致介紹了ArrayList的優點和隱藏的,不容易被發現的弊端。但是這一篇,我們還要再對ArrayList批判一番。

又因為它是陣列,當我們需要往列表最後丟一個數據的時候很簡單,但是如果要往中間丟呢?方法大家肯定都想到了。挪唄!後面的各位同學讓讓,擠個人進來:

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

先看一下這個人是不是真的進來的地方對,別跑到很後面去了;再看看進來以後地方還夠不夠了,不夠還得擴容(grow),然後不好意思,這人位置後面的全部copy往後移一位。這個System.arraycopy和Arrays.copyof可不一樣,它不會新建一個新的陣列物件,但是會挨個去賦值交換,就跟我們自己寫for迴圈,arr[i+1]=arr[i]一樣。極端一點的情況,假如我們要往List的頭部插一個數據(雖然ArrayList並沒有addFirst方法),那就得把後面所有的資料都挨個移位!

而addall是怎麼操作的呢?

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

一樣是移,只不過這次不是一個人了,我得算算要進來幾個人。怎麼算呢,先用toArray,再獲取length。這個toArray也是一比開銷,如果原來傳進來的也是個ArrayList還好,偷懶copyof就行了;如果不是,那就還得先轉換成陣列,再移位,再複製,是不是頭都大了?

同理,如果我想從列表裡刪掉一個或者幾個節點,那麼後面的也得統統移位,這個操作量就很大了。所以這裡我們隆重推薦ArrayList的一個兄弟:LinkedList。

不難想到,LinkedList的建構函式中不用去指定預設大小了。它裡面的資料結構也不是陣列了,而是節點(Node)。別誤會,這個Node可不是xml的,也跟org.w3c.dom半毛錢關係都沒有。這個Node是LinkedList的一個內部類:

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

每個節點記錄了:它自己儲存的內容,指向它的前一個和後一個節點的引用(或者說,指標)。

對LinkedList的add和remove操作,實際上是通過一系列link和unlink進行操作的,這些方法有:linkFirst、linkLast、linkBefore(Node)、unlinkFirst、unLinkLast、unLink(Node)。他們實際做的,就是修改節點中指向前一個和後一個的指標。

我們用add(E, index)舉例子:

    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

首先判斷了一下下標是否合法,然後看是不是在隊尾(是則視作linkLast),然後呼叫了一個node方法去取得某個下標的節點。這個方法中實際上也進行了一遍遍歷,所以開銷其實也是不小的。然後就呼叫linkBefore的方法,修改了這個插入的節點之前節點的”向後的指標“讓它指向自己,修改這個插入的節點之後節點的”向前的“節點讓它也指向自己,自己的兩個指標則指向前後兩個節點,這樣這個節點就算插入進去了。

有點繞是不是?鑑於我的繪圖水平有限,建議看不懂的去搜一下連結串列的圖,很簡單就懂了。

那麼有人問了最後那個節點呢?它的往下一個節點的指標指向啥?null唄。

從上面這個例子我們可以看出,對一個LinkedList的頭和尾進行資料操作是很高效的,因為只需要改改指標就行了。但是如果要往中間增刪節點,由於有一個遍歷過程,效率就沒那麼高了,但是仍然優於ArrayList(因為不需要進行大規模的資料遷徙),而addAll方法需要先把傳入的集合變化成陣列,再往裡插,效率會更加低一些,和ArrayList孰優孰劣我也沒驗證過,大家有興趣可以去試一下。

由於LinkedList往頭尾增刪資料很方便這種特性,我們可以用它模擬棧(stack)這種資料結構,實際上LinkedList也提供了一系列的方法,其中就有棧操作的push和pop:

    public void push(E e) {
        addFirst(e);
    }  //往頭(棧頂)上插個數據(壓棧)

    public E pop() {
        return removeFirst();
    }  //刪除並返回頭上的資料(出棧)

    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    } //返回但不刪除頭上的資料

    public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
     } //等於peek

    public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }  //返回但不刪除尾巴的資料

    public boolean offer(E e) {
        return add(e);
    } //等同於add,再尾部新增資料

    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }  //addFirst 不多解釋了,返回值不同而已

    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }//類比上面

    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    } //其實就是pop 寫法不同而已

    public E pollFirst() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }//你懂的

    public E pollLast() {
        final Node<E> l = last;
        return (l == null) ? null : unlinkLast(l);
    }//你懂的

    public E element() {
        return getFirst();
    }//getFirst換個名字而已=_= 用來instanceof名字更直觀?

其他的不再贅述了,大家可以自己去翻原始碼。另外,它的toArray就比較痛苦了:
    public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
    }
一樣是要遍歷整個連結串列,效率比ArrayList低了不止一點半點,從這裡我們可以看出只要是addAll都得經歷一個痛苦地轉陣列的過程,而LinkedList要更加痛苦一些。對於將一大批物件丟到集合裡這個過程,set比List效率更優,即使不看具體實現也比較好理解:set不需要維護裡面物件的有序性,自然更有優勢。