Java基礎(二)集合之LinkedList(集合第三篇)
1、LinkedList底層實現原理
LinkedList底層的資料結構是基於雙向迴圈連結串列的,且頭結點中不存放資料,如下:
從圖中可以看出,有頭節點和資料節點,頭節點是結構必須的,資料節點即是存放資料的節點。
每一個節點都有三個元素,一個是當前節點的資料,也就是data部分,data左邊就是指向該節點的上一個節點的指標,右邊就是指向該節點的下一個節點的指標。
既然是雙向連結串列,那麼必定存在一種資料結構——我們可以稱之為節點,節點例項儲存業務資料,前一個節點的位置資訊和後一個節點位置資訊,如下圖所示:
(1)、陣列與連結串列的結構比較
陣列是將元素在記憶體中連續存放,由於每個元素佔用記憶體相同,可以通過下標迅速訪問陣列中任何元素。但是如果要在陣列中增加一個元素,需要移動大量元素,在記憶體中空出一個元素的空間,然後將要增加的元素放在其中。同樣的道理,如果想刪除一個元素,同樣需要移動大量元素去填掉被移動的元素。如果應用需要快速訪問資料,很少插入和刪除元素,就應該用陣列。
連結串列中的元素在記憶體中不是順序儲存的,而是通過存在元素中的指標聯絡到一起,每個結點包括兩個部分:一個是儲存資料元素的資料域,另一個是儲存下一個結點地址的指標。
如果要訪問連結串列中一個元素,需要從第一個元素開始,一直找到需要的元素位置。但是增加和刪除一個元素對於連結串列資料結構就非常簡單了,只要修改元素中的指標就可以了。如果應用需要經常插入和刪除元素你就需要用連結串列。
a)、記憶體儲存區別
陣列從棧中分配空間, 對於程式設計師方便快速,但自由度小。
連結串列從堆中分配空間, 自由度大但申請管理比較麻煩.
b)、邏輯結構區別
陣列必須事先定義固定的長度(元素個數),不能
連結串列動態地進行儲存分配,可以適應資料動態地增減的情況,且可以方便地插入、刪除資料項。(陣列中插入、刪除資料項時,需要移動其它資料項)
c)總的來說
1、存取方式上,陣列可以順序存取或者隨機存取,而連結串列只能順序存取;
2、儲存位置上,陣列邏輯上相鄰的元素在物理儲存位置上也相鄰,而連結串列不一定;
3、儲存空間上,連結串列由於帶有指標域,儲存密度不如陣列大;
4、按序號查詢時,陣列可以隨機訪問,時間複雜度為O(1),而連結串列不支援隨機訪問,平均需要O(n)
5、按值查詢時,若陣列無序,陣列和連結串列時間複雜度均為O(1),但是當陣列有序時,可以採用折半查詢將時間複雜度降為O(logn);
6、插入和刪除時,陣列平均需要移動n/2個元素,而連結串列只需修改指標即可;
7、空間分配方面:
陣列在靜態儲存分配情形下,儲存元素數量受限制,動態儲存分配情形下,雖然儲存空間可以擴充,但需要移動大量元素,導致操作效率降低,而且如果記憶體中沒有更大塊連續儲存空間將導致分配失敗;
連結串列儲存的節點空間只在需要的時候申請分配,只要記憶體中有空間就可以分配,操作比較靈活高效;
2、自定義LinkedList
ExtList介面跟 上一篇的一樣 Java基礎(二)集合之ArrayList(集合第二篇)
ExtLinkedList實現類,完全根據原始碼的業務邏輯實現,並添加註釋+自己的理解
public class ExtLinkedList<E> implements ExtList<E>{
/**
* 連結串列的大小
*/
transient int size = 0;
/**
* 指向第一個節點的指標。
*/
transient Node first;
/**
* 指向最後一個節點的指標。
*/
transient Node<E> last;
@Override
public void add(E e) {
//預設在列表的尾部新增元素
linkLast(e);
}
//在指定下標位置新增元素
@Override
public void add(int index, E e) {
//檢查索引下標是否越界
checkPositionIndex(index);
if (index == size)
//如果索引的位置是在連結串列的最後,直接呼叫在尾部新增元素
linkLast(e);
else
//否則,在下標之後插入元素
linkBefore(e, node(index));
}
/**
* 在下標位置插入元素
* @param e 元素
* @param succ 要插入索引位置的節點
*/
void linkBefore(E e, Node<E> succ) {
//獲取原索引位置的節點的上一個節點
final Node<E> pred = succ.prev;
//1、新建一個節點,該節點的左節點指向原索引位置節點的上一個節點,
// 右節點指向原索引位置節點,元素為傳入的元素
final Node<E> newNode = new Node<>(pred, e, succ);
//2、將原索引位置節點的左節點指定為新新增的節點
succ.prev = newNode;
if (pred == null)
//3、如果索引位置節點的上一個節點為空,則是空連結串列,將新新增的節點設定為第一個節點
first = newNode;
else
//4、如果索引位置節點的上一個節點不為空,則將其右節點設定為新新增的節點
pred.next = newNode;
//5、連結串列數量加1
size++;
}
//在列表末尾新增元素
private void linkLast(E e) {
//獲取最後一個節點
final Node<E> l = last;
//1、新建一個節點,將該節點的左節點指向原來的最後一個節點,
// 右節點指向null(最後一個節點都是指向null),元素為傳入元素
final Node<E> newNode = new Node<>(l, e, null);
//2、將當前的連結串列的最後一個節點設定為新新增的節點
last = newNode;
if (l == null)
//3、如果最後一個節點為空,則是空連結串列,將新新增的節點設定為第一個節點
first = newNode;
else
//4、如果最後一個節點不為空,將原來最後一個節點的右節點指標指向新新增的節點
l.next = newNode;
//5、連結串列數量加1
size++;
}
@Override
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//根據傳入物件刪除節點
@Override
public boolean remove(Object o) {
//如果出入物件為空,迴圈刪除連結串列中的空值
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
@Override
public int getSize() {
return size;
}
@Override
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* 自定義Node節點
* @param <E>
*/
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;
}
}
/**
* 返回指定元素索引處的(非null)節點。
*/
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;
}
}
/**
* 取消連結串列非空節點x。
*/
E unlink(Node<E> x) {
final E element = x.item;//當前節點元素
final Node<E> prev = x.prev;//左節點
final Node<E> next = x.next;//右節點
if (prev == null) {
//1、如果上一個節點為空,則是刪除第一個節點,將將連結串列的頭部設定原來第二個節點即可
first = next;
} else {
//2、如果上一個節點不為空,則將要要刪除節點的上一個節點的右節點指向要刪除節點的下一個節點
prev.next = next;
//刪除當前節點的左指標(將當前節點的左節點設定為空)
x.prev = null;
}
if (next == null) {
//3、如果下一個節點為空,則要刪除的節點是最後一個節點,將將連結串列的尾部設定原來倒數第二個節點即可
last = prev;
} else {
//4、如果下一個節點不為空,則要將要刪除節點的下一個節點的左節點指向要刪除節點的上一個節點
next.prev = prev;
//刪除當前節點的右指標(將當前節點的右節點設定為空)
x.next = null;
}
//5、刪除當前節點元素
x.item = null;
//6、連結串列數量減一
size--;
//7、返回刪除的元素
return element;
}
//檢查索引下標是否越界
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//檢查元素索引是否越界
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 判斷引數是否是現有元素的索引。
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* 判斷引數是否是迭代器或新增操作的有效位置的索引。
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
/**
* 丟擲陣列下標越界異常資訊
*/
private String outOfBoundsMsg(int index) {
return "索引位置: "+index+", 陣列大小: "+size;
}
}
測試程式碼:
//測試ArrayList的刪除元素速度
@Test
public void testExtArrayList1(){
ExtList<User> list = new ExtArrayList<>();
for (int i = 0; i < 10000000; i++) {
list.add(new User((long)i,"張三"+i));
}
System.out.println("刪除元素開始時間:"+System.currentTimeMillis());
list.remove(0);
System.out.println("刪除元素結束時間:"+System.currentTimeMillis());
}
//測試LinkedList的刪除元素速度
@Test
public void testExtLInkedList(){
ExtList<User> list = new ExtLinkedList<>();
for (int i = 0; i < 10000000; i++) {
list.add(new User((long)i,"張三"+i));
}
System.out.println("刪除元素開始時間:"+System.currentTimeMillis());
list.remove(0);
System.out.println("刪除元素結束時間:"+System.currentTimeMillis());
}
可以得出結果:
LinkedList的刪除速度比ArrayList快一些。
學習Java基礎知識,做一個簡單的記錄。
上一篇:Java基礎(二)集合之ArrayList(集合第二篇)
下一篇:暫無