Implementing a Versatile Queue in Python: Front, Middle, Back Operations

Implementing a Versatile Queue in Python: Front, Middle, Back Operations

πŸ’‘ Problem Formulation: Implementing a queue in programming involves processing elements in a first-in, first-out (FIFO) manner. However, a conventional queue has limitations in flexibility. This article focuses on programming a Python queue that not only allows for traditional push (enqueue) and pop (dequeue) from the front but also facilitates these operations from the middle and back. The goal is to create an adaptable data structure which could, for example, enqueue an element at the back with push_back(element), dequeue from the middle with pop_middle(), or push to the front using push_front(element).

Method 1: Using a Deque

This method involves utilizing Python’s collections.deque, a double-ended queue which allows us to efficiently append and pop elements from both ends. It’s ideal for our versatile queue as it can be modified to perform middle operations with additional logic.

Here’s an example:

from collections import deque

class VersatileQueue(deque):
    def push_front(self, x):
        self.appendleft(x)
    
    def push_middle(self, x):
        self.insert(len(self) // 2, x)
    
    def push_back(self, x):
        self.append(x)
    
    def pop_front(self):
        return self.popleft()
    
    def pop_middle(self):
        return self.pop(len(self) // 2)
    
    def pop_back(self):
        return self.pop()

# Example usage:
v_queue = VersatileQueue()
v_queue.push_front(1)
v_queue.push_middle(2)
v_queue.push_back(3)
print(v_queue.pop_front())
print(v_queue.pop_middle())
print(v_queue.pop_back())

Output:

1
2
3

This code snippet introduces the VersatileQueue class, which extends Python’s deque. It showcases custom methods to push and pop elements at the front, middle and back. The push_middle and pop_middle methods find the middle index by integer division of the current queue length, allowing the class to perform the desired middle operations.

Method 2: Using a List

Here we take advantage of Python’s built-in list data structure to implement the queue. Although not as efficient as a deque for all operations, lists provide an intuitive means of accessing and modifying data at any index.

Here’s an example:

class ListQueue:
    def __init__(self):
        self.queue = []
    
    def push_front(self, x):
        self.queue.insert(0, x)
    
    def push_middle(self, x):
        self.queue.insert(len(self.queue) // 2, x)
    
    def push_back(self, x):
        self.queue.append(x)
    
    def pop_front(self):
        return self.queue.pop(0) if self.queue else None
    
    def pop_middle(self):
        return self.queue.pop(len(self.queue) // 2) if self.queue else None
    
    def pop_back(self):
        return self.queue.pop() if self.queue else None

# Example usage:
l_queue = ListQueue()
l_queue.push_front('a')
l_queue.push_middle('b')
l_queue.push_back('c')
print(l_queue.pop_front())
print(l_queue.pop_middle())
print(l_queue.pop_back())

Output:

a
b
c

This code demonstrates the ListQueue class using a Python list and manipulating indices to perform the required operations. The trade-off is that certain operations, particularly push_front, are less efficient than with a deque due to the underlying dynamic array data structure that requires shifting all elements in the list.

Method 3: Custom Node-Based Queue

The third method entails creating a custom queue with linked nodes. Each node has pointers to the previous and next nodes. This method offers good performance on all operations but requires more coding effort.

Here’s an example:

class Node:
    def __init__(self, value):
        self.value = value
        self.prev = self.next = None

class NodeQueue:
    def __init__(self):
        self.head = self.tail = None
        self.size = 0
    
    def _find_middle(self):
        mid = self.head
        for _ in range(self.size // 2):
            mid = mid.next
        return mid
    
    def push_front(self, x):
        new_node = Node(x)
        new_node.next = self.head
        if self.head:
            self.head.prev = new_node
        self.head = new_node
        if self.size == 0:
            self.tail = new_node
        self.size += 1
    
    def push_middle(self, x):
        new_node = Node(x)
        if not self.head:
            self.head = self.tail = new_node
        else:
            mid_prev_node = self._find_middle().prev
            new_node.next = mid_prev_node.next
            new_node.prev = mid_prev_node
            mid_prev_node.next.prev = new_node
            mid_prev_node.next = new_node
        self.size += 1
    
    def push_back(self, x):
        new_node = Node(x)
        new_node.prev = self.tail
        if self.tail:
            self.tail.next = new_node
        self.tail = new_node
        if self.size == 0:
            self.head = new_node
        self.size += 1
    
    def pop_front(self):
        if not self.head:
            return None
        ret_value = self.head.value
        self.head = self.head.next
        if self.head:
            self.head.prev = None
        self.size -= 1
        return ret_value
    
    def pop_middle(self):
        if not self.head:
            return None
        mid_node = self._find_middle()
        ret_value = mid_node.value
        if mid_node.prev:
            mid_node.prev.next = mid_node.next
        if mid_node.next:
            mid_node.next.prev = mid_node.prev
        self.size -= 1
        return ret_value
    
    def pop_back(self):
        if not self.tail:
            return None
        ret_value = self.tail.value
        self.tail = self.tail.prev
        if self.tail:
            self.tail.next = None
        self.size -= 1
        return ret_value

# Example usage:
n_queue = NodeQueue()
n_queue.push_front(7)
n_queue.push_middle(8)
n_queue.push_back(9)
print(n_queue.pop_front())
print(n_queue.pop_middle())
print(n_queue.pop_back())

Output:

7
8
9

This custom implementation of a NodeQueue utilizes individual Node objects with prev and next pointers. The push_middle and pop_middle methods include internal methods to locate the middle node. This structure excels at frequent insertions and deletions at all points, with direct reference manipulation rather than shifting entire data blocks, but the code base is more complex and prone to errors if not handled carefully.

Bonus One-Liner Method 4: List Slicing

Python’s list slicing is a powerful feature that can be leveraged for an elegant one-liner queue capable of pushing and popping elements from front, middle, and back.

Here’s an example:

queue = []

# Push front, middle, back:
queue.insert(0, 'front')
queue[len(queue)//2:len(queue)//2] = ['middle']
queue.append('back')

# Pop front, middle, back:
print(queue.pop(0))
print(queue.pop(len(queue)//2))
print(queue.pop())

Output:

front
middle
back

Using slicing and list methods, this code manipulates a regular Python list to perform the desired operations. It’s elegant and concise but lacks the structured approach of a dedicated queue class, potentially leading to less readable and maintainable code in complex scenarios.

Summary/Discussion

  • Method 1: Deque. Highly efficient for append and pop operations at both ends. Middle operations are less efficient but feasible.
  • Method 2: List. Intuitive and straightforward, but some operations are not time-efficient due to data shifting.
  • Method 3: Node-Based Queue. Highly efficient and flexible for all operations but more complex to implement and maintain.
  • Bonus Method 4: List Slicing. Elegant for simple scenarios or quick scripts but may become unwieldy in larger, more complex applications.

This method involves utilizing Python’s collections.deque, a double-ended queue which allows us to efficiently append and pop elements from both ends. It’s ideal for our versatile queue as it can be modified to perform middle operations with additional logic.

Here’s an example:

from collections import deque

class VersatileQueue(deque):
    def push_front(self, x):
        self.appendleft(x)
    
    def push_middle(self, x):
        self.insert(len(self) // 2, x)
    
    def push_back(self, x):
        self.append(x)
    
    def pop_front(self):
        return self.popleft()
    
    def pop_middle(self):
        return self.pop(len(self) // 2)
    
    def pop_back(self):
        return self.pop()

# Example usage:
v_queue = VersatileQueue()
v_queue.push_front(1)
v_queue.push_middle(2)
v_queue.push_back(3)
print(v_queue.pop_front())
print(v_queue.pop_middle())
print(v_queue.pop_back())

Output:

1
2
3

This code snippet introduces the VersatileQueue class, which extends Python’s deque. It showcases custom methods to push and pop elements at the front, middle and back. The push_middle and pop_middle methods find the middle index by integer division of the current queue length, allowing the class to perform the desired middle operations.

Method 2: Using a List

Here we take advantage of Python’s built-in list data structure to implement the queue. Although not as efficient as a deque for all operations, lists provide an intuitive means of accessing and modifying data at any index.

Here’s an example:

class ListQueue:
    def __init__(self):
        self.queue = []
    
    def push_front(self, x):
        self.queue.insert(0, x)
    
    def push_middle(self, x):
        self.queue.insert(len(self.queue) // 2, x)
    
    def push_back(self, x):
        self.queue.append(x)
    
    def pop_front(self):
        return self.queue.pop(0) if self.queue else None
    
    def pop_middle(self):
        return self.queue.pop(len(self.queue) // 2) if self.queue else None
    
    def pop_back(self):
        return self.queue.pop() if self.queue else None

# Example usage:
l_queue = ListQueue()
l_queue.push_front('a')
l_queue.push_middle('b')
l_queue.push_back('c')
print(l_queue.pop_front())
print(l_queue.pop_middle())
print(l_queue.pop_back())

Output:

a
b
c

This code demonstrates the ListQueue class using a Python list and manipulating indices to perform the required operations. The trade-off is that certain operations, particularly push_front, are less efficient than with a deque due to the underlying dynamic array data structure that requires shifting all elements in the list.

Method 3: Custom Node-Based Queue

The third method entails creating a custom queue with linked nodes. Each node has pointers to the previous and next nodes. This method offers good performance on all operations but requires more coding effort.

Here’s an example:

class Node:
    def __init__(self, value):
        self.value = value
        self.prev = self.next = None

class NodeQueue:
    def __init__(self):
        self.head = self.tail = None
        self.size = 0
    
    def _find_middle(self):
        mid = self.head
        for _ in range(self.size // 2):
            mid = mid.next
        return mid
    
    def push_front(self, x):
        new_node = Node(x)
        new_node.next = self.head
        if self.head:
            self.head.prev = new_node
        self.head = new_node
        if self.size == 0:
            self.tail = new_node
        self.size += 1
    
    def push_middle(self, x):
        new_node = Node(x)
        if not self.head:
            self.head = self.tail = new_node
        else:
            mid_prev_node = self._find_middle().prev
            new_node.next = mid_prev_node.next
            new_node.prev = mid_prev_node
            mid_prev_node.next.prev = new_node
            mid_prev_node.next = new_node
        self.size += 1
    
    def push_back(self, x):
        new_node = Node(x)
        new_node.prev = self.tail
        if self.tail:
            self.tail.next = new_node
        self.tail = new_node
        if self.size == 0:
            self.head = new_node
        self.size += 1
    
    def pop_front(self):
        if not self.head:
            return None
        ret_value = self.head.value
        self.head = self.head.next
        if self.head:
            self.head.prev = None
        self.size -= 1
        return ret_value
    
    def pop_middle(self):
        if not self.head:
            return None
        mid_node = self._find_middle()
        ret_value = mid_node.value
        if mid_node.prev:
            mid_node.prev.next = mid_node.next
        if mid_node.next:
            mid_node.next.prev = mid_node.prev
        self.size -= 1
        return ret_value
    
    def pop_back(self):
        if not self.tail:
            return None
        ret_value = self.tail.value
        self.tail = self.tail.prev
        if self.tail:
            self.tail.next = None
        self.size -= 1
        return ret_value

# Example usage:
n_queue = NodeQueue()
n_queue.push_front(7)
n_queue.push_middle(8)
n_queue.push_back(9)
print(n_queue.pop_front())
print(n_queue.pop_middle())
print(n_queue.pop_back())

Output:

7
8
9

This custom implementation of a NodeQueue utilizes individual Node objects with prev and next pointers. The push_middle and pop_middle methods include internal methods to locate the middle node. This structure excels at frequent insertions and deletions at all points, with direct reference manipulation rather than shifting entire data blocks, but the code base is more complex and prone to errors if not handled carefully.

Bonus One-Liner Method 4: List Slicing

Python’s list slicing is a powerful feature that can be leveraged for an elegant one-liner queue capable of pushing and popping elements from front, middle, and back.

Here’s an example:

queue = []

# Push front, middle, back:
queue.insert(0, 'front')
queue[len(queue)//2:len(queue)//2] = ['middle']
queue.append('back')

# Pop front, middle, back:
print(queue.pop(0))
print(queue.pop(len(queue)//2))
print(queue.pop())

Output:

front
middle
back

Using slicing and list methods, this code manipulates a regular Python list to perform the desired operations. It’s elegant and concise but lacks the structured approach of a dedicated queue class, potentially leading to less readable and maintainable code in complex scenarios.

Summary/Discussion

  • Method 1: Deque. Highly efficient for append and pop operations at both ends. Middle operations are less efficient but feasible.
  • Method 2: List. Intuitive and straightforward, but some operations are not time-efficient due to data shifting.
  • Method 3: Node-Based Queue. Highly efficient and flexible for all operations but more complex to implement and maintain.
  • Bonus Method 4: List Slicing. Elegant for simple scenarios or quick scripts but may become unwieldy in larger, more complex applications.