【问题标题】:Efficient implementation of queue - time complexity of enqueue and dequeue队列的高效实现——入队和出队的时间复杂度
【发布时间】:2019-01-02 17:56:52
【问题描述】:

我目前正在阅读一本关于数据结构/算法的教科书。练习之一是使用 python 列表结构实现一个高效的队列:入队和出队的时间复杂度平均需要 O(1)。这本书说,对于特定的出队情况,时间复杂度应该只有 O(n),其余时间应该是 O(1)。我实现了它,队列的后部是列表的末尾,队列的前部是列表的开头;当我使一个元素出队时,我不会从列表中删除它,而是简单地增加一个计数器,以便该方法知道列表中的哪个元素代表队列的前面。这是我的代码:

class FasterQueue:
    def __init__(self):
        self.items = []
        self.index = 0
    def enqueue(self, item):
        self.items.append(item)
    def dequeue(self):
        index = self.index
        self.index += 1
        return self.items[index]
    def isEmpty(self):
        return self.items == []
    def size(self):
        return len(self.items)

我的问题:这本书说在某些情况下出队应该采用 O(1)。我不知道这是什么情况,因为似乎出队总是会在某个索引处获得值。我的队列实现是无效的还是我错过了其他东西?还是教科书只是在寻找另一种更常见的实现方式?

非常感谢您的帮助。

【问题讨论】:

  • dequeue 应该始终从队列的一端删除该项目。那是O(1)
  • @JacobIRR 他没有使用典型的python列表,实现代表一个队列。队列是先进先出的,因此出队应该从队列的开头移除,而不是结尾。
  • 我想我能想到的唯一一件事是,当您尝试从“空”队列中出队时会发生什么。入队一次,出队两次。
  • 这不是 O(1),因为获取列表项的最坏情况是 O(1)...我猜教科书只是犯了一个错误?
  • 我认为这实际上可能是一个索引错误,但是是的,我真的没有看到任何“理由”有一个令人难以置信的长列表,它不断消耗内存但出队为 O(1),但我怀疑这本书希望您在清空列表或其他内容时整理列表。

标签: python data-structures queue time-complexity


【解决方案1】:

使用更多 Python 风格的功能,我会这样做:

class FasterQueue:
    def __init__(self):
        self.items = []
        self.index = 0

    def enqueue(self, item):
        self.items.append(item)

    def dequeue(self):
        # do this first so IndexErrors don't cause us ignore items
        obj = self.items[self.index]
        # release the reference so we don't "leak" memory
        self.items[self.index] = None
        self.index += 1
        return obj

    def isEmpty(self):
        return self.index == len(self.items)

    def size(self):
        return len(self.items)

    def try_shrink(self):
        nactive = len(self.items) - self.index
        if nactive + 2 < self.index // 2:
            self.items = self.items[self.index:]
            self.index = 0

添加了一个try_shrink 方法来尝试释放已用的内存空间,在dequeue() 的末尾调用它可能会很有用——否则列表将任意增长并浪费大量内存。那里的常数并不惊人,但应该防止它经常收缩。这个操作将是 O(n) 并且可能是暗示的内容

【讨论】:

  • 我确实对您对 dequeue 方法中的引用做了什么有疑问。在重新分配数组 None 中的索引后,我对 obj 如何仍然引用原始对象感到有些困惑。返回时为什么不是obj None?
  • @davidim 请参阅youtube.com/watch?v=_AEJHKGk9ns 了解您的问题。我也刚刚意识到我没有回答您关于“队列的实现无效或我错过了什么”的问题!基本上没问题,从其他回复中可以看出,它周围有各种警告,但它看起来是有效的。我会看看我是否可以在这里把一些实现的时间安排在一起
  • 感谢您的链接和回复!
  • 现在,我明白了!您是在告诉 obj 引用 self.items[self.index] 所引用的同一对象。然后你将 self.items[self.index] 重新绑定到 None,但这实际上并没有修改对象!非常感谢。
【解决方案2】:

根据我的理解,入队应该在最后插入,出队应该从头开始删除。 所以代码应该是

class FasterQueue:
def __init__(self):
    self.items = []

def enqueue(self, item):
    self.items.append(item)

def dequeue(self):
    if self.items:
        return self.items.pop(0)
    print("Underflow")

def isEmpty(self):
    return self.items == []

def size(self):
    return len(self.items)

【讨论】:

    【解决方案3】:

    O(n) 是管理列表长度的必要结果。

    这里有一个解决方案。通常,它以 O(1) 运行,并且有时它是 O(n),这是由于 dequeue 方法内部发生的额外步骤。

    当列表变得太大并触发清理时,会发生 O(n) 步骤。请注意,通常,这应该在 dequeue 方法中专门完成。在外面做,往往会更复杂,效率更低。

    class FasterQueue:
        def __init__(self, maxwaste=100):
            self.items = []
            self.nout = 0
            self.maxwaste = maxwaste
        def enqueue(self, item):
            self.items.append(item)
        def dequeue(self):
            if len(self.items):
                retv = self.items[self.nout]
                self.nout += 1
                if self.nout >= self.maxwaste:
                    self.items = self.items[self.nout:]
                    self.nout =0
                return retv
            else:
                print( 'empty' )
                raise ValueError
        def listQ(self):
            return ' '.join( self.items[self.nout:] )
        def isEmpty(self):
            return self.nout == len(self.items)
        def size(self):
            return len(self.items) - self.nout
    
    q = FasterQueue(5)
    
    for n in range(10):
        q.enqueue( str(n) )
    
    print( 'queue size %d  nout %d items %s'%(q.size(),q.nout,q.listQ()) )
    print( q.items )
    
    while True:
        try:
            print( 'dequeue %s'%q.dequeue() )
            print( 'queue size %d  nout %d items %s'%(q.size(),q.nout,q.listQ()) )
            print( q.items )
        except:
            print( 'empty' )
            break
    

    运行上述代码会产生以下输出,注意当超过 maxwaste 时回收浪费的内存。为了演示操作,此处将 Maxwaste 设置得很小。

    queue size 10  nout 0 items 0 1 2 3 4 5 6 7 8 9
    ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
    dequeue 0
    queue size 9  nout 1 items 1 2 3 4 5 6 7 8 9
    ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
    dequeue 1
    queue size 8  nout 2 items 2 3 4 5 6 7 8 9
    ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
    dequeue 2
    queue size 7  nout 3 items 3 4 5 6 7 8 9
    ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
    dequeue 3
    queue size 6  nout 4 items 4 5 6 7 8 9
    ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
    dequeue 4
    queue size 5  nout 0 items 5 6 7 8 9
    ['5', '6', '7', '8', '9']
    dequeue 5
    queue size 4  nout 1 items 6 7 8 9
    ['5', '6', '7', '8', '9']
    dequeue 6
    queue size 3  nout 2 items 7 8 9
    ['5', '6', '7', '8', '9']
    dequeue 7
    queue size 2  nout 3 items 8 9
    ['5', '6', '7', '8', '9']
    dequeue 8
    queue size 1  nout 4 items 9
    ['5', '6', '7', '8', '9']
    dequeue 9
    queue size 0  nout 0 items 
    []
    empty
    empty
    

    【讨论】:

      【解决方案4】:

      为了完整起见,这里是使用环形缓冲区的答案。

      这个例子永远以 O(1) 运行,但它通过限制队列的大小来做到这一点。如果您想允许队列增长或动态调整大小,那么您将再次拥有 O(n) 行为。

      换句话说,只有当你不以某种方式管理列表的大小时,你才有 O(1)。这就是问题的重点。

      好的,这里是固定队列长度的环形缓冲区实现。

      class FasterQueue:
          def __init__(self, nsize=100):
              self.items = [0]*nsize
              self.nin = 0
              self.nout = 0
              self.nsize = nsize
          def enqueue(self, item):
              next = (self.nin+1)%self.nsize
              if next != self.nout:
                  self.items[self.nin] = item
                  self.nin = next
                  print self.nin, item
              else:
                  raise ValueError
          def dequeue(self):
              if self.nout != self.nin:
                  retv = self.items[self.nout]
                  self.nout = (self.nout+1)%self.nsize
                  return retv
              else:
                  raise ValueError
      
          def printQ(self):
              if self.nout < self.nin:
                  print( ' '.join(self.items[self.nout:self.nin]) )
              elif self.nout > self.nin:
                  print( ' '.join(self.items[self.nout:]+self.items[:self.nin]) )
      
          def isEmpty(self):
              return self.nin == self.nout
          def size(self):
              return (self.nin - self.nout + self.nsize)%self.nsize
      
      q = FasterQueue()
      
      q.enqueue( 'a' )
      q.enqueue( 'b' )
      q.enqueue( 'c' )
      
      print( 'queue items' )
      q.printQ()
      print( 'size %d'%q.size() )
      
      
      while True:
          try:
              print( 'dequeue %s'%q.dequeue() )
              print( 'queue items' )
              q.printQ()
          except:
              print( 'empty' )
              break
      

      【讨论】:

      • @SamMason,这是答案。我很确定它必须是一个戒指或一个游泳池。这是一个经典的问题。我在 DSP 和设备驱动程序中处理过很多次。我最初使用 pop() 的想法假设 python 是通过引用和使用池而不是通过复制来做到这一点的。
      • 是的,我在低级语言中使用过类似的实现,它也可以通过链表在 Python 中实现。从问题的表述来看,似乎 OP 并没有在“生产就绪”实施之后,只是想了解所有部分是如何组合在一起的
      • @SamMason 尽管如此,将列表继续到无穷大除了避免实际删除项目的 O(n) 成本外,没有任何用处。这就是问题的教学重点
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-08-24
      • 2018-03-13
      • 1970-01-01
      • 2014-08-22
      • 1970-01-01
      • 1970-01-01
      • 2013-07-07
      相关资源
      最近更新 更多