[英]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.