二,LinkedList
1, linkedList底層數據結構
linkedList底層是一個雙向鏈表
2,LinkedList和ArrayList的對比
1、順序插入速度ArrayList會比較快,因為ArrayList是基於數組實現的,數組是事先new好的,只要往指定位置塞一個數據就好了;LinkedList則不同,每次順序插入的時候LinkedList將new一個對象出來,如果對象比較大,那么new的時間勢必會長一點,再加上一些引用賦值的操作,所以順序插入LinkedList必然慢於ArrayList
2、基於上一點,因為LinkedList里面不僅維護了待插入的元素,還維護了Entry的前置Entry和后繼Entry,如果一個LinkedList中的Entry非常多,那么LinkedList將比ArrayList更耗費一些內存
3、數據遍歷的速度,看最后一部分,這里就不細講了,結論是:使用各自遍歷效率最高的方式,ArrayList的遍歷效率會比LinkedList的遍歷效率高一些
4、有些說法認為LinkedList做插入和刪除更快,這種說法其實是不准確的:
(1)LinkedList做插入、刪除的時候,慢在尋址,快在只需要改變前后Entry的引用地址
(2)ArrayList做插入、刪除的時候,慢在數組元素的批量copy,快在尋址
所以,如果待插入、刪除的元素是在數據結構的前半段尤其是非常靠前的位置的時候,LinkedList的效率將大大快過ArrayList,因為ArrayList將批量copy大量的元素;越往后,對於LinkedList來說,因為它是雙向鏈表,所以在第2個元素后面插入一個數據和在倒數第2個元素后面插入一個元素在效率上基本沒有差別,但是ArrayList由於要批量copy的元素越來越少,操作速度必然追上乃至超過LinkedList。
從這個分析看出,如果你十分確定你插入、刪除的元素是在前半段,那么就使用LinkedList;如果你十分確定你刪除、刪除的元素在比較靠后的位置,那么可以考慮使用ArrayList。如果你不能確定你要做的插入、刪除是在哪兒呢?那還是建議你使用LinkedList吧,因為一來LinkedList整體插入、刪除的執行效率比較穩定,沒有ArrayList這種越往后越快的情況;二來插入元素的時候,弄得不好ArrayList就要進行一次擴容,記住,ArrayList底層數組擴容是一個既消耗時間又消耗空間的操作,在我的文章Java代碼優化中,第9點有詳細的解讀。
3, 對LinkedList以及ArrayList的迭代
ArrayList使用最普通的for循環遍歷,LinkedList使用foreach循環比較快,看一下兩個List的定義:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
注意到ArrayList是實現了RandomAccess接口而LinkedList則沒有實現這個接口,關於RandomAccess這個接口的作用,看一下JDK API上的說法:
為此,我寫一段代碼證明一下這一點,注意,雖然上面的例子用的Iterator,但是做foreach循環的時候,編譯器默認會使用這個集合的Iterator,測試代碼如下:
public class TestMain
{
private static int SIZE = 111111;
private static void loopList(List<Integer> list)
{
long startTime = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++)
{
list.get(i);
}
System.out.println(list.getClass().getSimpleName() + "使用普通for循環遍歷時間為" +
(System.currentTimeMillis() - startTime) + "ms");
startTime = System.currentTimeMillis();
for (Integer i : list)
{
}
System.out.println(list.getClass().getSimpleName() + "使用foreach循環遍歷時間為" +
(System.currentTimeMillis() - startTime) + "ms");
}
public static void main(String[] args)
{
List<Integer> arrayList = new ArrayList<Integer>(SIZE);
List<Integer> linkedList = new LinkedList<Integer>();
for (int i = 0; i < SIZE; i++)
{
arrayList.add(i);
linkedList.add(i);
}
loopList(arrayList);
loopList(linkedList);
System.out.println();
}
}
我截取三次運行結果:
ArrayList使用普通for循環遍歷時間為6ms
ArrayList使用foreach循環遍歷時間為12ms
LinkedList使用普通for循環遍歷時間為38482ms
LinkedList使用foreach循環遍歷時間為11ms
ArrayList使用普通for循環遍歷時間為5ms
ArrayList使用foreach循環遍歷時間為12ms
LinkedList使用普通for循環遍歷時間為43287ms
LinkedList使用foreach循環遍歷時間為9ms
ArrayList使用普通for循環遍歷時間為4ms
ArrayList使用foreach循環遍歷時間為12ms
LinkedList使用普通for循環遍歷時間為22370ms
LinkedList使用foreach循環遍歷時間為5ms
有了JDK API的解釋,這個結果並不讓人感到意外,最最想要提出的一點是:如果使用普通for循環遍歷LinkedList,在大數據量的情況下,其遍歷速度將慢得令人發指。具體原因如下:
當使用普通for循環時, 其實使用的是LinkedList中的get的實現:
由於LinkedList是雙向鏈表,因此第6行的意思是算出i在一半前還是一半后,一半前正序遍歷、一半后倒序遍歷,這樣會快很多,當然,先不管這個,分析一下為什么使用普通for循環遍歷LinkedList會這么慢。
原因就在第7第8行,第10第11行的兩個for循里面,以前者為例:
1、get(0),直接拿到0位的Node0的地址,拿到Node0里面的數據
2、get(1),直接拿到0位的Node0的地址,從0位的Node0中找到下一個1位的Node1的地址,找到Node1,拿到Node1里面的數據
3、get(2),直接拿到0位的Node0的地址,從0位的Node0中找到下一個1位的Node1的地址,找到Node1,從1位的Node1中找到下一個2位的Node2的地址,找到Node2,拿到Node2里面的數據。
后面的以此類推。
也就是說,LinkedList在get任何一個位置的數據的時候,都會把前面的數據走一遍。假如我有10個數據,那么將要查詢1+2+3+4+5+5+4+3+2+1=30次數據,相比ArrayList,卻只需要查詢10次數據就行了,隨着LinkedList的容量越大,差距會越拉越大。其實使用LinkedList到底要查詢多少次數據,大家應該已經很明白了,來算一下:按照前一半算應該是(1 + 0.5N) * 0.5N / 2,后一半算上即乘以2,應該是(1 + 0.5N) * 0.5N = 0.25N2 + 0.5N,忽略低階項和首項系數,得出結論,LinikedList遍歷的時間復雜度為O(N2),N為LinkedList的容量。