【问题标题】:Why is heap slower than sort for K Closest Points to Origin?为什么 K Closest Points to Origin 的堆比排序慢?
【发布时间】:2019-10-10 13:17:05
【问题描述】:

编码任务是here

堆解决方案:

import heapq
class Solution:
    def kClosest(self, points: List[List[int]], K: int) -> List[List[int]]:
        return heapq.nsmallest(K, points, key = lambda P: P[0]**2 + P[1]**2)

排序解决方案:

class Solution(object):
    def kClosest(self, points: List[List[int]], K: int) -> List[List[int]]:
        points.sort(key = lambda P: P[0]**2 + P[1]**2)
        return points[:K]

根据here的解释,Python的heapq.nsmallest是O(n log(t)),Python的List.sort()是O(n log(n))。但是,我的提交结果显示 sort 比 heapq 快。这怎么发生的?理论上是相反的,不是吗?

【问题讨论】:

  • Big O 完全没有告诉您两种算法在同一数据集上的相对性能。它仅告诉您对于足够大的数据,渐近更快的算法将获胜。你没有使数据足够大。有时不会有足够大的实际数据。 Big O 是一个非常粗略的分析工具。
  • @Gene 谢谢!我一直在想cs95的回答和你的评论有什么关系。如果有足够大的实际数据,用 Python 还是 C 实现还重要吗?
  • 所以cs95删除了他的回答以及他与其他人的讨论。 :/ 我在手机上看到了他的讨论。首先,假设原因是 heapq 是在 Python 中实现的,而 sort 是在 C++ 中使用 timsort 实现的。然后有人说确实存在heapq的c ++实现版本。其他的我不记得了,只能等别人解释了。
  • 已删除答案的评论:“heapq 是用 C 实现的。它也有一个等效的 Python 代码。我认为问题可能是 heapq 在每次比较时重新评估 key 函数,因为它不像 sort 那样存储一次调用它的结果。 ——丹 D。”
  • 你可以试试dist_points = (x**2 + y**2, x, y for x, y in points)return [x, y for _, x, y in heapq.nsmallest(K, dist_points)]。不确定这是否会更快。

标签: python algorithm sorting complexity-theory heapq


【解决方案1】:

让我们选择大O符号from the Wikipedia的定义:

Big O 表示法是一种数学表示法,它描述了当参数趋于特定值或无穷大时函数的限制行为。

...

在计算机科学中,大 O 表示法用于根据算法的运行时间或空间需求如何随着输入大小的增长而增长进行分类。

所以 Big-O 类似于:

因此,当您在小范围/数字上比较两种算法时,您不能强烈依赖 Big-O。我们来分析例子:

我们有两种算法:第一种是 O(1),适用于正好 10000 个滴答声,第二种是 O(n^2)。所以在 1~100 范围内,第二个会比第一个快(100^2 == 10000 所以(x<100)^2 < 10000)。但是从 100 开始,第二个算法会比第一个慢。

类似的行为出现在您的函数中。我用各种输入长度对它们进行计时并构建时序图。以下是大数字函数的时间安排(黄色是sort,蓝色是heap):

您可以看到sortheap 消耗更多时间,并且时间比heap's 增加得更快。但是,如果我们仔细观察较低的范围:

我们会看到在小范围内sortheap 快!看起来heap 具有“默认”时间消耗。因此,具有较差 Big-O 的算法比具有更好 Big-O 的算法运行得更快并没有错。这只是意味着它们的范围使用太小,以至于更好的算法比更差的算法更快。

这是第一个情节的时序代码:

import timeit
import matplotlib.pyplot as plt

s = """
import heapq
def k_heap(points, K):
    return heapq.nsmallest(K, points, key = lambda P: P[0]**2 + P[1]**2)

def k_sort(points, K):
    points.sort(key = lambda P: P[0]**2 + P[1]**2)
    return points[:K]
"""

random.seed(1)
points = [(random.random(), random.random()) for _ in range(1000000)]
r = list(range(11, 500000, 50000))
heap_times = []
sort_times = []
for i in r:
    heap_times.append(timeit.timeit('k_heap({}, 10)'.format(points[:i]), setup=s, number=1))
    sort_times.append(timeit.timeit('k_sort({}, 10)'.format(points[:i]), setup=s, number=1))

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
#plt.plot(left, 0, marker='.')
plt.plot(r, heap_times, marker='o')
plt.plot(r, sort_times, marker='D')
plt.show()

对于第二个情节,替换:

r = list(range(11, 500000, 50000))  -> r = list(range(11, 200))
plt.plot(r, heap_times, marker='o') -> plt.plot(r, heap_times)
plt.plot(r, sort_times, marker='D') -> plt.plot(r, sort_times)

【讨论】:

    【解决方案2】:

    正如已经讨论过的,在 python 中使用 tim sort 快速实现排序是一个因素。这里的另一个因素是堆操作不像合并排序和插入排序那样对缓存友好(tim 排序是这两者的混合)。

    堆操作访问存储在远程索引中的数据。

    Python 使用基于 0 索引的数组来实现其堆库。所以对于第 k 个值,它的子节点索引是 k * 2 + 1 和 k * 2 + 2。

    每次在堆中添加/删除元素后执行向上/向下渗透操作时,它都会尝试访问远离当前索引的父/子节点。这不是缓存友好的。这也是为什么堆排序通常比快速排序慢的原因,尽管它们都是渐近相同的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-10-26
      • 2017-03-18
      • 2017-07-23
      • 2016-02-18
      • 2021-06-19
      • 2015-06-25
      • 2021-12-09
      • 2016-09-08
      相关资源
      最近更新 更多