上文講解了ArrayList的底層實現原理,感興趣的小夥伴可以去看下,本文重點討論LinkedList集合。
首先說下ArrayList和LinkedList的區別:(相同點都是有序的~)
① ArrayList底層數據結構是動態數組,LinkedList底層數據結構是雙向鏈表。
② 查詢或者修改的時候,ArrayList比LinkedList的效率更高,因爲LinkedList是線性的基於鏈表的數據存儲方式,所以需要移動指針從前往後依次查找,即使源碼中(下面會詳細說明)用了二分法,但是效率還是不如ArrayList底層基於數組,直接通過索引定位,效率極快。
③ 增加或者刪除的時候,LinkedList比ArrayList的效率更高,因爲ArrayList是數組,所以在其中進行增刪操作時,會對操作點之後所有數據的下標索引造成影響,需要進行數據的移動,而LinkedList只需要修改prev和next的指針引用即可。
綜上所述,Arraylist適用於查詢或者修改比較多的場景,LinkeList適用於查詢/修改較少,增加和刪除較多的場景。
純手寫LinkedList源碼(白話文分析):
package com.example;
public class MyLinkedList<E> implements MyList<E> {
//E LinkedList 存放的數據類型
/**
* 集合的大小
*/
transient int size = 0;
/**
* 第一個節點
*/
transient Node<E> first;
/**
* 最後一個節點
*/
transient Node<E> last;
@Override
public int size() {
return size;
}
@Override
public boolean add(E e) {
linkLast(e);
return true;
}
@Override
public E get(int index) {
// 檢查我們index是否越界
checkElementIndex(index);
// 通過二分法查找具體Node對象/節點的item
return node(index).item;
}
/** 【刪除原理:改變相互引用的指針】
* 步驟1:當前要刪除Node節點的上一個的next指向當前要刪除節點的下一節點
* 步驟2:當前要刪除Node節點的下一個節點的prev指向當前要刪除節點的上一個節點
* 步驟3:當前要刪除Node節點/對象,所有屬性置爲null,等待gc回收
* @param index
* @return
*/
@Override
public E remove(int index) {
// 檢查我們index是否越界
checkElementIndex(index);
// node(index)首先獲取到當前刪除節點,然後再刪除unlink(),即調整指針位置
return unlink(node(index));
}
/**
* 添加我們的節點,作爲最後一個元素
* @param e
*/
void linkLast(E e) {
// 獲取當前的最後一個節點
final Node<E> l = last;
// 封裝我們當前自定義元素
final Node newNode = new Node<E>(l, e, null);
// 當前新增節點肯定是鏈表中最後一個節點(當前新增節點賦給last)
last = newNode;
if (l == null) //注意,走到這一步last變了,但l沒變
// 如果我們鏈表中沒有最後一個節點說明當前新增的元素是第一個
first = newNode;
else
// 原來的最後一個節點的下一個節點就是當前新增的節點
l.next = newNode;
size++;
}
/**
* 鏈表:其實可以理解爲全表掃描(折半查找)
* 數組:直接通過索引定位,效率極快
* @param index index小於折半值,從頭開始查;index大於折半值,從尾開始查
* @return
*/
Node<E> node(int index) {
/* 舉個例子
現在鏈表中有1-100節點,如果想查第88個節點,正常情況下從1查到88,即索引從0到87
寫JDK的人比較聰明,運用了折半查找(也成爲二分法),查詢步驟如下:
size / 2 = 50,如果88大於50,那麼查50~100就行了(索引49-99)
*/
// size >> 1 → size/2 → if裏面的判斷解析爲 index < size/2
// 假設鏈表中有1-10節點,查詢下標爲0,又因爲0<10/2,所以在0-4之間找
if (index < (size >> 1)) {
// 獲取到第一個節點
Node<E> x = first;
for (int i = 0; i < index; i++) {
// 如果index小於折半值,從頭(0)查詢到index【基於索引】
x = x.next;
}
return x;
} else {
// 獲取到最後一個節點
Node<E> x = last;
for (int i = size - 1; i > index; i--) {
// 如果index大於折半值,從尾(size-1)查詢到index【基於索引】
x = x.prev;
}
return x;
}
// 1-10 | 3 1-3 | 7 10-7
}
/**
* 刪除節點,重新連接鏈表
* @param x 當前刪除的Node節點
* @return
*/
E unlink(MyLinkedList.Node<E> x) {
// 獲取到當前刪除的節點的元素值
E element = x.item;
// 獲取當前刪除元素的下一個節點
Node<E> next = x.next;
// 獲取當前刪除元素的上一個節點
Node<E> prev = x.prev;
if (prev == null) { /** 判斷prev */
// 如果prev爲空,說明當前刪除節點是第一個節點,需要把next置爲第一個節點(first爲全局變量)
first = next;
} else {
// 如果prev不爲空, 上一個Node節點的next指向下一個Node節點
prev.next = next;
// 當前刪除節點的prev變爲空,告訴給gc實現回收
x.prev = null;
}
if (next == null) { /** 判斷next */
// 如果next爲空,說明當前刪除節點是最後一個節點,需要把prev置爲最後一個節點(last爲全局變量)
last = prev;
} else {
// 如果next不爲空,下一個Node節點的prev指向上一個Node節點
next.prev = prev;
// 當前刪除節點的next變爲空 告訴給gc實現回收
x.next = null;
}
// 當前刪除的節點的元素值置爲空
x.item = null;
size--;
return element;
}
/**
* 鏈表中的節點
* @param <E>
*/
private static class Node<E> {
// 節點元素值 zhangsan,lisi..
E item;
// 當前節點的下一個node(節點/對象)
MyLinkedList.Node<E> next;
// 當前節點的上一個node
MyLinkedList.Node<E> prev;
// 使用構造函數傳遞參數
Node(MyLinkedList.Node<E> prev, E element, MyLinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
Node(E element) {
this.item = element;
}
public void setPrev(Node<E> prev) {
this.prev = prev;
}
public void setNext(Node<E> next) {
this.next = next;
}
}
private void checkElementIndex(int index) {
if (!isElementIndex(index)) {
throw new IndexOutOfBoundsException("index已經越界啦~~~");
}
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
public static void main(String[] args) {
Node node1 = new Node("第一關遊戲");
Node node2 = new Node("第二關遊戲");
node1.next = node2;
node2.prev = node1;
System.out.println("node:" + node1);
}
}
在上述main方法最後一行打個斷點,Debug啓動,會發現node1和node2是相互引用的,驗證了LinkedList底層是基於雙向鏈表。
此時,測試一下我們手寫的LinkesList:
package com.example.test;
import com.example.MyLinkedList;
public class Test001 {
public static void main(String[] args) {
MyLinkedList<String> linkedList = new MyLinkedList<>();
linkedList.add("001");
linkedList.add("002");
linkedList.add("003");
linkedList.remove(1);
System.out.println(linkedList.get(1));
}
}
會發現,當我們刪除索引爲1的元素,那麼003則會替代002的位置,該測試把我們的增,刪,查都用上了,改的話無非就是先調用node(即查詢指定索引對應的Node節點),然後替換Node的item屬性爲新元素即可。
【總結】
鏈表數據底層原理實現:雙向鏈表頭尾相接
① 在底層中使用靜態內部類Node節點存放節點元素
三個屬性 prev(關聯的上一個節點),item(當前的值) ,next(下一個節點)
② add原理是如何實現? 答案: 一直在鏈表之後新增
③ get原理:採用折半查找 範圍查詢定位node節點
④ remove原理:改變相互引用的指針