我想使用Python 3
in place对列表进行排序,没有额外的空间。
据我所知,Python使用sorted(myList)
对列表进行排序,这会创建一个新的排序数组,显然会占用O(N)的额外空间。或者使用myList.sort()
,它使用Timsort,最坏情况下的空间复杂度也是O(N)。
我搜索了所有的文档,但是没有找到任何用于常数空间算法的内置函数(选择排序,插入排序, shell 排序,堆排序,鸡尾酒排序等)。
我知道我可以找到这些算法的实现,但内置的 * 手动优化 * 实现是我希望找到的最好的。
任何建议都很感激。
2条答案
按热度按时间zysjyyx41#
最好的选择是使用heap sort,它既有合理的时间效率(时间复杂度为 O(n log n)),又有空间效率(空间复杂度保证为 O(1))。
虽然Python有一个实现二进制堆的内置模块
heapq
,但它只导出执行就地堆排序所需的两个函数之一,heapify
,它将列表转换为min堆;另一个必要的函数_siftup
,一个将给定起始位置的较小子节点冒泡的函数(以此类推,直到碰到一片叶子为止)不被导出。如果没有
_siftup
,只能通过将堆中的最小值弹出到一个新列表中来执行堆排序,这需要 O(n) 的空间复杂度:heapsort可以通过将所有值推到堆上,然后每次弹出一个最小值来实现:
字符串
更重要的是,
heapify
将列表转换为最小堆,这对于就地排序并不理想,因为我们希望在最后交换较大的排序项,而不是相反。引用自heapq
的文档:我们的pop方法返回最小的项,而不是最大的项(在教科书中称为“最小堆”;“最大堆”在文本中更常见,因为它适合就地排序)。
幸运的是,为了满足
heapq.merge
在反向模式下的需求,heapq
实际上还使用其他未导出的函数实现了最大堆,包括_heapify_max
,heapify
的最大堆版本,以及_siftup_max
,_siftup
的最大堆版本。然而,
heapq._siftup_max
函数不接受结束位置作为参数,这是限制堆的大小以保留列表末尾已经排序的项所必需的。因此,为了解决缺少结束位置参数的问题,同时保持 O(1) 的空间复杂度,我们可以传递给它一个array.array
的切片memoryview
,因为你在注解中提到你有“通常是整数,总是可以容纳在内存中”,它可以很容易地被加载为'q'
类型的array
(64位有符号整数)。但是,
heapq
模块将尝试从其C实现_heapq
(如果可用)导入,_heapq._heapify_max
将专门将参数验证为list
并拒绝array
。heapq._heapify_max
的Python实现没有此限制,所以要导入它,我们需要先自己导入_heapq
,然后从_heapq
模块对象中删除_heapify_max
,这样当我们导入heapq
时,heapq
的_heapify_max
就不会被覆盖:型
下面是如何使用
heapq
的内置函数执行堆排序,首先使用_heapify_max
将数组堆化,然后迭代地将根处的最大数与末尾的叶子交换,并使用_siftup_max
筛选它,直到它的所有子元素都较小:型
或者使用min heap执行堆排序,你必须在最后反转结果:
型
以便:
型
产出:
型
此处演示max heap和min heap
或者,正如@KellyBundy在评论中指出的那样,我们可以通过使用代理对象来解决
heapq._siftup
缺少结束位置参数的问题,该代理对象在切片时通过人为设置的size
属性来限制堆的大小,并在调用其__len__
方法时将该属性报告为堆的长度。除了允许任何列表作为输入之外,与
memoryview
相比,这种方法的额外好处是我们不需要首先导入_heapq
来删除heapify
,因为我们现在可以向它传递实际的列表:型
在这里演示proxy object
类似地,我们可以使用代理对象使基于
heappop
的堆排序就地工作,方法是使代理对象仅返回堆末尾的项,而不是在弹出时实际删除它。但是,在这种情况下,
heappop
的C实现不仅会将代理对象验证为list
对象(这可以通过使代理对象继承list
来弥补),但也可以直接将其长度作为C属性访问,而不是调用我们重写的__len__
方法,所以我们必须删除C实现来调用Python版本。这种方法的好处是坚持使用公开可用的API,因此最不容易受到
heapq
实现更改的影响:型
在这里演示
heappop
最后,请注意,如果
heapq
完全改变了它的实现,以至于上面的方法都不起作用(同样,特别是对于基于heappop
的解决方案,这种可能性非常小),那么您总是可以从头开始实现堆排序:型
llycmphe2#
你提到了插入排序。这里有一个简单而相当快的,几乎肯定只需要O(1)空间:
字符串
如果列表被创建为一个更长的列表,那么你删除的元素正好是pop()导致的realloc,如果realloc移动到不同的内存区域,这可能需要超过O(1)的空间。或者如果CPython改变了它的分配策略,或者你使用了不同的Python实现。但是在任何情况下,我认为这不太可能比在分配的空间内转移内存更多。
pops/insert确实需要线性时间,但这是快速的低级内存移动。因此,虽然这确实使整个事情花费O(n²)时间,但这是相对快速的O(n²),即一个相当小的隐藏常数。插入点是用二分搜索找到的,所以这只是O(n log n)比较。
通过一些测试(Attempt This Online!):
型