上篇文章說了下,數據和ArrayList,這篇文章咱們說下在面試中有很大機率二者做爲兄弟同時出現的LinkedList,但願你們看完這篇文章後可以有恍然大悟的感受。java
LinkedList底層是鏈表實現的,那麼咱們首先說下什麼是鏈表。node
和上篇文章的數組相比,鏈表要相對於更復雜一點,二者也是很是基礎、經常使用,並且在面試中同時出現的機率也是很大的。面試
上篇文章咱們說到,數據是須要連續的內存空間來存儲的,而鏈表恰好與它相反,鏈表是不須要連續的內存空間的,它是經過將好多的零散的內存使用「指針」串聯起來使用,若是數組和鏈表都想在計算機中申請大小爲10M的內存,而計算機中只有10M的零散內存,那麼數組就會申請內存失敗,鏈表就會成功。數組
想對數組和ArrayList有多些瞭解的小夥伴能夠看個人上篇文章 :緩存
[juejin.im/post/5c99c7…]bash
既然鏈表在內存中都是零散的塊,那麼咱們是怎麼稱呼這些小塊塊呢?數據結構
咱們把這些塊叫作「結點」,爲了把每一個結點都串聯起來,結點中不只會存儲數據,還有存儲下一個結點的地址。app
那咱們怎麼稱呼記錄下個結點地址的指針呢?函數
咱們把他們叫作「後繼指針next」。post
鏈表中最特殊的是頭結點和尾結點,顧名思義,也就是鏈表的第一個結點和最後一個結點,第一個結點保存着鏈表的基地址,經過這個結點,咱們就能定位到它,最後一個結點的指針指向的是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的區別是什麼?(老經典的面試題)
歡迎你們能在留言區中留言,說出你的答案。
若是對本文有任何異議或者說有什麼好的建議,能夠加我好友(有沒有問題都歡迎你們加我好友,公衆號後臺聯繫做者),也能夠在下面留言區留言,我會及時修改。但願這篇文章能幫助你們在面試路上乘風破浪。
這樣的分享我會一直持續,你的關注、轉發和好看是對我最大的支持,感謝。關注我,咱們一塊兒成長。
關注公衆號,最新的文章會出如今那裏哦。