簡體   English   中英

Python list.clear() 時間和空間復雜度?

[英]Python list.clear() time and space complexity?

我正在寫一篇關於 Python list.clear()方法的博客文章,其中我還想提到底層算法的時間和空間復雜性。 我預計時間復雜度為O(N) ,迭代元素並釋放內存? 但是,我發現一篇文章提到它實際上是一個O(1)操作。 然后,我在 CPython 實現中搜索了該方法的源代碼,發現了一個我認為是list.clear()的實際內部實現的list.clear() ,但是,我不太確定它是。 下面是該方法的源代碼:

static int
_list_clear(PyListObject *a)
{
    Py_ssize_t i;
    PyObject **item = a->ob_item;
    if (item != NULL) {
         /* Because XDECREF can recursively invoke operations on
           this list, we make it empty first. */
        i = Py_SIZE(a);
        Py_SIZE(a) = 0;
        a->ob_item = NULL;
        a->allocated = 0;
        while (--i >= 0) {
           Py_XDECREF(item[i]);
        }
        PyMem_FREE(item);
    }
    /* Never fails; the return value can be ignored.
       Note that there is no guarantee that the list is actually empty
       at this point, because XDECREF may have populated it again! */
    return 0;
}

我可能是錯的,但對我來說它看起來像O(N) 另外,我在這里發現了一個類似的問題,但那里沒有明確的答案。 只想確認list.clear()的實際時間和空間復雜度,也許還有一些支持答案的解釋。 任何幫助表示贊賞。 謝謝。

正如您所注意到的, list.clearCPython實現是 O(n)。 代碼迭代元素以減少每個元素的引用計數,但沒有辦法避免它。 毫無疑問,這是一個 O(n) 操作,並且,給定一個足夠大的列表,您可以測量在clear()花費的時間作為列表大小的函數:

import time

for size in 1_000_000, 10_000_000, 100_000_000, 1_000_000_000:
    l = [None] * size
    t0 = time.time()
    l.clear()
    t1 = time.time()
    print(size, t1 - t0)

輸出顯示線性復雜度; 在我使用 Python 3.7 的系統上,它會打印以下內容:

1000000 0.0023756027221679688
10000000 0.02452826499938965
100000000 0.23625731468200684
1000000000 2.31496524810791

每個元素的時間當然很小,因為循環是用 C 編碼的,每次迭代都做很少的工作。 但是,正如上面的測量結果所示,即使是很小的每個元素因素最終也會加起來。 小的每元素常量不是忽略操作成本的原因,或者同樣適用於移動l.insert(0, ...)的列表元素的循環,這也非常有效 - 但很少有人會聲稱在開始時插入是 O(1)。 (並且clear可能會做更多的工作,因為 deref 將為引用計數實際上為零的對象運行任意的析構函數鏈。)

在哲學層面上,人們可能會爭辯說,在評估復雜性時應該忽略內存管理的成本,否則就不可能確定地分析任何事情,因為任何操作都可能觸發 GC。 這個論點是有道理的; GC 確實偶爾且不可預測地出現,其成本可以考慮在所有分配中攤銷。 類似地,復雜性分析傾向於忽略malloc的復雜性,因為它所依賴的參數(如內存碎片)通常與分配大小甚至與已分配塊的數量沒有直接關系。 但是,在list.clear情況下,只有一個分配的塊,不會觸發 GC,並且代碼仍在訪問每個列表元素。 即使假設 O(1) malloc 和分攤 O(1) GC, list.clear仍然需要與列表中元素數量成正比的時間。

從問題鏈接的文章是關於 Python 語言的,並沒有提到特定的實現。 不使用引用計數的 Python 實現(例如 Jython 或 PyPy)可能具有真正的 O(1) list.clear ,並且對它們而言,文章中的聲明是完全正確的。 因此,在概念層面解釋 Python 列表時,說清除列表是 O(1) 並沒有錯——畢竟,所有對象引用都在一個連續的數組中,而你只釋放一次。 這是您的博客文章可能應該提出的觀點,這就是鏈接文章想要表達的意思。 過早考慮引用計數的成本可能會使讀者感到困惑,並讓他們對 Python 的列表產生完全錯誤的想法(例如,他們可以想象它們是作為鏈表實現的)。

最后,在某些時候,人們必須接受內存管理策略確實會改變某些操作的復雜性。 例如,在 C++ 中銷毀鏈表從調用者的角度來看是 O(n); 在 Java 或 Go 中丟棄它是 O(1)。 並且不是垃圾收集語言只是將相同的工作推遲到以后的瑣碎意義上 - 移動收集器很可能只會遍歷可達對象並且確實永遠不會訪問丟棄鏈表的元素。 引用計數使得丟棄大型容器在算法上類似於手動收集,而 GC 可以刪除它。 雖然 CPython 的list.clear必須接觸每個元素以避免內存泄漏,但 PyPy 的垃圾收集器很可能永遠不需要做任何此類事情,因此有一個真正的 O(1) list.clear

這是 O(1) 忽略內存管理。 說它是 O(N) 內存管理是不太正確的,因為內存管理是復雜的。

大多數時候,出於大多數目的,我們將內存管理的成本與觸發它的操作的成本分開處理。 否則,你能做的幾乎所有事情都會變成 O(誰知道呢),因為幾乎任何操作都可能觸發垃圾收集傳遞或昂貴的析構函數或其他東西。 哎呀,即使在像 C 這樣具有“手動”內存管理的語言中,也不能保證任何特定的mallocfree調用都會很快。

有一種觀點認為,應區別對待引用計數操作。 畢竟, list.clear顯式執行與列表長度相等的許多Py_XDECREF操作,即使結果沒有對象被釋放或最終確定,引用計數本身也必然需要與列表長度成正比的時間。

如果計算Py_XDECREF操作list.clear顯式執行,但忽略可能由引用計數操作觸發的任何析構函數或其他代碼,並且假設PyMem_FREE是常數時間,則list.clear是 O(N),其中 N 是列表的原始長度。 如果不考慮所有內存管理開銷,包括顯式Py_XDECREF操作,則list.clear是 O(1)。 如果計算所有內存管理成本,則list.clear的運行時不能漸近地受列表長度的任何函數的限制。

正如其他答案所指出的,清除長度為n的列表需要 O( n ) 時間。 但我認為這里還有一個關於攤銷復雜性的補充說明。

如果您從一個空列表開始,並以任何順序執行N 個appendclear操作,那么所有這些操作的總運行時間始終為 O( N ),每個操作的平均時間為 O(1),無論多長時間list 進入過程中,無論這些操作中有多少是clear

clear一樣, append的最壞情況也是 O( n ) 時間,其中n是列表的長度。 那是因為當需要增加底層數組的容量時,我們必須分配一個新數組並復制所有內容。 但是復制每個元素的成本可以“收費”到其中一個append操作,該操作將列表的長度設為需要調整數組大小的長度,這樣從空列表開始的N 個append操作總是需要 O( N ) 時間。

同樣,在clear方法中減少元素的引用計數的成本可以“收費”到首先插入該元素的append操作,因為每個元素只能被清除一次。 結論是,如果你使用的是列表,在你的算法內部數據結構,而你的算法反復清除一個循環內部列表,然后分析你的算法的時間復雜度的目的,你要數clear該列表作為一個O上(1) 操作,就像在相同情況下append計算為 O(1) 操作一樣。

快速time檢查表明它是O(n)。

讓我們執行以下操作並預先創建列表以避免開銷:

import time
import random

list_1000000 = [random.randint(0,10) for i in range(1000000)]
list_10000000 = [random.randint(0,10) for i in range(10000000)]
list_100000000 = [random.randint(0,10) for i in range(100000000)]

現在檢查清除這四個不同大小的列表所需的時間,如下所示:

start = time.time()
list.clear(my_list)
end = time.time()
print(end - start))

結果:

list.clear(list_1000000) takes 0.015
list.clear(list_10000000) takes 0.074
list.clear(list_100000000) takes 0.64

需要更健壯的時間測量,因為這些數字在每次運行時可能會有所不同,但結果表明,隨着輸入大小的增長,執行時間幾乎呈線性變化。 因此我們可以得出 O(n) 復雜度的結論。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM