1
votes

I am currently reading a textbook on data structures/algorithms. One of the exercises is to implement a efficient queue using the python list structure: the time complexity of both enqueue and dequeue needs to be O(1) on average. The book says that the time complexity should only be O(n) for a specific case of dequeue, and the rest of the time it should be O(1). I implemented it such that the rear of the queue is the end of the list and front of the queue is the beginning of the list; when I dequeue an element, I do not delete it from the list, but I simply increment a counter so that the method will know which element in the list represents the front of the queue. Here is my code:

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)

My question: the book says that there is some case where dequeue should take O(1). I have no idea what case this is because it seems like dequeue will always just be getting the value at a certain index. Is my implementation of the queue invalid or am I missing something else? Or is the textbook just looking for another more common implementation?

Thank you so much for the help.

4
dequeue should always remove the item from one end of the queue. That's O(1)JacobIRR
@JacobIRR He's not working with a typical python list, the implementation represents a queue. queues are FIFO, and so dequeue should remove from the start of queue, not the end.Paritosh Singh
i guess the only thing i can think of is, what happens when you try to dequeue past an "empty" queue so to speak. enqueue once, dequeue twice.Paritosh Singh
Wouldn't that still be O(1) since the worst case on get item for lists is O(1)... I guess the textbook just made a mistake?davidim
i think it would probably be an index error actually, however yes, i dont really see any "reason" to have an incredibly long list that keeps consuming memory but dequeues as O(1), but i suspect the book expects you to perhaps tidy up the list when you empty it out or something.Paritosh Singh

4 Answers

0
votes

making this use some more Python-esque features, I'd do something like:

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

have added a try_shrink method that tries to free up used memory space, and it might be useful to call this at the end of dequeue() — otherwise the list will grow arbitrarily long and waste a large amount of memory. the constants in there aren't amazing, but should prevent it from shrinking too often. this operation would be O(n) and might be what was being alluded to

0
votes

As per my understanding, enqueue should insert at the end and dequeue should delete from the start. So code should be

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)
0
votes

The O(n) is a necessary consequence of managing the list length.

Here is a solution. In general, this runs as O(1), and from time to time it is O(n) as a result of an extra step that occurs inside the dequeue method.

The O(n) step occurs when the list grows too large and triggers the cleanup. Note that in general, this should be done specifically inside the dequeue method. Doing it outside, will tend to be more elaborate and less efficient.

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

Running the above code produces the following output, note the recovery of wasted memory when maxwaste is exceeded. Maxwaste is set small here for the purpose of demonstrating the operation.

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
0
votes

For completeness, here is an answer using a ring buffer.

This example runs as O(1) forever, but it does this by limiting the size of the queue. If you want to allow the queue to grow, or size dynamically, then you again have your O(n) behavior.

In other words, you have O(1) only if you do not manage the size of the list in some way. That is the point of the question.

Okay, here is the ring buffer implementation with fixed queue length.

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