簡體   English   中英

插入排序的實現不正確,或者對時間復雜度的理解不正確?

[英]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無法對其進行緩存。 向其中添加上述讀寫操作時,它會變慢。

插入排序實現在技術上是正確的,但不是最佳選擇。

  • 它執行3個任務,並且
  • 它測試兩個條件( index > 0arr[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.

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