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