Open in Colab

Double Ended Queue#

The Queue Abstract Data Type#

Same idea as Queue, but now you can insert people in the front of the queue (cut-queue 😂) and also remove people from the end of the queue (people leaving cause queue too long?).

Note possible confusion in the method names below, as add_front really means insert an item at the front the queue but it uses append because we treat the rear of a list as the “front”.

Python Implementation#

from __future__ import annotations

from typing import Generic, TypeVar, List

T = TypeVar("T")


class DeQueueList(Generic[T]):
    """Creates a double-ended queue that uses python's default list as the underlying
    data structure.

    Attributes:
        queue_items (List[T]): The list that stores the items in the queue.
            We treat the end of the list as the start of the queue and
            the start of the list as the end of the queue.
    """

    _queue_items: List[T]

    def __init__(self) -> None:
        self._queue_items = []

    def __len__(self) -> int:
        """Return the size of the dequeue."""
        return len(self.queue_items)

    @property
    def queue_items(self) -> List[T]:
        """Read only property for the queue items."""
        return self._queue_items

    @property
    def size(self) -> int:
        """Return the size of the queue.

        Returns:
            (int): The size of the queue.
        """
        return len(self)

    def is_empty(self) -> bool:
        """Check if queue is empty.

        Returns:
            (bool): True if queue is empty, False otherwise.
        """
        return self.size == 0

    def add_front(self, item: T) -> None:
        """Insert an item at the front of the queue.

        Args:
            item (T): The current item to be added.
        """
        self.queue_items.append(item)

    def add_rear(self, item: T) -> None:
        """Insert an item at the end of the queue.

        In this implementation, the item is inserted at the start of the list.

        Args:
            item (T): The current item to be queued.
        """
        self.queue_items.insert(0, item)

    def remove_front(self) -> T:
        """Pop an item from the start of the queue.

        In this implementation, the item at the end of the list is returned and removed.
        We are using the list's pop method to do this.

        Raises:
            (Exception): If queue is empty.

        Returns:
            (T): The item at the start of the queue.
        """
        if self.is_empty():
            raise Exception("Queue is empty")
        return self.queue_items.pop()

    def remove_rear(self) -> T:
        """Pop an item from the end of the queue.

        Raises:
            (Exception): If queue is empty.

        Returns:
            (T): The item at the end of the queue.
        """
        if self.is_empty():
            raise Exception("Queue is empty")
        return self.queue_items.pop(0)

We push 4 items in this sequence 4, dog, True, 8.4 and now the “top” of the stack is 8.4.

So as we pop them, it goes from 8.4, True, dog, 4.

q = DeQueueList()
print(q.is_empty())

q.add_rear(4)
q.add_rear("dog")
q.add_front("cat")
q.add_front(True)
print(q.size)
print(q.is_empty())
q.add_rear(8.4)
print(q.remove_rear())
print(q.remove_front())
True
4
False
8.4
True

Time Complexity#

Table 63 Time Complexity#

Operations

Time Complexity

add_rear

\(\O(n)\)

add_front

\(\O(1)\)

remove_rear

\(\O(n)\)

remove_front

\(\O(1)\)

With reference to the previous section on Concept, the time complexity for both add_rear (enqueue) and dequeue (remove_front) are \(\O(n)\) and \(\O(1)\), an obvious consequence because the native python list’s operations insert and pop are \(\O(n)\) and \(\O(1)\) respectively, so the result follows. The same applies to add_front and remove_rear.

Space Complexity#

Space complexity: \(\O(n)\). The space required depends on the number of items stored in the list queue_items, so if queue_items stores up to \(n\) items, then space complexity is \(\O(n)\).

Further Readings#