面試問你連結串列和LinkedList怎麼答?

邊緣煩惱發表於2019-03-26

面試問你連結串列和LinkedList怎麼答?

上篇文章說了下,資料和ArrayList,這篇文章我們說下在面試中有很大概率兩者作為兄弟同時出現的LinkedList,希望大家看完這篇文章後能夠有恍然大悟的感覺。

LinkedList底層是連結串列實現的,那麼我們首先說下什麼是連結串列。

和上篇文章的陣列相比,連結串列要相對於更復雜一點,兩者也是非常基礎、常用,而且在面試中同時出現的概率也是很大的。

上篇文章我們說到,資料是需要連續的記憶體空間來儲存的,而連結串列剛好與它相反,連結串列是不需要連續的記憶體空間的,它是通過將好多的零散的記憶體使用“指標”串聯起來使用,如果陣列和連結串列都想在計算機中申請大小為10M的記憶體,而計算機中只有10M的零散記憶體,那麼陣列就會申請記憶體失敗,連結串列就會成功。

想對陣列和ArrayList有多些瞭解的小夥伴可以看我的上篇文章 :

[juejin.im/post/5c99c7…]

既然連結串列在記憶體中都是零散的塊,那麼我們是怎麼稱呼這些小塊塊呢?

我們把這些塊叫做“結點”,為了把每個結點都串聯起來,結點中不僅會儲存資料,還有儲存下一個結點的地址。

那我們怎麼稱呼記錄下個結點地址的指標呢?

我們把他們叫做“後繼指標next”。

面試問你連結串列和LinkedList怎麼答?

連結串列中最特殊的是頭結點和尾結點,顧名思義,也就是連結串列的第一個結點和最後一個結點,第一個結點儲存著連結串列的基地址,通過這個結點,我們就能定位到它,最後一個結點的指標指向的是NULL,說明這個是連結串列的最後一個結點。

LinkedList中元素的所有操作都是在這樣的資料結構上進行的,大家可以腦補下。

連結串列也可以進行插入、刪除和查詢操作。陣列在進行插入和刪除操作的時候,會進行資料的搬移操作,因為資料要保證他的記憶體空間是連續的,而連結串列則不需要,因為他的記憶體空間本就不是連續的 ,它只需要改變相鄰結點的指標改變就夠了,所以速度是非常快的。

但是查詢就不會那麼快了,連結串列查詢資料要一個一個的遍歷結點,直到找到相應的結點。

連結串列還有雙向連結串列和迴圈連結串列,我們要說的LinkedList就是一個雙向連結串列,上面說到的單向連結串列是隻存了後一個結點的地址,而雙向連結串列呢,是同時儲存了前一個和後一個共兩個元素的地址,所以就可以很方便的獲取到一個結點的前後兩個元素,而且我們也可以從前後兩個方向來遍歷。

接下來我們看下LinkedList的原始碼:

首先是繼承關係:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
複製程式碼

也是同時繼承了Cloneable 和 Serializable ,Deque是一個雙端佇列,說明在LinkedList中同時也支援對佇列的操作。

LinkedList的屬性只有三個:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;
複製程式碼

頭結點,尾結點,連結串列中的元素數量,三者都是被 transient關鍵字修飾的,有小夥伴不理解這個關鍵字的,歡迎看我上上篇文章:面試問你java中的序列化怎麼答?

Node是它的一個內部類,用來儲存資料:

   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;
        }
    }
複製程式碼

接下來,我們看下核心的 add方法,這次我還是在每行程式碼的上面新增上我的註釋,幫助大家能夠更好的理解。因為我的讀者水平不一,所以必須要照顧到所有人:

  /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
        /**
     * Links e as last element.
     */
    void linkLast(E e) {
        //儲存 last 尾結點
        final Node<E> l = last;
        //將要儲存的元素放到 新建一個結點中,
        final Node<E> newNode = new Node<>(l, e, null);
        // 這樣這個新的節點就變成 了尾結點
        last = newNode;
        // 判斷下如果這個尾結點為空,就說明這個連結串列是空的
        //那麼這個新的結點就是 首結點。
        if (l == null)
            first = newNode;
            //如果不是空的,那麼之前舊的尾結點的 next 儲存的就是這個新結點
        else  
            l.next = newNode;
        size++;
        modCount++;
    }
複製程式碼

接下來,addAll 方法有兩個過載函式,前一個是呼叫的後一個,所以我們只說一個:

public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    public boolean addAll(int index, Collection<? extends E> c) {
       //首先進行下標合理性檢查,下面有這個方法
        checkPositionIndex(index);
        //將集合轉換為 Object 陣列
        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;
        //定義下標位置的前置結點和後繼結點
        Node<E> pred, succ;
        if (index == size) {
         //從尾部新增,前置結點是 之前的尾結點,後繼結點為null
            succ = null;
            pred = last;
        } else {
        //從指定位置新增,後繼結點是下標是index的結點;
        //前置結點是下標位置的前一個結點
            succ = node(index);
            pred = succ.prev;
        }
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
            //如果插入位置在頭部
                first = newNode;
            else
            //非空連結串列插入
                pred.next = newNode;
            pred = newNode;  //更新前置結點為最新的結點
        }
        if (succ == null) {
        //如果是從尾部插入的,插入完成後重置尾結點
            last = pred;
        } else {
        //如果不是尾部,那麼把之前的資料和尾部連線起來
            pred.next = succ;
            succ.prev = pred;
        }
        //集合的原來數量+新集合的數量
        size += numNew;
        modCount++;
        return true;
    }
        private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
複製程式碼

我們再說下根據下標來獲取元素的方法 get

/**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
    //元素的下標檢查
        checkElementIndex(index);
        return node(index).item;
    }
       /**
     * Returns the (non-null) Node at the specified element index.
     */
    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;
        }
    }
複製程式碼

從程式碼中可以看得出來,連結串列獲取指定元素效率還是很低的,需要將元素遍歷才能找到目標元素。需要的時間複雜度是O(n)的,但是我們在工作中不能僅僅利用複雜度分析就決定使用哪個資料結構。

陣列簡單易用,在實現上使用的是連續的記憶體空間,可以通過CPU的快取機制,來實現預讀,訪問速度會比較快。而連結串列,由於記憶體不是連續的,所以不能通過這種方法來實現預讀。連結串列本身沒有大小限制,天然支援動態擴容,這也是和陣列最大的區別。

如果你的程式對記憶體使用要求很高,那麼就可以選擇陣列,因為連結串列中的每一個結點都需要消耗額外的記憶體去儲存指向下一個結點的指標,所以記憶體消耗會翻倍,而且對於連結串列的頻繁刪除和插入,會導致頻繁的記憶體申請和釋放,造成記憶體碎片,就會引起頻繁的GC(Garbage Collection 垃圾回收)。

這次只分析了這幾個比較核心的方法原始碼,大家也可以自己嘗試著去看看原始碼,學習下JDK中程式碼的風格,多思考下為什麼要這麼寫,相信會有不少的進步。

簡單分析完之後,我們總結下問題吧。

ArrayList和LinkedList的區別是什麼?(老經典的面試題)

歡迎大家能在留言區中留言,說出你的答案。

如果對本文有任何異議或者說有什麼好的建議,可以加我好友(有沒有問題都歡迎大家加我好友,公眾號後臺聯絡作者),也可以在下面留言區留言,我會及時修改。希望這篇文章能幫助大家在面試路上乘風破浪。

這樣的分享我會一直持續,你的關注、轉發和好看是對我最大的支援,感謝。關注我,我們一起成長。

關注公眾號,最新的文章會出現在那裡哦。

面試問你連結串列和LinkedList怎麼答?

相關文章