【问题标题】:Traversing heapified list遍历堆积列表
【发布时间】:2011-12-17 23:12:44
【问题描述】:

我正在进行蒙特卡罗模拟。作为这项任务的一部分,我生成均匀分布在间隔(0,100) 上的样本。

generate = lambda: uniform(0,100)

当所有最接近的生成点对满足条件时,迭代停止。

check = lambda a,b: True if (b-a)<5 else False

我需要有一些结构来有效地保留所有生成的点并能够按升序遍历它们以在所有后续对上执行check

Python 中有一个heapq 模块,它支持非常有效的堆结构。我决定使用它。

我遇到了一个问题。我发现这个模块不支持遍历过程。我发现按升序访问堆值的唯一方法是使用heapq.heappop。但它会从堆中删除值。

我找到了解决方法,只是将堆对象复制到新对象中,并在新对象上使用heappop 进行迭代。但是我认为每次迭代都将整个结构复制到内存中并不是很有效。

我还有其他方法可以更有效地做我想做的事情吗?


用于说明的简化代码。

import heapq
from random import uniform
from itertools import tee, izip, count
from copy import copy


def pairwise(iterable): #get values from iterator in pairs
    a, b = tee(iterable)
    next(b, None)
    return izip(a, b)


check = lambda a,b: True if (b-a)<5 else False
generate = lambda: uniform(0,100)


def iterate_heap(heap):
    heap = copy(heap) #Here I have to copy the heap to be able to traverse
    try:
        while True:
            yield heapq.heappop(heap)
    except IndexError:
        return


def trial():
    items = []

    for i in count():
        item = generate()
        heapq.heappush(items, item)

        it = iterate_heap(items)
        it = pairwise(it)

        if i>0 and all(check(a,b) for a,b in it): #if i==0 then 'it' returns no values and 'all' returns True
            return i

print "The solution is reached. It took %d iterations." % trial()

paiwise 函数来自here 的配方。


更新: 在这个使用heappop 的实现中,每次迭代的复杂度是O(n*log(n))

复制堆:O(n)

向堆中添加新值:O(log(n))

遍历:n 元素 * O(log(n)) 从堆中弹出每个值 -> O(n*log(n))

结果:O(n+log(n)+n*log(n)) = O(n*log(n)

但我希望遍历是O(n),因此产生的复杂度将是O(n)

顺便说一下,如果我们只使用排序列表,我们需要在每次添加时对列表进行排序,所以O(n*log(n)),但遍历将是n*O(1) -&gt; O(n)。所以,得到的复杂度仍然是O(n*log(n))

我找到了解决办法。这是使用bisect 模块。找到要添加的位置是O(log(n))。添加到列表中的是O(n)(由于实现,插入到位后的所有值都必须移动)。遍历是O(n)。因此,得到的复杂度为O(n)

不过,如果有一种方法可以在 Python 中使用堆来解决此任务,我会感到很困惑。

【问题讨论】:

  • 随机注释:True if (b-a)&lt;5 else False 仅相当于 (b - a) &lt; 5
  • 听起来你想要en.wikipedia.org/wiki/B-tree
  • @JochenRitzel 是的,你是对的。我将“堆”与“二叉搜索树”混淆了。所以,是的,没有理由使用堆。
  • 您对 bisect 的计算是错误的。用 bisect 构造一个大小为 n 的排序列表需要 O(log(n)) per insert,所以它也是 O(nlog(n))。将其与移动列表部分的开销相乘,甚至是 O(nn*log(n))
  • 你是对的,查找和插入需要 O(logn+n)。尽管如此,这样做 n 次会给你带来 O(nn) 的复杂性。 B-Tree 在 O(nlogn) 内完成。

标签: python heap


【解决方案1】:

来自python docs

这两个可以毫无疑问地将堆视为常规 Python 列表:heap[0] 是最小的项,而 heap.sort() 保持堆不变!

您是否有理由不能将堆视为列表并对其进行迭代?

【讨论】:

  • 堆未排序。 heap[0] 确实是最小值。但是我需要按升序顺序遍历这些值,所以我需要在添加每个 O(nlog(n)) 值后对堆进行排序。但是当堆没有排序的时候,我只能用heappop来访问最小值。所以,我希望 O(n) 复杂度(堆遍历 O(n) 和向堆添加值 O(log(n))),而不是 O(nlog(n)) (O(n)在列表遍历(只是迭代)和 O(n*log(n)) 上添加。
  • 嗯。在我使用heappop 的实现中,复杂度为 O(n*log(n)) :n 元素弹出,O(log(n) 弹出每个元素的复杂度。我预计复杂度为O(n)。所以,是的,我肯定需要在没有heappop 的情况下想出一些东西。
  • @ovgolovin 另一种说法——即使在你之后heapify(some_list)type(some_list) 仍然是&lt;type 'list'&gt;。因此,要按升序浏览项目,只需使用for item in heap,降序使用for item in reversed(heap)
  • @agf 让我们列出els = [1,2,3,4]。让我们将2 添加到elsheapq.heappush(els ,2) -> els: [1, 2, 3, 4, 2]。然后堆化els: heapq.heapify(els) -> els: [1, 2, 3, 4, 2]。所以,els 没有排序。它只是堆积了。这就是为什么heapifying的复杂度是O(log(n)),而不是排序中的O(n*log(n))
  • @ovgolovin 当您说“排序”时,我以为您的意思是“保持堆不变”。如果您需要对它们进行排序,听起来heapq 并没有实际帮助?
【解决方案2】:

我会在堆上使用 list.sort()。这使堆条件保持不变,并且可以直接迭代底层列表。

FWIW,list.sort 使用的Timsort 算法将利用堆中已经存在的部分排序。

【讨论】:

  • 为什么还要使用堆。为什么我不能在列表中附加一个新值然后就地排序?我更新了问题。我警惕成为O(n*log(n)) 的复杂性。我只想要O(n)
  • 如果我将值添加到列表末尾然后对其进行排序,Timsort 的复杂性是多少?如果它是O(n),那么它会变成“越简单越好”:只是附加到列表然后对其进行排序。 :o)
  • 堆适用于您进行周期更新并且只想访问最低值时。否则,如果您需要完整排序并访问所有值,则首选 sort()。我使用堆提供了一个答案,因为那是您提出的问题。
  • 感谢您的回答。我将“二叉搜索树”与“堆”混淆了。如果我使用列表,我需要在每次迭代时对其进行排序。在排序列表附加一个未排序值的情况下,Timsort 是否具有O(n) 的复杂性,还是仍然具有O(n*log(n))
  • 如果我坚持使用 Timsort,复杂性不会有任何改变。希望 Timsort 会使用插入排序,那么复杂度将是相同的O(n)(因为插入),如果它使用 Mergesort,那么复杂度将变为O(n*log(n))。所以 Timsort 并不比 bisect-insertion 好。二叉搜索树会更好(因为将值添加到树将是O(log(n)),但遍历树仍将是O(n)(因此总体复杂度将是O(n) + O(log(n)) = O(n)。我还没有找到一个好的实现Python 中的这种结构。
【解决方案3】:

为了记录,在这种情况下,正确的数据结构是 B-Tree。有一个implementation

 from blist import sortedlist

运行时复杂度尽可能低:O(n*logn) 构建列表,O(n) 迭代。

【讨论】:

  • 谢谢!它绝对比列表和平分更好。因为迭代是一样的O(n),但是blistO(log(n))添加项目到列表的复杂度,而老套的list+bisect有O(log(n)+n)->O(n)的复杂度。
  • 这个模块很棒。它甚至 gives 文档中的复杂性
  • 我做了一些时间比较。 bisect 原来是最有效的。详情见我的回答。
【解决方案4】:

我做了一些效率计算。

使用bisect 模块可以获得最佳性能: 列表中间的 10000 次插入在我的计算机 (Python 2.7) 上计时 0.037 秒。

使用来自blist 模块的sortedlist 时钟为 0.287 秒,插入次数相同。

使用传统的listsort 在每个append 时钟2.796 秒后应用。 (现在Timsort 算法在 Python 中使用,它被认为在几乎排序的列表上非常有效;但事实证明它不如使用 bisect 高效)。


我用来进行这些计算的代码:

import bisect
import timeit
import __main__
import blist

N = 10000 #Number of executions
L = 1000 #Length of initial list

def test_f_bisect(a):
    bisect.insort_right(a,500)


def test_f_list_sort(a):
    a.append(500)
    a.sort()


test_f_blist_init = '''
from __main__ import test_f_blist
import blist
a = blist.sortedlist(range({L}))
'''.format(L=L)
def test_f_blist(a):
    a.add(500)


names = dir(__main__)
for name in names:
    attr = getattr(__main__,name)
    if hasattr(attr,'__call__'):
        if name.startswith('test_f_'):
            init_name = name + '_init'
            if hasattr(__main__, init_name):
                init = getattr(__main__,init_name)
            else:
                init = 'from __main__ import {name}; a = list(range({L}))'.format(name=name, L=L)
            t = timeit.Timer(stmt='{name}(a)'.format(name=name),
                             setup=init)

            time = t.timeit(N)
            print('{name}: {time}'.format(name=name,time=time))

【讨论】:

    【解决方案5】:

    我创建了一个迭代器类,它将执行最小堆的惰性按顺序遍历。它具有以下优点:

    1. 不需要原始堆的副本
    2. 不修改原始堆
    3. 如果提早停止,延迟迭代会更有效

    为了跟踪下一个迭代项,我实际上只是使用了另一个堆 self.next_items

    import heapq
    
    class HeapIter:
    
        def __init__(self, heap):
            self.original_heap = heap
            self.next_items = []
            if len(self.original_heap) > 0:
                self.next_items.append((self.original_heap[0], 0))
    
        def current_element(self):
            if len(self.next_items) == 0:
                return None
            return self.next_items[0][0]
    
        def next(self):
            if len(self.next_items) == 0:
                return None
            next_elem, next_index = heapq.heappop(self.next_items)
            child_1 = 2 * next_index + 1
            child_2 = child_1 + 1
            if child_1 < len(self.original_heap):
                heapq.heappush(self.next_items, (self.original_heap[child_1], child_1))
            if child_2 < len(self.original_heap):
                heapq.heappush(self.next_items, (self.original_heap[child_2], child_2))
            return next_elem
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-28
      • 2016-08-13
      • 1970-01-01
      • 1970-01-01
      • 2010-12-18
      • 2022-01-07
      相关资源
      最近更新 更多