Python中的QuickSort - 使用列表推导解释实现的行为

时间:2018-03-12 03:27:50

标签: python algorithm quicksort

我有一个名为quickSort()的函数就是这样做的。我试图理解“小”的行为。并且'更大'清单(见下文)。具体来说,在函数中使用递归时,Python如何处理内存?每次调用该函数时,请执行“较小的”操作。并且'更大'列表被覆盖(我假设发生这种情况......)?如果是这样,额外的内存将在何处释放,因为每个函数执行时列表都会变小......

def quickSort(lst):
   if len(lst) <= 1: 
      return lst
   smaller = [x for x in lst[1:] if x < lst[0]]
   larger = [x for x in lst[1:] if x >= lst[0]]
   return quickSort(smaller) + [lst[0]] + quickSort(larger)

创建的两个列表究竟发生了什么,以及在此方案中使用就地排序是否有任何好处。

免责声明:我一般是Python和算法的新手,详细解释将不胜感激。提前致谢。

编辑:

以下是重新提出的问题:

从初始列表开始,[8,5,2,9,1,7,3,4,6]
quickSort#1生成3个列表:[5,2,1,7,3,4,6] , [8] , [9]
quickSort#2产生3个列表:[2,1,3,4] , [5] , [6,7] (plus [8] , [9])
quickSort#3产生5个列表:[1] , [2] , [3,4] and [6] , [7] (plus [8] , [9])
quickSort#4生成1个列表:[1] , [2] , [3] , [4] , [5] , [6] , [7] , [8] ,[9]

它们都在此过程中连接在一起。我想知道的是列表[8,5,2,9,1,7,3,4,6] , [5,2,1,7,3,4,6] , [8]等是否存储,直到程序退出。 (上面的实现可能不正确,我只想说明我的观点)。

2 个答案:

答案 0 :(得分:2)

快速排序 O(nlogn),它会不断将问题分成两半,因为您将看到此问题的作用。

假设列表 x 并且调用quickSort(x)

x = [6,12,4,5,2,5,14,23,1,5]
quickSort(x)

这是一个递归函数,从内部它将再次调用该函数。但是,函数的每次调用都有自己的堆栈,它不能访问其范围之外的变量。

好的,在初始调用此函数时,smallerlarger将存储与小于和大于列表中第一个值的值相关联的列表。

smaller = [4, 5, 2, 5, 1, 5]
larger = [12, 14, 23]

这种分裂是通过列表理解完成的。我们遍历列表中除第一个值之外的每个值。如果要评估的当前值小于列表中的第一个值,则我们将其添加到smaller列表中。然后,我们对larger列表执行相同的操作。

现在返回功能是

return quickSort(smaller) + [6] + quickSort(larger)

让我们先做较小的一面。

quickSort([4, 5, 2, 5, 1, 5])

lst[0] = 4
smaller = [2, 1]
larger = [5, 5, 5]

return quickSort(smaller) + [4] + quickSort(larger)

所以这个过程一直持续到我们到达len(lst)<=1并且递归向后解开的点。想象一下,每次调用函数时,树的一个新分支都会延伸。当函数最终返回时,值将传播回树中。最终结果是一个排序数组。

  

[1,2,4,5,5,5,6,12,14,23]

答案 1 :(得分:2)

我认为JahKnows's answer解释了你想要的几乎所有东西,并且比我更加简洁,但还有一些你似乎不清楚的要点。

首先,你担心内存泄漏&#34;。这里没有内存泄漏,因为Python使用垃圾收集器自动找出要删除的内容。 CPython实现(您可能正在使用的实现)使用显式内部引用计数:只要您对对象的最后引用(局部变量,列表元素,对象属性等)消失,对象就会消失被删除,其内存被释放。由于您熟悉C ++:这就像包裹在shared_ptr中的每个值一样。其他实现使用更高级(更有效但不太确定)的收集器,但是,他们仍然无法删除您仍然引用的任何内容。

特别是,对quickSort的每次递归调用都有自己的局部变量lstsmallerlarger。你不能del在任何地方,或为它们分配新值,所以它们不会消失,直到函数调用完成。函数调用无法完成,直到它完成的两个递归调用完成。

所以,你有O(log N)递归&#34;帧&#34;在任何给定时间,每个都有O(N)本地存储空间,因此使用的总空间为O(N log N)

你问在现场做事是否有优势,答案是:是的。它更复杂(并且还有其他一些缺点),但是如果你就地排序,并且你只是改变了传递下来并备份堆栈的单个列表的位,你就可以避免{ {1}}空间费用(加上一些O(N log N)时间费用,但由于总时间为O(N) ...)。

如果不切换到原位,可以避免空间成本吗?好吧,除非您可以在重新递归时对结构列表进行结构化(这很难 - 在这种情况下,为什么不在原地进行?),您显然需要至少O(N long N)个空间来进行任何复制分类。并且O(N)并不比O(N log N)差得多。但我们可以改进。

例如,考虑一下:

O(N)

但这显然更复杂,更容易出错。它甚至可能更慢。这值得么?好吧,如果你真的需要一个复制快速排序,并且正在使用的额外内存正在推动32位崩溃的边缘或驱使你进入64位交换地狱,那么是的。否则,可能不会。