文章目錄
(一)複雜度分析:ArrayList
(二)複雜度分析:LinkedList
(三)均攤複雜度
(四)ArrayList的縮容
(五)複雜度震盪
(一)複雜度分析:ArrayList
- 查:
get(int index)
方法的時間複雜度是O(1)
數組的特點:隨機訪問(無論訪問哪個元素)速度非常快@Override public E get(int index) { rangeCheck(index); //假設存放的是int類型,地址值=index*4+數組的首地址 return elements[index]; }
- 改:
set(int index, E element)
方法的時間複雜度是O(1)@Override public E set(int index, E element) { rangeCheck(index); E old = elements[index]; elements[index] = element;//存儲也是先計算地址,再存,也很快 return old; }
- 增:
add(int index, E element)
方法的時間複雜度如下:
O(n)的n是數據規模,此處的數據規模是size
數組執行add方法需要向右挪動index之後的元素- 最好情況複雜度(index=size):O(1)
- 最壞情況複雜度(index=0):O(n)
- 平均情況複雜度(0<index<size):O[(1+2+…+n)/n],相當於O(n/2),即O(n)
@Override public void add(int index, E element) { if (element == null) return;//加上就不可以存儲null rangeCheckForAdd(index); ensureCapacity(size + 1);//capacity取值爲size+1,就是保證比size多一個,不怕不夠用 for (int i = size; i > index; i--) { elements[i] = elements[i - 1]; } elements[index] = element; size++; }
- 刪:
remove(int index)
方法的時間複雜度如下:
數組執行remove方法需要向左挪動index之後的元素- 最好情況複雜度(index=size):O(1)
- 最壞情況複雜度(index=0):O(n)
- 平均情況複雜度(0<index<size): O(n)
@Override public E remove(int index) { rangeCheck(index); E old = elements[index]; for (int i = index + 1; i < size; i++) { elements[i - 1] = elements[i]; } elements[--size] = null; return old; }
總結:數組增刪慢,查詢、修改快
(二)複雜度分析:LinkedList
- 查:
get(int index)
方法的時間複雜度如下:- 最好情況複雜度:O(1)
- 最壞情況複雜度:O(n)
- 平均情況複雜度: O(n)
@Override public E get(int index) { return node(index).element; } private Node<E> node(int index) { rangeCheck(index); Node<E> node = first; //從first開始,next index次 就可以找到要找的結點 for (int i = 0; i < index; i++) { node = node.next; } return node; }
- 改:
set(int index, E element)
方法的時間複雜度如下:- 最好情況複雜度:O(1)
- 最壞情況複雜度:O(n)
- 平均情況複雜度: O(n)
@Override public E set(int index, E element) { Node<E> node = node(index); E old = node.element; node.element = element; return old; } private Node<E> node(int index) { rangeCheck(index); Node<E> node = first; //從first開始,next index次 就可以找到要找的結點 for (int i = 0; i < index; i++) { node = node.next; } return node; }
- 增:
add(int index, E element)
方法的時間複雜度如下:
網上常說的鏈表的插入時間複雜度是O(1)指的是插入那一瞬間的複雜度是O(1),整體的複雜度還是O(n),因爲同樣調用了node(int index)
方法- 最好情況複雜度:O(1)
- 最壞情況複雜度:O(n)
- 平均情況複雜度: O(n)
@Override public void add(int index, E element) { rangeCheckForAdd(index); //要特殊處理index=0的情況 if (index == 0) { first = new Node<>(element, first); } else { Node<E> prev = node(index - 1); prev.next = new Node<>(element, prev.next); } size++; }
- 刪:
remove(int index)
方法的時間複雜度如下:
網上常說的鏈表的刪除時間複雜度是O(1)指的是插入那一瞬間的複雜度是O(1),整體的複雜度還是O(n),因爲同樣調用了node(int index)
方法- 最好情況複雜度:O(1)
- 最壞情況複雜度:O(n)
- 平均情況複雜度: O(n)
@Override public E remove(int index) { rangeCheck(index); Node<E> node = first; if (index == 0) { first = first.next; } else { Node<E> prev = node(index - 1); node = prev.next; // prev.next = prev.next.next; prev.next = node.next; } size--; return node.element; }
注意:鏈表的增刪的瞬間,不用像數組那樣挪動元素,但是從整體上來看鏈表的時間複雜度還是O(n)
鏈表的優點:省內存(用一個就創建一個,不像數組動態創建,1.5倍擴容)
(三)均攤複雜度
下面分析AbstractList類中add(E element)
方法的時間複雜度
最好情況複雜度:O(1)
最壞情況複雜度:O(n)(容量不夠需要擴容時)
平均情況複雜度:O(1)(因爲大多數情況都是O(1),(1+1+1+1…+n)/n,所以平均下來也是O(1))
均攤複雜度:O(1)(絕大多數都是O(1),只有個別少數是O(n)的情況用均攤複雜度更合適)
什麼情況下適合使用均攤複雜度?
經過連續的多次複雜度比較低的情況後,出現個別複雜度比較高的情況
(四)ArrayList的縮容
如果內存使用比較緊張,動態數組有比較多的剩餘空間,可以考慮進行縮容操作
(比如剩餘空間佔總容量的一半時,就進行縮容)
縮容和擴容很類似,只是縮容是申請一塊小一點的內存空間,然後把數組拷貝過來
我們之前是在add時增加擴容操作,現在應該在remove時增加縮容操作
我們是在add之前判斷是否要擴容,現在應該在remove之後判斷是否縮容
代碼如下:
測試效果如下:
clear()
方法也可以縮容,可以直接創建一個默認長度的數組,如下:
(五)複雜度震盪
如果擴容倍數、縮容時機設計不得當,有可能會導致複雜度震盪
- 我們現在的擴容倍數是1.5倍,如下:
- 我們現在的縮容時機是剩餘容量大於總容量一半,如下:
接下來把擴容倍數設定爲2倍,如下:
讓縮容時機的判斷語句在size = newCapacity時也爲真
效果如下:
假設數組的默認長度爲4,使用普通的add(E element)
方法增加元素,複雜度爲O(1)
再一次添加元素時需要擴容,複雜度爲O(n)
如果此時刪掉剛纔添加的元素
需要縮容,複雜度爲O(n)
此時又增加一個元素,又要擴容,複雜度又爲O(n)…
如果一直循環這樣的操作,複雜度一直維持在O(n),這種情況叫做複雜度震盪(之前一直是O(1),後面突然維持O(n))
可見如果擴容倍數、縮容時機設計不得當,有可能會導致複雜度震盪
解決方案:讓擴容和縮容的幅度相乘不等於1即可
我們上述的情況是2.0(擴容幅度)× 0.5(縮容幅度)= 1.0,剛好等於1