參考博客:java
blog.csdn.net/kangxidageg…node
在java中,Collection是必須瞭解的。數組
Collection主要爲兩大分類,一是鏈表List,一是集合Set(固然還有其餘分支)。如今咱們把注意力放到鏈表List上面來,鏈表List是咱們再平常的開發中常常會用到的數據結構。安全
List主要分爲三大類,ArrayList,LinkedList,Vector。那麼,接下來咱們來了解這三個數據結構。數據結構
ArrayList是咱們最經常使用的數據結構,可是,咱們有沒有去真正的瞭解這個數據結構呢,有沒有去了解一下這個數據結構的內部源碼呢? 函數
從這部分的源碼能夠看出,ArrayList是List的實現類,也是能夠序列化的,並且它的初始容量是10。 從這部分能夠看出,ArrayList是能夠在建立的時候指定集合的長度的。List<String> list1=new ArrayList<>();
List<String> list2=new ArrayList<>(20);
複製代碼
咱們能夠看到,list2就是我指定長度爲20建立的。讓咱們繼續來看源碼。 源碼分析
經過addAll()方法,咱們來追蹤到了當容量不夠的時候,ArrayList的擴容方法,也就是如圖的grow()方法。 代碼以下:private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼
從代碼中咱們能夠看出,參數minCapacity是當前集合須要的容量,參數oldCapacity是以前集合的容量,參數newCapacity是根據舊的集合容量擴充後的容量長度,擴充方法是:spa
int newCapacity = oldCapacity + (oldCapacity >> 1)
複製代碼
是經過二進制的位移方法計算的,當參數oldCapacity爲10的時候,也就是「1010」,向右位移一位ie,變成了「101」,換算成10進制,那就是5,也就是說,當oldCapacity爲10的時候,通過計算後的newCapacity是變成了15,這也就是咱們常常說的,ArrayList的擴充方式是擴充1.5倍。.net
固然,實際的擴充方法沒有說的這麼簡單,咱們發現當發現咱們擴充後的容量仍是小於須要的容量的時候,咱們直接將須要的容量設置爲當前的集合的容量大小,這個時候,是沒有遵循1.5倍的擴充邏輯的。
並且,ArrayList集合的容量大小,實際上是不能大於「Integer.MAX_VALUE - 8」的(也就是int的最大值減去8),當大於這個值了,那麼就會設置當前集合的容量爲Integer.MAX_VALUE。而到這裏,也是沒有遵循ArrayList的1.5倍擴充機制的。
Vector實際上是和ArrayList的模式有極多類似之處,經過源碼咱們能夠來分析一下:
在這裏,咱們看到了,雖然Vector和ArrayList有不少類似的地方,可是Vector在這裏有了一個參數是ArrayList沒有的,就是參數capacityIncrement。參數capacityIncrement是Vector的擴容增長量,是能夠人爲添加修改的,初始化是在Vector的構造函數裏面,以下圖:
而這個參數,是ArrayList裏面沒有的。固然,ArrayList中一樣也有Vector中沒有的參數,Vector是沒有默認容量長度這個參數的,由於當你寫代碼的時候,以下:
List<String> list=new Vector<>();
複製代碼
這個時候,雖然咱們沒有設置Vector的默認長度,可是咱們深刻到源碼中去看,發現實際調用了Vector的另一個構造函數,給Vector設置默認的容量長度10。源碼以下:
接下來,讓咱們來分析一下Vector的擴容機制吧。在上面,咱們有說到,Vector是能夠本身設置擴容增長量的,那麼,咱們跟蹤一下代碼,來看看Vector的擴容機制吧。
在這段源碼中,咱們能夠看到,這個方法是由synchronized修飾的,也就是說Vector是線程安全的。不過咱們暫時不討論這點,繼續往下看。 好的,咱們將擴容的方法grow()的代碼複製過來,以下:private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼
參數capacityIncrement就是咱們設置的擴容增長量,當咱們給capacityIncrement賦值了,那麼擴容的時候就根據咱們的設置擴容,不然就直接翻倍。
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
複製代碼
後面的代碼咱們不作詳細分析了,由於是和ArrayList裏的擴容機制是同樣的。
LinkedList和ArrayList和Vector是徹底不一樣的,在以前的源碼分析中咱們能夠看到,ArrayList和Vector的底層都是數組,而LinkedList的底層是大相徑庭的,它底層是鏈表,那麼咱們經過源碼來了解一下LinkedList吧。
這段源碼中出現的三個參數,都是由transient修飾的,也就是表明着不能被序列化。 看到這裏,咱們發現LinkedList的底層是一個雙向鏈表,由於它一個節點包含了兩個指針,一個前指針,指向前一個元素,一個後指針,指向後一個元素。還有一部分關於構造函數的和其餘添加刪除的方法函數,我就不展示源碼給你們了,一個帶參數的構造,一個不帶參數的構造,你們能夠本身去看看源碼。
ArrayList是底層是由可變長度數組(當元素個數超過數組的長度時,會產生一個新的數組,將原數組的數據複製到新數組,再將新的元素添加到新數組中。)組成的,初始容量爲10,擴充通常擴充1.5倍。它是線程不安全的,查詢速度比較快,刪減增長速度比較慢。
Vector的底層也是由可變長度數組(當元素個數超過數組的長度時,會產生一個新的數組,將原數組的數據複製到新數組,再將新的元素添加到新數組中。)組成的,初始容量也是10,擴充通常默認狀況下擴充爲2倍。它是線程安全的,它大部分的方法都包含關鍵字synchronized,因此不論是查詢速度仍是刪減增長速度都是比較慢。
ArrayList和Vector中,從指定的位置檢索一個對象,或在集合的末尾插入、刪除一個元素的時間是同樣的,時間複雜度都是O(1)。可是若是在其餘位置增長或者刪除元素花費的時間是O(n)。
LinkedList的底層是雙向鏈表,是線程不安全的。
LinkedList中,在插入、刪除任何位置的元素所花費的時間都是同樣的,時間複雜度都爲O(1),可是他在檢索一個元素的時間複雜度爲O(n)
在描述算法複雜度時,常常用到o(1), o(n), o(logn), o(nlogn)來表示對應算法的時間複雜度, 這裏進行概括一下它們表明的含義: 這是算法的時空複雜度的表示。不只僅用於表示時間複雜度,也用於表示空間複雜度。
O(n):O後面的括號中有一個函數,指明某個算法的耗時/耗空間與數據增加量之間的關係。其中的n表明輸入數據的量。 表明數據量增大幾倍,耗時也增大幾倍。好比常見的遍歷算法。
O(1):就是最低的時空複雜度了,也就是耗時/耗空間與輸入數據大小無關,不管輸入數據增大多少倍,耗時/耗空間都不變。 哈希算法就是典型的O(1)時間複雜度,不管數據規模多大,均可以在一次計算後找到目標(不考慮衝突的話)
數組靜態分配內存。
缺點:在內存中,數組是一塊連續的區域。數組須要預留空間,每次申請數組以前必須規定數組的大小,若是大小不合理,則可能會浪費內存。並且它對內存空間要求高,必須有足夠的連續內存空間。並且數組大小固定,不能動態拓展。
優勢:數組利用下標定位,時間複雜度爲O(1),數組插入或刪除元素的時間複雜度O(n)。
鏈表動態分配內存。
優勢:鏈表在內存中是不連續的,插入刪除速度快(由於有next指針指向其下一個節點,經過改變指針的指向能夠方便的增長刪除元素)。 內存利用率高,不會浪費內存(可使用內存中細小的不連續空間(大於node節點的大小),而且在須要空間的時候才建立空間)。 大小沒有固定,拓展很靈活。
缺點:不能隨機查找,必須從第一個開始遍歷,查找效率低。
單鏈表只有一個指向下一結點的指針,也就是隻能next。 雙鏈表除了有一個指向下一結點的指針外,還有一個指向前一結點的指針,能夠經過prev()快速找到前一結點,顧名思義,單鏈表只能單向讀取。
雙鏈表的優勢:一、刪除單鏈表中的某個結點時,必定要獲得待刪除結點的前驅,獲得該前驅有兩種方法,第一種方法是在定位待刪除結點的同時一路保存當前結點的前驅。第二種方法是在定位到待刪除結點以後,從新從單鏈表表頭開始來定位前驅。儘管一般會採用方法一。但其實這兩種方法的效率是同樣的,指針的總的移動操做都會有2*i次。而若是用雙向鏈表,則不須要定位前驅結點。所以指針總的移動操做爲i次。
二、查找時也同樣,咱們能夠借用二分法的思路,從head(首節點)向後查找操做和last(尾節點)向前查找操做同步進行,這樣雙鏈表的效率能夠提升一倍。
爲何目前市場應用上單鏈表的應用要比雙鏈表的應用要普遍的多呢?
從存儲結構來看,每一個雙鏈表的節點要比單鏈表的節點多一個指針,而長度爲n就須要 n*length(這個指針的length在32位系統中是4字節,在64位系統中是8個字節) 的空間,這在一些追求時間效率不高應用下並不適應,由於它佔用空間大於單鏈表所佔用的空間;這時設計者就會採用以時間換空間的作法,使用單鏈表,這時一種工程整體上的衡量。