为什么我的 Python 脚本在我的 HeapSort 实现上的运行速度比它应该慢



我有赋值将堆排序算法实现到Python或Java(或任何其他语言)中。由于我对Python或Java不是那么"流利",我决定两者兼而有之。

但是在这里我遇到了一个问题,程序的运行时间比"应该"高得多。 我的意思是,堆排序应该运行到 O(n * log n),对于以几 GHz 时钟速率运行的当前处理器,我没想到该算法对于大小为 2000k 的数组运行超过 320 秒

因此,对于我所做的工作,我从Python和Java中的此类伪代码中实现了算法(我还尝试了Rosetta Code中的Julia中的代码,以查看运行时间是否相似,为什么Julia?随机选择)

所以我检查了输出是否存在小输入大小问题,例如大小为 10、20 和 30 的数组。看起来数组在两种语言/实现中都正确排序。

然后,我使用实现相同算法的 heapq 库再次检查运行时间是否相似。当实际情况如此时,我感到很惊讶...但是经过几次尝试后,我尝试了最后一件事,那就是更新Python,然后,使用heapq的程序运行速度比以前的程序快得多。实际上,320k阵列的2k秒左右,现在大约1.5秒左右。

我重试了我的算法,问题仍然存在。

所以这是我实现的 Heapsort 类:

class MaxHeap:
heap = []
def __init__(self, data=None):
if data is not None:
self.buildMaxHeap(data)
@classmethod
def toString(cls):
return str(cls.heap)
@classmethod
def add(cls, elem):
cls.heap.insert(len(cls.heap), elem)
cls.buildMaxHeap(cls.heap)
@classmethod
def remove(cls, elem):
try:
cls.heap.pop(cls.heap.index(elem))
except ValueError:
print("The value you tried to remove is not in the heap")
@classmethod
def maxHeapify(cls, heap, i):
left = 2 * i + 1
right = 2 * i + 2
largest = i
n = len(heap)
if left < n and heap[left] > heap[largest]:
largest = left
if right < n and heap[right] > heap[largest]:
largest = right
if largest != i:
heap[i], heap[largest] = heap[largest], heap[i]
cls.maxHeapify(heap, largest)
@classmethod
def buildMaxHeap(cls, heap):
for i in range(len(heap) // 2, -1, -1):
cls.maxHeapify(heap, i)
cls.heap = heap
@staticmethod
def heapSort(table):
heap = MaxHeap(table)
output = []
i = len(heap.heap) - 1
while i >= 0:
heap.heap[0], heap.heap[i] = heap.heap[i], heap.heap[0]
output = [heap.heap[i]] + output
heap.remove(heap.heap[i])
heap.maxHeapify(heap.heap, 0)
i -= 1
return output

要记录每个数组大小(10000 - 320000)的运行时,我在主函数中使用此循环:

i = 10000
while i <= 320000:
tab = [0] * i
j = 0
while j < i:
tab[j] = randint(0, i)
j += 1
start = time()
MaxHeap.heapSort(tab)
end = time()
pprint.pprint("Size of the array " + str(i))
pprint.pprint("Total execution time: " + str(end - start) + "s")
i *= 2

如果您需要其余代码来查看错误可能在哪里,请不要犹豫,我会提供它。只是不想无缘无故地共享整个文件。

如前所述,我预期的运行时间来自最坏情况的运行时间:O(n * log n) 使用现代架构和 2.6GHz 处理器,我预计大约 1 秒甚至更短(由于运行时间以纳秒为单位,我想即使是 1 秒仍然太长)

以下是结果:

Python (own) :                 Java (Own)
Time        Size               Time       Size 
593ms.       10k               243ms.      10k
2344ms.      20k               600ms.      20k
9558ms.      40k               1647ms.     40k
38999ms.     80k               6666ms.     80k
233811ms.    160k              62789ms.    160k
1724926ms.   320k              473177ms.   320k
Python (heapq)                 Julia (Rosetta Code)
Time        Size               Time        Size
6ms.         10k               21ms.        10k
14ms.        20k               21ms.        20k
15ms.        40k               23ms.        40k
34ms.        80k               28ms.        80k
79ms.        160k              39ms.        160k
168ms.       320k              60ms.        320k

And according to the formula the O(n * log n) give me :
40000       10k
86021       20k
184082      40k
392247      80k
832659      160k
1761648     320k

我认为这些结果可以用来确定根据机器(理论上)应该花费多少时间

如您所见,高运行时间结果来自我的算法,但我无法判断代码中的位置,这就是我在这里寻求帮助的原因。(在Java和Python中运行缓慢)(没有尝试在java lib中使用堆排序,有一个可以看到与我的实现的区别,我的不好)

多谢。

编辑 :我忘了补充一点,我在MacBook Pro上运行此程序(最新版本MacOS,i7 2,6GHz。以防问题也可能来自代码以外的任何其他内容。

编辑 2 :这是我在收到答案后对算法所做的修改。该程序的运行速度比以前快大约 200 倍,因此现在对于大小为 320k 的数组,它的运行时间仅为 2 秒

class MaxHeap:
def __init__(self, data=None):
self.heap = []
self.size = 0
if data is not None:
self.size = len(data)
self.buildMaxHeap(data)
def toString(self):
return str(self.heap)
def add(self, elem):
self.heap.insert(self.size, elem)
self.size += 1
self.buildMaxHeap(self.heap)
def remove(self, elem):
try:
self.heap.pop(self.heap.index(elem))
except ValueError:
print("The value you tried to remove is not in the heap")
def maxHeapify(self, heap, i):
left = 2 * i + 1
right = 2 * i + 2
largest = i
if left < self.size and heap[left] > heap[largest]:
largest = left
if right < self.size and heap[right] > heap[largest]:
largest = right
if largest != i:
heap[i], heap[largest] = heap[largest], heap[i]
self.maxHeapify(heap, largest)
def buildMaxHeap(self, heap):
for i in range(self.size // 2, -1, -1):
self.maxHeapify(heap, i)
self.heap = heap
@staticmethod
def heapSort(table):
heap = MaxHeap(table)
i = len(heap.heap) - 1
while i >= 0:
heap.heap[0], heap.heap[i] = heap.heap[i], heap.heap[0]
heap.size -= 1
heap.maxHeapify(heap.heap, 0)
i -= 1
return heap.heap

它使用与之前给出的相同的主运行

有趣的是,您发布了计算机的时钟速度 - 您可以计算算法所需的实际步数...但是你需要知道很多关于实现的信息。例如,在 python 中,每次创建对象或超出范围时,解释器都会更新基础对象上的计数器,并在这些 ref 计数达到 0 时释放内存。相反,您应该查看相对速度。

您发布的第三方示例显示,当输入数组长度加倍时,速度小于加倍。这似乎不对,是吗?事实证明,对于这些示例,构建数组的初始工作可能主导了对数组进行排序所花费的时间!

在您的代码中,已经有一条注释指出了我要说的话......

heap.remove(heap.heap[i])此操作将遍历列表(从索引 0 开始),查找匹配的值,然后将其删除。这已经很糟糕了(如果它按预期工作,如果您的代码按预期工作,您正在该行进行 320k 比较!但情况变得更糟 - 从数组中删除对象不是就地修改 - 删除对象后的每个对象都必须在列表中向前移动。最后,不能保证您实际上正在删除那里的最后一个对象......可能存在重复值!

这是一个有用的网站,列出了python中各种操作的复杂性 - https://wiki.python.org/moin/TimeComplexity。为了尽可能高效地实现算法,您需要尽可能多的数据结构操作为 O(1)。下面是一个示例...这里有一些原始代码,大概是 heap.heap 是一个列表......

output = [heap.heap[i]] + output
heap.remove(heap.heap[i])

行为

output.append(heap.heap.pop())

将避免分配新列表并使用常量时间操作来改变旧列表。(向后使用输出比使用 O(n) time insert(0) 方法要好得多!如果你真的需要顺序,你可以使用一个 dequeue 对象来输出以获得 appendleft 方法)

如果您发布了整个代码,我们可能还有很多其他的小事情可以提供帮助。希望这有所帮助!

相关内容

最新更新