[英]Python list.pop(i) time complexity?
我在網上list.pop()
一下,知道list.pop()
時間復雜度為 O(1) 但list.pop(i)
) 的時間復雜度為 O(n) 。 當我在編寫 leetcode 時,很多人在 for 循環中使用pop(i)
,他們說它的時間復雜度為 O(n),實際上它比我的代碼快,我的代碼只使用一個循環,但該循環中有很多行。 我想知道為什么會發生這種情況,我應該使用pop(i)
而不是多行來避免它嗎?
示例:Leetcode 26. 從排序數組中刪除重復項
我的代碼:(比 75% 快)
class Solution(object):
def removeDuplicates(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
left, right = 0, 0
count = 1
while right < len(nums)-1:
if nums[right] == nums[right+1]:
right += 1
else:
nums[left+1]=nums[right+1]
left += 1
right += 1
count += 1
return count
和其他人的代碼,比 90% 快:(這家伙不說 O(n),但為什么 O(n^2) 比我的 O(n) 快?)
我的優化代碼(快於 89%)
class Solution(object):
def removeDuplicates(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
left, right = 0, 0
while right < len(nums)-1:
if nums[right] != nums[right+1]:
nums[left+1]=nums[right+1]
left += 1
right += 1
return left + 1
您的算法確實需要 O(n) 時間,而“逆序彈出”算法確實需要 O(n²) 時間。 然而,LeetCode 並未報告您的時間復雜度優於 89% 的提交; 它報告您的實際運行時間優於所有提交的 89%。 實際運行時間取決於測試算法的輸入; 不僅是大小,還有重復的數量。
它還取決於如何平均跨多個測試用例的運行時間; 如果大多數測試用例是針對二次解更快的小輸入,那么即使二次解的時間復雜度更高,它也可能整體領先。 @Heap Overflow 在評論中還指出,LeetCode 判斷系統的開銷時間與算法運行所需的時間成比例地大且變化很大,因此差異可能只是由於該開銷的隨機變化造成的。
為了闡明這一點,我使用timeit測量了運行時間。 下圖顯示了我的結果; 考慮到時間復雜性,這些形狀正是您所期望的,並且交叉點在我的機器上介於8000 < n < 9000
之間。 這是基於排序列表,其中每個不同的元素平均出現兩次。 我用來生成時間的代碼如下。
計時碼:
def linear_solution(nums):
left, right = 0, 0
while right < len(nums)-1:
if nums[right] != nums[right+1]:
nums[left+1]=nums[right+1]
left += 1
right += 1
return left + 1
def quadratic_solution(nums):
prev_obj = []
for i in range(len(nums)-1,-1,-1):
if prev_obj == nums[i]:
nums.pop(i)
prev_obj = nums[i]
return len(nums)
from random import randint
from timeit import timeit
def gen_list(n):
max_n = n // 2
return sorted(randint(0, max_n) for i in range(n))
# I used a step size of 1000 up to 15000, then a step size of 5000 up to 50000
step = 1000
max_n = 15000
reps = 100
print('n', 'linear time (ms)', 'quadratic time (ms)', sep='\t')
for n in range(step, max_n+1, step):
# generate input lists
lsts1 = [ gen_list(n) for i in range(reps) ]
# copy the lists by value, since the algorithms will mutate them
lsts2 = [ list(g) for g in lsts1 ]
# use iterators to supply the input lists one-by-one to timeit
iter1 = iter(lsts1)
iter2 = iter(lsts2)
t1 = timeit(lambda: linear_solution(next(iter1)), number=reps)
t2 = timeit(lambda: quadratic_solution(next(iter2)), number=reps)
# timeit reports the total time in seconds across all reps
print(n, 1000*t1/reps, 1000*t2/reps, sep='\t')
結論是,對於足夠大的輸入,您的算法確實比二次解法快,但是 LeetCode 用來測量運行時間的輸入“不夠大”,無法克服判斷開銷的變化,而且平均值包括在二次算法更快的較小輸入上測量的時間。
僅僅因為解決方案不是 O(n),你不能假設它是 O(n^2)。
它並沒有完全變成 O(n^2) 因為他以相反的順序使用 pop 減少了每次彈出的時間,在正向順序上使用 pop(i) 將比反向使用更多的時間,因為 pop 搜索從反向開始,在每個循環中,他都在減少背面的元素數量。 以非相反的順序嘗試相同的解決方案,運行幾次以確保,你會看到。
無論如何,關於為什么他的解決方案更快,你有一個包含很多變量的 if 條件,他只使用了一個變量prev_obj
,使用相反的順序可以只使用一個變量。 因此,在您的情況下,基本數學運算的數量更多,因此在 O(n) 復雜度相同的情況下,您的每個 n 循環都比他的長。
看看你的計數變量,在每次迭代中,它的值是left+1
你可以返回left+1
,只需刪除它就會減少你必須做的 n 數量count=count+1
。
我剛剛發布了這個解決方案,速度提高了 76%
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
a=sorted(set(nums),key=lambda item:item)
for i,v in enumerate(a):
nums[i]=v
return len(a)
而這個速度超過 90% 。
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
a ={k:1 for k in nums} #<--- this is O(n)
for i,v in enumerate(a.keys()): #<--- this is another O(n), but the length is small so O(m)
nums[i]=v
return len(a)
如果您查看 for 循環,您可以說它們都超過 O(n),但是因為當我循環遍歷減少的成員而您的代碼循環遍歷所有成員時,我們正在使用重復成員。 因此,制作該唯一 set/dict 所需的時間如果少於您遍歷這些額外成員並檢查 if 條件所需的時間,那么我的解決方案可以更快。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.