[英]Incorrect implementation of insertion sort, or incorrect understanding of time complexity?
我一直在編寫一些python腳本來測試和計時各種常用算法,這完全出於我自己的目的。 我確信已經有資源可以做到這一點,但是我發現自己編寫這些資源很有幫助。
我編寫了一個腳本,該腳本實現冒泡,選擇和插入排序,並且每個腳本都會運行10次迭代,這些迭代具有不同的數組大小以及最佳/最壞/平均數組順序情況。
在大多數情況下,我看到了我所期望的結果,例如選擇排序總是花費相同的時間,而與數組的順序無關,而冒泡排序則按預期執行糟糕的操作。 我還看到插入排序的性能確實隨着給定數組順序的改善而提高,但是我對選擇和插入的比較感到困惑。
我知道這兩種算法在最壞情況下的時間復雜度均為O(n ^ 2),並且插入排序的平均時間復雜度優於選擇排序,但是我發現在很多情況下,插入排序是表現比選擇排序差,這對我來說似乎不正確。 我希望兩者在最壞的情況下表現相同,並且即使不是最壞的情況,該插入排序也會表現更好。 我是否誤解了如何解釋這些結果,還是在實現這兩種算法時出錯了?
這是我的腳本:
import random
import time
import sys
from enum import Enum
class Case(Enum):
BEST = 1
WORST = 2
AVERAGE = 3
def bubble_sort(arr):
sorted = False
while not sorted:
sorted = True
for i in range(0, len(arr)):
# n
if i + 1 < len(arr) and arr[i] > arr[i + 1]:
scratch = arr[i]
arr[i] = arr[i + 1]
arr[i + 1] = scratch
sorted = False
return arr
def selection_sort(arr):
for i in range(0, len(arr)):
# n
min_index = i
for j in range(i + 1, len(arr)):
# n
if arr[j] < arr[min_index]:
min_index = j
scratch = arr[i]
arr[i] = arr[min_index]
arr[min_index] = scratch
return arr
def insertion_sort(arr):
for i in range(1, len(arr)):
# n
index = i
while index > 0 and arr[index - 1] > arr[index]:
# worst case n, best case 1
scratch = arr[index]
arr[index] = arr[index - 1]
arr[index - 1] = scratch
index -= 1
return arr
TOTAL_RUNS = 10
def verify(algorithm, name):
# first let's test that it actually sorts correctly
arr = list(range(1, 20))
random.shuffle(arr)
arr = algorithm(arr)
for i in range(0, len(arr) - 1):
if arr[i] > arr[i + 1]:
raise Exception("NOT SORTED!")
print("timing " + name + " sort...")
def time_the_algorithm(algorithm, case):
total = 0
min = sys.maxsize
max = 0
sizes = [1000,5000,10000]
for size in sizes:
for i in range(0, TOTAL_RUNS):
arr = list(range(1, size))
if case == Case.WORST:
# for worst case, reverse entire array
arr = list(reversed(arr))
elif case == Case.AVERAGE:
# average case, random order
random.shuffle(arr)
start = time.time()
arr = algorithm(arr)
end = time.time()
elapsed = end - start
total += elapsed
if elapsed > max:
max = elapsed
if elapsed <= min:
min = elapsed
print(name + ", n={0:} - ".format(size) + str(case) + ": avg {0:.2f}s, min {1:.2f}s, max {2:.2f}s".format(total/TOTAL_RUNS, min, max))
# worst case
time_the_algorithm(algorithm, Case.WORST)
# avg case
time_the_algorithm(algorithm, Case.AVERAGE)
# best case
time_the_algorithm(algorithm, Case.BEST)
verify(insertion_sort, "insertion")
verify(selection_sort, "selection")
verify(bubble_sort, "bubble")
這是我的輸出:
timing insertion sort...
insertion, n=1000 - Case.WORST: avg 0.06s, min 0.06s, max 0.06s
insertion, n=5000 - Case.WORST: avg 1.42s, min 0.06s, max 1.46s
insertion, n=10000 - Case.WORST: avg 6.90s, min 0.06s, max 5.70s
insertion, n=1000 - Case.AVERAGE: avg 0.03s, min 0.03s, max 0.03s
insertion, n=5000 - Case.AVERAGE: avg 0.71s, min 0.03s, max 0.70s
insertion, n=10000 - Case.AVERAGE: avg 3.44s, min 0.03s, max 2.76s
insertion, n=1000 - Case.BEST: avg 0.00s, min 0.00s, max 0.00s
insertion, n=5000 - Case.BEST: avg 0.00s, min 0.00s, max 0.00s
insertion, n=10000 - Case.BEST: avg 0.00s, min 0.00s, max 0.00s
timing selection sort...
selection, n=1000 - Case.WORST: avg 0.02s, min 0.02s, max 0.02s
selection, n=5000 - Case.WORST: avg 0.43s, min 0.02s, max 0.43s
selection, n=10000 - Case.WORST: avg 2.17s, min 0.02s, max 1.84s
selection, n=1000 - Case.AVERAGE: avg 0.01s, min 0.01s, max 0.02s
selection, n=5000 - Case.AVERAGE: avg 0.43s, min 0.01s, max 0.44s
selection, n=10000 - Case.AVERAGE: avg 2.30s, min 0.01s, max 1.93s
selection, n=1000 - Case.BEST: avg 0.01s, min 0.01s, max 0.02s
selection, n=5000 - Case.BEST: avg 0.42s, min 0.01s, max 0.41s
selection, n=10000 - Case.BEST: avg 2.26s, min 0.01s, max 1.92s
timing bubble sort...
bubble, n=1000 - Case.WORST: avg 0.11s, min 0.11s, max 0.11s
bubble, n=5000 - Case.WORST: avg 3.15s, min 0.11s, max 3.24s
bubble, n=10000 - Case.WORST: avg 15.09s, min 0.11s, max 13.66s
bubble, n=1000 - Case.AVERAGE: avg 0.09s, min 0.09s, max 0.10s
bubble, n=5000 - Case.AVERAGE: avg 2.62s, min 0.09s, max 2.63s
bubble, n=10000 - Case.AVERAGE: avg 12.53s, min 0.09s, max 10.90s
bubble, n=1000 - Case.BEST: avg 0.00s, min 0.00s, max 0.00s
bubble, n=5000 - Case.BEST: avg 0.00s, min 0.00s, max 0.00s
bubble, n=10000 - Case.BEST: avg 0.00s, min 0.00s, max 0.00s
編輯:
我采納了@ asaf-rosemarin的建議,並嘗試將for循環替換為while循環,以查看這是否會使計時更均勻,但似乎絲毫沒有影響性能
def insertion_sort(arr):
for i in range(1, len(arr)):
# n
for j in range(i, 0, -1):
# worst case n, best case 1
if arr[j - 1] > arr[j]:
scratch = arr[j]
arr[j] = arr[j - 1]
arr[j - 1] = scratch
else:
break
return arr
輸出:
timing insertion sort...
insertion, n=1000 - Case.AVERAGE: avg 0.03s, min 0.03s, max 0.03s
insertion, n=5000 - Case.AVERAGE: avg 0.72s, min 0.03s, max 0.74s
insertion, n=10000 - Case.AVERAGE: avg 3.61s, min 0.03s, max 3.13s
timing selection sort...
selection, n=1000 - Case.AVERAGE: avg 0.02s, min 0.02s, max 0.02s
selection, n=5000 - Case.AVERAGE: avg 0.47s, min 0.02s, max 0.51s
selection, n=10000 - Case.AVERAGE: avg 2.52s, min 0.02s, max 2.17s
timing bubble sort...
bubble, n=1000 - Case.AVERAGE: avg 0.10s, min 0.09s, max 0.10s
bubble, n=5000 - Case.AVERAGE: avg 2.56s, min 0.09s, max 2.50s
bubble, n=10000 - Case.AVERAGE: avg 12.31s, min 0.09s, max 10.34s
您對時間復雜度的理解是正確的,並且我在您的實現中找不到任何錯誤,因此我的猜測是原因是for ... in range
比python中的while
循環快。
(這里有更多信息, 為什么在Python中遍歷range()比使用while循環更快? )
編輯:
時間復雜度之間的比較與實現的實際運行時間之間的比較之間不一致的原因是,時間復雜度僅考慮比較的數量,而忽略了額外的操作開銷(因為每個比較的開銷為O(1)
),但是這些額外的操作及其實現方式(例如,編譯與解釋,緩存友好性)可能會嚴重影響運行時間。
我有個主意。
在插入排序的內部循環中,您將用每個循環替換數組中的項目。 就實現方式(讀寫操作的數量)而言,這將創建一個偽氣泡排序算法。 也許您可以將下一個數字保留在變量中的位置i
在已排序數組中找到它的合適位置,然后移動所有項目。
另外,與選擇排序相比,您對數組的訪問次數要多得多。 在選擇排序中,您只能在內部循環中對數組進行2次訪問,並且除非有新的最小數字,否則索引不會更改,因此python會對其進行緩存。 在插入排序中,您在內部循環中對數組進行了6次訪問,索引在每次迭代時都會更改,並且對數組的所有訪問都依賴於index
變量,因此python無法對其進行緩存。 向其中添加上述讀寫操作時,它會變慢。
插入排序實現在技術上是正確的,但不是最佳選擇。
index > 0
和arr[index - 1] > arr[index]
) 在內循環的每次迭代中。 您可以完成一項作業和一項測試。 要刪除不必要的任務,請考慮
def insertion_sort(arr):
for i in range(1, len(arr)):
index = i
scratch = arr[i]
while index > 0 and arr[index - 1] > arr[index]:
arr[index] = arr[index - 1]
index -= 1
arr[index] = scratch
return arr
要減少測試數量,請考慮
def insertion_sort(arr):
for i in range(1, len(arr)):
scratch = arr[i]
if scratch < arr[0]:
# Don't bother about the values; just shift the array
for index in range(i, 0, -1):
arr[index] = arr[index - 1]
arr[0] = scratch
else:
index = i
# Don't bother about indices: the loop is naturally guarded by arr[0]
while arr[index - 1] > arr[index]:
arr[index] = arr[index - 1]
index -= 1
arr[index] = scratch
return arr
計時算法幾乎沒有問題。
首先,為了進行公平的比較,您應該將它們與相同的數據進行計時。 在對平均情況進行計時時,每種算法都會獲得自己的混洗數組集。
其次, total
是在不同規模的運行中累積的。 這給在較短數據集上表現更好的算法帶來了不公平的優勢。
Nitpick:將arr[i]
與arr[min_index]
(選擇排序)交換的Python方法是
arr[i], arr[min_index] = arr[min_index], arr[i]
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.