【问题标题】:How to maintain dictionary in a heap in python?如何在python的堆中维护字典?
【发布时间】:2013-01-25 13:30:06
【问题描述】:

我有一本字典如下:

{'abc':100,'xyz':200,'def':250 .............}

它是一个字典,键是实体的名称,值是该实体的计数。我需要从字典中返回前 10 个元素。

我可以写一个堆来做,但我不确定如何对键映射做值,因为某些值是相等的。

还有其他数据结构可以做到这一点吗?

【问题讨论】:

  • “前 10 个元素”是否意味着最大的 10 个值?
  • 是的,这意味着最大的价值
  • 应该如何处理重复的?我的意思是,我们应该简单地忽略它们,就好像它们是不同的值一样,还是应该只保留一个重复的值?

标签: python data-structures heap


【解决方案1】:

使用堆是时间复杂度最好的解决方案:O(nlogk)。 其中 n 是堆的长度,k 在这里是 10

现在键映射的技巧是我们可以创建另一个类来比较键并定义魔术方法__lt__()__gt__()。覆盖 运算符

import heapq
class CompareWord:
  def __init__(self , word , value):
    self.word = word
    self.value = value

  def __lt__(self, other):   #To override > operator
    return self.value < other.value

  def __gt__(self , other):  #To override < operator
    return self.value > other.value

  def getWord(self):
    return self.word

def findKGreaterValues(compare_dict , k):
  min_heap = []
  for word in compare_dict:
      heapq.heappush(min_heap , CompareWord(word ,compare_dict[word] ))
      if(len(min_heap) > k):
          heapq.heappop(min_heap)   
  answer = []
  for compare_word_obj in min_heap:
      answer.append(compare_word_obj.getWord())

  return answer

【讨论】:

    【解决方案2】:

    您可以在您的类中实现 lt 函数,您可以在其中指定应该比较哪些属性。

    def __lt__(self, other):
       return self.attribute if self.attribute < other else other
    
    
    

    【讨论】:

      【解决方案3】:

      下面怎么样,应该是O(len(xs))。

      您只需将前 n 个元素与其余元素中最大的一个交换即可。

          def largest_n(xs, n):
              for i in range(n):
                  for j in range(i+1,len(xs)):
                      if xs[j] > xs [i]:
                          xs[i], xs[j] = xs[j], xs[i]
              return xs[:n]
      

      【讨论】:

        【解决方案4】:

        使用heapq 你可能想做这样的事情:

        heap = [(-value, key) for key,value in the_dict.items()]
        largest = heapq.nsmallest(10, heap)
        largest = [(key, -value) for value, key in largest]
        

        请注意,由于 heapq 仅实现最小堆,因此最好将值反转,以便更大的值变得更小。

        对于较小的堆,此解决方案会较慢,例如:

        >>> import random
        >>> import itertools as it
        >>> def key_generator():
        ...     characters = [chr(random.randint(65, 90)) for x in range(100)]
        ...     for i in it.count():
        ...             yield ''.join(random.sample(characters, 3))
        ... 
        >>> the_dict = dict((key, random.randint(-500, 500)) for key, _ in zip(key_generator(), range(3000)))
        >>> def with_heapq(the_dict):
        ...     items = [(-value, key) for key, value in the_dict.items()]
        ...     smallest = heapq.nsmallest(10, items)
        ...     return [-value for value, key in smallest]
        ... 
        >>> def with_sorted(the_dict):
        ...     return sorted(the_dict.items(), key=(lambda x: x[1]), reverse=True)[:10]
        ... 
        >>> import timeit
        >>> timeit.timeit('with_heapq(the_dict)', 'from __main__ import the_dict, with_heapq', number=1000)
        0.9220538139343262
        >>> timeit.timeit('with_sorted(the_dict)', 'from __main__ import the_dict, with_sorted', number=1000)
        1.2792410850524902
        

        对于 3000 个值,它仅比 sorted 版本稍快,后者是 O(nlogn) 而不是 O(n + mlogn)。如果我们将 dict 的大小增加到 10000,heapq 版本会变得更快:

        >>> timeit.timeit('with_heapq(the_dict)', 'from __main__ import the_dict, with_heapq', number=1000)
        2.436316967010498
        >>> timeit.timeit('with_sorted(the_dict)', 'from __main__ import the_dict, with_sorted', number=1000)
        3.585728168487549
        

        时间可能还取决于您正在运行的机器。您可能应该分析哪种解决方案最适合您的情况。如果效率不重要,我建议使用sorted 版本,因为它更简单。

        【讨论】:

        • +1,但是对 heapify 的调用是多余的,事实上,heapq.nlargest(the_dict.items()) 就是你所需要的。
        【解决方案5】:

        想象一个这样的字典(a-z 与 a=1 和 z=26 的映射):

        >>> d={k:v for k,v in zip((chr(i+97) for i in range(26)),range(1,27))}
        >>> d
        {'g': 7, 'f': 6, 'e': 5, 'd': 4, 'c': 3, 'b': 2, 'a': 1, 'o': 15, 'n': 14, 'm': 13, 'l': 12, 'k': 11, 'j': 10, 'i': 9, 'h': 8, 'w': 23, 'v': 22, 'u': 21, 't': 20, 's': 19, 'r': 18, 'q': 17, 'p': 16, 'z': 26, 'y': 25, 'x': 24}
        

        现在你可以这样做了:

        >>> v=list(d.values())
        >>> k=list(d.keys())
        >>> [k[v.index(i)] for i in sorted(d.values(),reverse=True)[0:10]]
        ['z', 'y', 'x', 'w', 'v', 'u', 't', 's', 'r', 'q']
        

        您还说过映射的某些值将相等。现在让我们更新d,使其包含字母A-Z,映射为1-26:

        >>> d.update({k:v for k,v in zip((chr(i+65) for i in range(26)),range(1,27))})
        

        现在A-Za-z 都映射到1-26

        >>> d
        {'G': 7, 'F': 6, 'E': 5, 'D': 4, 'C': 3, 'B': 2, 'A': 1, 'O': 15, 'N': 14, 'M': 13, 'L': 12, 'K': 11, 'J': 10, 'I': 9, 'H': 8, 'W': 23, 'V': 22, 'U': 21, 'T': 20, 'S': 19, 'R': 18, 'Q': 17, 'P': 16, 'Z': 26, 'Y': 25, 'X': 24, 'g': 7, 'f': 6, 'e': 5, 'd': 4, 'c': 3, 'b': 2, 'a': 1, 'o': 15, 'n': 14, 'm': 13, 'l': 12, 'k': 11, 'j': 10, 'i': 9, 'h': 8, 'w': 23, 'v': 22, 'u': 21, 't': 20, 's': 19, 'r': 18, 'q': 17, 'p': 16, 'z': 26, 'y': 25, 'x': 24} 
        

        因此,对于重复映射,唯一合理的结果是返回具有值的键列表:

        >>> [[k[x] for x,z in enumerate(v) if z==i ] for i in sorted(d.values(),reverse=True)[0:10]]
        [['Z', 'z'], ['Z', 'z'], ['Y', 'y'], ['Y', 'y'], ['X', 'x'], ['X', 'x'], ['W', 'w'], ['W', 'w'], ['V', 'v'], ['V', 'v']]
        

        你可以在这里使用 heapq:

        [[k[x] for x,z in enumerate(v) if z==i ] for i in heapq.nlargest(10,v)]
        

        您没有说明要对重复结果做什么,所以我假设您希望在结果列表保持 N 长的同时消除这些重复。

        这样做:

        def topn(d,n):
            res=[]
            v=d.values()
            k=d.keys()
            sl=[[k[x] for x,z in enumerate(v) if z==i] for i in sorted(v)]
            while len(res)<n and sl:
                e=sl.pop()
                if e not in res:
                    res.append(e)
        
            return res
        
        >>> d={k:v for k,v in zip((chr(i+97) for i in range(26)),range(1,27))}
        >>> d.update({k:v for k,v in zip((chr(i+65) for i in range(0,26,2)),range(1,27,2))})  
        >>> topn(d,10)
        [['z'], ['Y', 'y'], ['x'], ['W', 'w'], ['v'], ['U', 'u'], ['t'], ['S', 's'], ['r'], ['Q', 'q']]
        

        【讨论】:

          【解决方案6】:

          Bakuriu 的回答是正确的(使用 heapq.nlargest)。

          但如果您对使用正确的算法感兴趣,quickselect 使用与快速排序类似的原理,并且是由同一个人发明的:C.A.R.霍尔。

          但是,它的不同之处在于没有对数组进行完全排序:完成后,如果您要求前 n 个元素,那么它们位于数组中的前 n 个位置,但不一定按排序顺序。

          与快速排序一样,它首先选择一个枢轴元素并旋转数组,以使所有 a[:j] 小于或等于 a[j] 并且所有 a[j+1:] 都大于 a[j ].

          接下来,如果 j == n,那么最大的元素是 a[:j]。如果 j > n,则仅在枢轴左侧的元素上递归调用快速选择。如果 j

          因为 quickselect 只在数组的一侧被递归调用(不像 quicksort 在两边都被递归调用),它在线性时间内工作(如果输入是随机排序的,并且没有重复的键)。这也有助于将递归调用转换为 while 循环。

          这里有一些代码。为了帮助理解它,外部 while 循环中的不变量是元素 xs[:lo] 保证在 n 最大的列表中,并且元素 xs[hi:] 保证不在 n 最大的列表中。

          import random
          
          def largest_n(xs, n):
              lo, hi = 0, len(xs)
              while hi > n:
                  i, j = lo, hi
                  # Pivot the list on xs[lo]
                  while True:
                      while i < hi and xs[i] >= xs[lo]:
                          i += 1
                      j -= 1
                      while j >= lo and xs[j] < xs[lo]:
                          j -= 1
                      if i > j:
                          break
                      xs[i], xs[j] = xs[j], xs[i]
                  # Move the pivot to xs[j]
                  if j > lo:
                      xs[lo], xs[j] = xs[j], xs[lo]
                  # Repeat on one side or the other based on the location of the pivot.
                  if n <= j:
                      hi = j
                  else:
                      lo = j + 1
              return xs[:n]
          
          
          for k in xrange(100):
              xs = range(1000)
              random.shuffle(xs)
              xs = largest_n(xs, 10)
              assert sorted(xs) == range(990, 1000)
              print xs
          

          【讨论】:

          • 在最坏的情况下,快速选择可以持续到 O(n^2),尽管除非选择的枢轴是最大元素,否则这种情况不会经常发生。我实施了中位数的中位数和快速选择,我还想让堆进行比较。为了克服快速选择,您可以从列表中获取大约 3 个随机数,并将它们的中值作为枢轴。
          【解决方案7】:

          如果字典保持常量,则不要尝试直接创建heapq 或通过collections.Counter 创建一个collections.Counter,您可以尝试使用值作为键以相反的顺序对字典项进行排序,然后然后从中获取前 10 个元素。您需要从元组重新创建字典

          >>> some_dict = {string.ascii_lowercase[random.randint(0,23):][:3]:random.randint(100,300) for _ in range(100)}
          >>> some_dict
          {'cde': 262, 'vwx': 175, 'xyz': 163, 'uvw': 288, 'qrs': 121, 'mno': 192, 'ijk': 103, 'abc': 212, 'wxy': 206, 'efg': 256, 'opq': 255, 'tuv': 128, 'jkl': 158, 'pqr': 291, 'fgh': 191, 'lmn': 259, 'rst': 140, 'hij': 192, 'nop': 202, 'bcd': 258, 'klm': 145, 'stu': 293, 'ghi': 264, 'def': 260}
          >>> sorted(some_dict.items(), key = operator.itemgetter(1), reverse = True)[:10]
          [('stu', 293), ('pqr', 291), ('uvw', 288), ('ghi', 264), ('cde', 262), ('def', 260), ('lmn', 259), ('bcd', 258), ('efg', 256), ('opq', 255)]
          

          如果您使用 heapq,要创建堆,则需要 nlogn 操作,如果您通过插入元素或 logn 来构建列表,则需要 mlogn 操作来获取顶m元素

          如果您对项目进行排序,Python 排序算法在最坏的情况下保证为O(nlogn)(参考TIM Sort),并且获取前 10 个元素将是一个常量操作

          【讨论】:

          • 实际上构建堆的是O(n)而不是O(nlogn)。来自文档:“heapify(x) # 将列表转换为堆,就地,在线性时间
          【解决方案8】:

          对于获取前10个元素,假设数字在第二位:

          from operator import itemgetter
          
          topten = sorted(mydict.items(), key=itemgetter(1), reverse = True)[0:10]
          

          如果您想按值排序,则只需将其更改为key=itemgetter(1,0)

          对于数据结构,堆听起来就像您想要的那样。只需将它们保留为元组,然后比较数字项。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-05-22
            • 2019-04-09
            • 2019-06-21
            • 2013-03-05
            • 2016-08-05
            • 2021-05-08
            相关资源
            最近更新 更多