ArrayList和LinkedList優缺點對比,實際與你背的不一樣!


arraylist和linkedlist有什么特點?我相信基本准備過或者說學習過的人應該都對答如流吧,底層實現,數據結構,數組,鏈表,查找效率,增刪效率等等,這些基本上搜索引擎可以隨便找到,而且基本上所有的文章差不多都是那點兒貨,大家也把這些東西奉若真理,人雲亦雲,其實只需要非常簡單的代碼就可以測試這個結論到底對不對。

實現原理

簡單的說一下實現原理吧,有助於下面內容的展開,簡單帶過:ArrayList與LinkedList都是List接口實現類

  • ArrayList:
    雖然叫list,底層是通過動態數組來實現,也就是一塊連續的內存,初始化默認大小是10,每次擴容為1.5倍

  • LinkedList
    java中的鏈表,內部定義了Node類,類中item用於存儲數據,兩個Node類分別為prev和next來實現雙向鏈表,每次新增元素時new Node()

目前熟知的優缺點

  • ArrayList
    優點:由於底層實現是數組,查找效率為O(1)
    缺點:插入和刪除操作需要移動所有受影響的元素,效率相對較低

  • LinkedList
    優點:插入和刪除時只需要添加或刪除前后對象的引用,插入較快
    缺點:在內存中存儲不連續,只能通過遍歷查詢,效率相對較低

測試

首先由於數組的特性,所以插入操作的話我們分為三種情況:從頭部插入,從中間插入和從尾部插入

**測試結果肯定機器性能和當時運行狀況的影響,由於只是簡單的測試,並不需要非常復雜嚴謹的測試策略,我這里基本也就是一個情況跑5~10次,而且代碼也只是簡單的單線程循環插入操作,大體測試結果和趨勢是沒問題的

 1    @Test  2     public void addFromHeaderTestArray() {  3         ArrayList<Integer> list = new ArrayList<>();  4         int i = 0;  5 
 6         long timeStart = System.nanoTime();  7 
 8         while (i < 100) {  9             list.add(0, i); 10             i++; 11  } 12         long timeEnd = System.nanoTime(); 13         System.out.println("ArrayList cost" + (timeEnd - timeStart)); 14  } 15 
16  @Test 17     public void addFromHeaderTestLinked() { 18         LinkedList<Integer> list = new LinkedList<>(); 19         int i = 0; 20         long timeStart = System.nanoTime(); 21         while (i < 100) { 22  list.addFirst(i); 23             i++; 24  } 25         long timeEnd = System.nanoTime(); 26 
27         System.out.println("LinkedList cost" + (timeEnd - timeStart)); 28     }

 

代碼比較簡單,只貼出來一段了,每次改一下循環次數和add方法就可以了,以下時間單位均為納秒

  • 頭部插入
    測試結果(100條):
name/times 1 2 3 4 5
ArrayList 88300 84900 101700 93800 88100
LinkedList 68400 71200 88600 93300 96000

只測試了100條的情況,結果符合預期,這里我特意放了一條特殊情況,因為我們的實驗確實比較簡單不夠復雜和系統,結果跟當時電腦的運行狀況有關,但是我們這里只是插入100條,如果你換成10000條,你就會發現差距相當明顯了,而且不管跑多少次,都不會出現ArrayList更快的情況,這里具體結果不貼了,有興趣自己跑一下,結論是沒問題的。

  • 中間插入
1        while (i < 100) { 2             int temp = list.size(); 3             list.add(temp / 2, i); 4             i++; 5         }

測試結果(100條):

|name/times|1|2|3|4|5|
|—|
|ArrayList|128300|92800|106300|77600|90700|
|LinkedList|175100|210900|164200|164200|195700|

測試結果(10000條):

name/times 1 2 3 4
ArrayList 9745300 10319900 10986800 11696600
LinkedList 66968400 63269400 70954900 65432600

這次中間插入分別測試了100條和10000條,是不是有點兒慌了?怎么和自己學的不一樣了?從中間插入居然ArrayList更快?

  • 尾部插入
1      while (i < 10000) { 2  list.add(i); 3            i++; 4        }

測試結果(100條):

name/times 1 2 3 4 5
ArrayList 32100 23600 23500 27800 16100
LinkedList 72200 73400 70200 74800 90000

結果已經很明顯了,只貼了100條的數據,有興趣可以自己試試,插入更多條也還是ArrayList更快,怎么樣?看到這里是不是顛覆認知了

測試結果
頭部插入:ArrayList>LinkedList
中間插入:ArrayList<LinkedList
尾部插入:ArrayList<LinkedList (差不多,AL稍微快一點;對於尾部插入而言,ArrayList 與 LinkedList 的性能幾乎是一致的。)

結果分析

  • 頭部插入:由於ArrayList頭部插入需要移動后面所有元素,所以必然導致效率低
  • 中間插入:查看源碼會注意到LinkedList的中間插入其實是先判斷插入位置距離頭尾哪邊更接近,然后從近的一端遍歷找到對應位置,而ArrayList是需要將后半部分的數據復制重排,所以兩種方式其實都逃不過遍歷的操作,相對效率都很低,但是從實驗結果我們可以看到還是ArrayList更勝一籌,我猜測這與數組在內存中是連續存儲有關
  • 尾部插入:ArrayList並不需要復制重排數據,所以效率很高,這也應該是我們日常寫代碼時的首選操作,而LinkedList由於還需要new對象和變換指針,所以效率反而低於ArrayList

刪除操作這里不做實驗了,但是可以明確的告訴大家,結果是一樣的,因為邏輯和添加操作並沒有什么區別

數組擴容
再說一下數組擴容的問題,雖然ArrayList存在擴容的效率問題,但這只是在容量較小的時候,假如初始是10,第一次擴容結束是15,沒有增加太多,但是如果是10000,那么擴容后就到了15000,實際上越往后每次擴容對后續性能影響越小,而且即便存在擴容問題,實驗結果表明還是優於LinkedList的。

 

 

LinkedList源碼

 1 // 在index前添加節點,且節點的值為element
 2 public void add(int index, E element) {  3     addBefore(element, (index==size ? header : entry(index)));  4 }  5 
 6 // 獲取雙向鏈表中指定位置的節點
 7 private Entry<E> entry(int index) {  8     if (index < 0 || index >= size)  9         throw new IndexOutOfBoundsException("Index: "+index+
10                                             ", Size: "+size); 11     Entry<E> e = header; 12     // 獲取index處的節點。 13     // 若index < 雙向鏈表長度的1/2,則從前向后查找; 14     // 否則,從后向前查找。
15     if (index < (size >> 1)) { 16         for (int i = 0; i <= index; i++) 17             e = e.next; 18     } else { 19         for (int i = size; i > index; i--) 20             e = e.previous; 21  } 22     return e; 23 } 24 
25 // 將節點(節點數據是e)添加到entry節點之前。
26 private Entry<E> addBefore(E e, Entry<E> entry) { 27     // 新建節點newEntry,將newEntry插入到節點e之前;並且設置newEntry的數據是e
28     Entry<E> newEntry = new Entry<E>(e, entry, entry.previous); 29     // 插入newEntry到鏈表中
30     newEntry.previous.next = newEntry; 31     newEntry.next.previous = newEntry; 32     size++; 33     modCount++; 34     return newEntry;
  }

 

從中,我們可以看出:通過add(int index, E element)向LinkedList插入元素時。先是在雙向鏈表中找到要插入節點的位置index;找到之后,再插入一個新節點
雙向鏈表查找index位置的節點時,有一個加速動作若index < 雙向鏈表長度的1/2,則從前向后查找; 否則,從后向前查找

接着,我們看看ArrayList.java中向指定位置插入元素的代碼。如下:

 1 // 將e添加到ArrayList的指定位置
 2 public void add(int index, E element) {  3     if (index > size || index < 0)  4         throw new IndexOutOfBoundsException(  5         "Index: "+index+", Size: "+size);  6 
 7     ensureCapacity(size+1);  // Increments modCount!!
 8     System.arraycopy(elementData, index, elementData, index + 1,  9          size - index); 10     elementData[index] = element; 11     size++; 12 }

ensureCapacity(size+1) 的作用是“確認ArrayList的容量,若容量不夠,則增加容量。
真正耗時的操作是 System.arraycopy(elementData, index, elementData, index + 1, size - index);

 

實際上,我們只需要了解: System.arraycopy(elementData, index, elementData, index + 1, size - index); 會移動index之后所有元素即可這就意味着,ArrayList的add(int index, E element)函數,會引起index之后所有元素的改變!

插入位置的選取對LinkedList有很大的影響,一直往數據中間部分與尾部插入刪除的時候,ArrayList比LinkedList更快

原因大概就是當數據量大的時候,system.arraycopy的效率要比每次插入LinkedList都需要從兩端查找index和分配節點node來的更快。

 

LinkedList 雙向鏈表實現

這里寫圖片描述

可以看到, LinkedList 的成員變量只有三個:

  • 頭節點 first
  • 尾節點 last
  • 容量 size

節點是一個雙向節點: 
這里寫圖片描述

用一副圖表示節點:

這里寫圖片描述

雙向鏈表的底層結構圖:

這里寫圖片描述

每個節點元素里包含三部分:前一個節點元素地址,下一個節點元素地址,當前節點元素內容

 

LinkedList 特點

 

  • 雙向鏈表實現
  • 元素時有序的,輸出順序與輸入順序一致
  • 允許元素為 null
  • 所有指定位置的操作都是從頭開始遍歷進行的
  • 和 ArrayList 一樣,不是同步容器

 

並發訪問注意事項

 

linkedList 和 ArrayList 一樣,不是同步容器。所以需要外部做同步操作,或者直接用 Collections.synchronizedList 方法包一下,最好在創建時就包一下:

List list = Collections.synchronizedList(new LinkedList(...));

 

 

Thanks:

https://blog.csdn.net/zycR10/article/details/99775821?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

https://www.cnblogs.com/syp172654682/p/9817277.html

深入理解linkedlist: https://blog.csdn.net/u011240877/article/details/52876543

blog.csdn.net/dearKundy/article/details/84663512

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM