1. 程式人生 > >均攤時間複雜度與複雜度震盪

均攤時間複雜度與複雜度震盪

前言

我們平常分析複雜度一般是分析一個演算法從頭執行到尾它的複雜度是怎樣的。但我們在專案中經常會寫一個複雜度較高的演算法,但是這個高複雜度的演算法是為了方便其他操作。此時我們通常會將這個複雜度較高的演算法和其他的操作放在一起來分析複雜度。這個複雜度較高的演算法複雜度將會均攤到其他的操作中。這種複雜度分析法我們就叫做均攤複雜度分析法。最典型的例子就是我們寫一個動態陣列這樣的一個類。動態陣列每新增一個元素,或者刪除一個元素。我們就要用到均攤複雜度分析法。

均攤複雜度案例分析

(1)我們先寫出實現動態陣列MyVector類的基本框架

import java.util.Arrays;
public class MyVector<Item> { private Item[] data; private int size; // 儲存陣列中的元素個數 private int capacity; // 儲存陣列中可以容納的最大的元素個數 public MyVector(){ data = (Item[])new Object[100]; size = 0; capacity = 100; } }

(2)為陣列中新增一個元素

 // 平均複雜度為 O(1)
    public
void push_back(Item e){ if(size == capacity) //元素個數等於陣列容量 resize(2 * capacity); //為陣列擴容將在下面講到 data[size++] = e; }

(3)從陣列中拿出一個元素

   // 平均複雜度為 O(1)
    public Item pop_back(){

        if(size <= 0)
            throw new IllegalArgumentException("can not pop back for empty vector."
); Item ret = data[size-1]; size --; // 在size達到靜態陣列最大容量的1/4時才進行resize,原因請往下看 // resize的容量是當前最大容量的1/2 // 防止複雜度的震盪 if(size == capacity / 4) resize(capacity / 2); return ret; }

(4)當新增,刪除元素,執行到一定時候,我們就要對陣列進行擴容,縮容操作。即resize()方法。它的複雜度是O(n)級別的。

   // 複雜度為 O(n)
    private void resize(int newCapacity){
     if(newCapacity < size)
            throw new IllegalArgumentException("newCapacity can't be less than size.");
        Item[] newData = (Item[])new Object[newCapacity];
        for(int i = 0 ; i < size ; i ++)
            newData[i] = data[i];

        data = newData;
        capacity = newCapacity;
    }

(5)通過對上面的動態陣列的操作為例。我們來進行均攤複雜度分析
1.png

假設我們的陣列長度為n,那麼我們每進行一次新增操作,他的演算法複雜度為O(1)
2.png

當我們新增到最後一個元素時,如果此時再往裡新增一個元素,那麼就將進行陣列的擴容操作。這次擴容的演算法複雜度為O(n),此時我們就要用到均攤複雜度分析法,前面n次操作耗費時間總共為n,第n+1次操作耗費時間為n,相當於執行n+1次操作耗費的時間為2n,那麼平均來看,我們每次操作其實耗費的時間為2,他仍然是一個O(1)級別的演算法。在這裡我把一次線性操作(第n+1次)的複雜度均攤到前面n次操作中。這就是典型的均攤複雜度分析。那麼我們再看看這個陣列刪除操作他的複雜度是怎樣的。
3.png

此時假設經過前面的新增運算元組容量變為2n,同樣每次執行一次刪除操作,時間複雜度為O(1)級別的。
4.png

同樣當我們刪除元素,刪除到一定程度時,為了避免空間的浪費,就要對陣列進行縮容。是不是可以引用前面陣列擴容的思路,當陣列元素的個數,等於當前陣列容量的1/2時,就將陣列容量減小一半。那麼前面(n-1)次操作耗費時間總和為(n-1),第n次操作耗費時間為(n+1),n為對陣列進行縮容操作耗時,1為刪除這個元素耗時。所以均攤來看每次耗費時間仍然是2,時間複雜度為O(1)。那麼如果你這樣做的話你可能迎來一個新的問題——複雜度震盪
5.png

當我們刪除元素,刪除到陣列元素個數為陣列容量的1/2時。按照前面的思路。此時我們進行了縮容操作。變成下面了這副圖。
6.png

如果剛好業務湊巧,前面剛進行了一次刪除,對陣列進行了一次縮容操作。後面又緊接著進行了一次陣列的新增操作。因為我們知道當對陣列進行縮容操作時,陣列元素個數剛好等於陣列的容量。此時如果再新增一個元素。那麼我們就要進行擴容操作。不管是擴容還是縮容那麼他的複雜度都是O(n)級別的。如果在這時一直重複進行刪除,新增操作。那麼整個演算法的複雜度將由原來的O(1)級別上升到O(n)級別。這就是所謂的複雜度震盪。
7.png

出現複雜度震盪肯定是不好的,那我們如何來解決這個問題呢,其實很簡單。就是我們推遲陣列的縮容操作。當元素個數等於陣列容量的1/4時,再進行縮容操作,縮容為當前陣列容量的1/2。這樣陣列就有了剩餘的空間,無論是新增還是刪除。都是O(1)級別的。你學會了嗎?
最後說兩件事
1.中秋活動的中獎名單已出,如下,沒寫聯絡方式的儘快寫上。好給你發紅包。
8.jpg
2.最近建立了個免費的知識星球——讀書會,歡迎大家加入。一起分享交流。9.jpg

更多內容歡迎大家關注

yuandatou