How to Use the Doubly Ended Queue (Deque) with Python

Avatar

By squashlabs, Last Updated: July 9, 2023

How to Use the Doubly Ended Queue (Deque) with Python

What is a Deque?

A deque (pronounced “deck”) is short for a doubly ended queue. It is an abstract data type that allows us to insert and remove elements from both ends. This means that a deque can be used as both a queue and a stack.

In a deque, elements can be added or removed from either the front or the rear end, providing us with the flexibility to implement various data structures efficiently. This makes deques a powerful tool in solving many programming problems.

The Python programming language provides a built-in module called collections that includes a class called deque. This class allows us to create and manipulate deques easily.

To use the deque class, we need to import it from the collections module:

from collections import deque

Once imported, we can create a deque object by calling the deque() function:

my_deque = deque()

Now, we have an empty deque object called my_deque. We can start adding elements to it using the append() method:

my_deque.append(10)     # Insert 10 at the rear end
my_deque.appendleft(5)  # Insert 5 at the front end

Similarly, we can remove elements from the deque using the pop() method:

rear_element = my_deque.pop()       # Remove and return the rear element
front_element = my_deque.popleft()  # Remove and return the front element

The pop() method removes and returns the last element from the deque, while the popleft() method removes and returns the first element.

Deques also support other useful methods such as extend(), extendleft(), rotate(), and clear(), which provide additional functionality for manipulating and managing the deque.

Related Article: How To Convert a Dictionary To JSON In Python

Creating a Deque

To create a deque in Python, you can use the collections module that comes with the standard library. The collections module provides a class called deque which represents a double-ended queue.

To begin, you need to import the deque class from the collections module:

from collections import deque

Once you have imported the deque class, you can create a new deque object by calling the class constructor:

my_deque = deque()

By default, the deque class creates an empty deque. However, you can also initialize a deque with some initial values by passing an iterable as an argument to the constructor. For example, to create a deque with the elements 1, 2, and 3, you can do:

my_deque = deque([1, 2, 3])

It’s important to note that a deque can contain elements of any data type, including strings, numbers, or even other objects.

Once you have created a deque, you can perform various operations on it. For instance, you can add elements to the deque using the append() method:

my_deque.append(4)
my_deque.appendleft(0)

The append() method adds an element to the right end of the deque, while the appendleft() method adds an element to the left end of the deque.

Similarly, you can remove elements from the deque using the pop() and popleft() methods:

element = my_deque.pop()
element = my_deque.popleft()

The pop() method removes and returns the rightmost element from the deque, while the popleft() method removes and returns the leftmost element from the deque.

You can also access elements in a deque using indexing, just like you would with a list or tuple:

element = my_deque[0]

In addition to these basic operations, the deque class provides other useful methods, such as extend(), extendleft(), rotate(), and reverse(). You can refer to the Python documentation for more information on these methods.

Adding and Removing Elements

The Python deque class provides several methods to add and remove elements from both ends of the queue. Let’s explore some of these methods:

Adding elements:

To add elements to the left end of the deque, you can use the appendleft() method. This method takes an element as an argument and adds it to the beginning of the deque.

from collections import deque

deque_obj = deque([1, 2, 3])
deque_obj.appendleft(0)
print(deque_obj)  # Output: deque([0, 1, 2, 3])

Similarly, you can use the append() method to add elements to the right end of the deque. This method appends the given element to the end of the deque.

from collections import deque

deque_obj = deque([1, 2, 3])
deque_obj.append(4)
print(deque_obj)  # Output: deque([1, 2, 3, 4])

Removing elements:

To remove elements from the left end of the deque, you can use the popleft() method. This method removes and returns the leftmost element from the deque.

from collections import deque

deque_obj = deque([1, 2, 3])
removed_element = deque_obj.popleft()
print(removed_element)  # Output: 1
print(deque_obj)  # Output: deque([2, 3])

Similarly, you can use the pop() method to remove elements from the right end of the deque. This method removes and returns the rightmost element from the deque.

from collections import deque

deque_obj = deque([1, 2, 3])
removed_element = deque_obj.pop()
print(removed_element)  # Output: 3
print(deque_obj)  # Output: deque([1, 2])

It’s important to note that both popleft() and pop() methods raise an IndexError if the deque is empty.

In addition to the above methods, the deque class also provides the extendleft() and extend() methods to add multiple elements at once. The extendleft() method extends the deque by appending elements from an iterable to the left end, while the extend() method extends the deque by appending elements from an iterable to the right end.

from collections import deque

deque_obj = deque([1, 2, 3])
deque_obj.extendleft([-1, 0])
print(deque_obj)  # Output: deque([0, -1, 1, 2, 3])

deque_obj = deque([1, 2, 3])
deque_obj.extend([4, 5])
print(deque_obj)  # Output: deque([1, 2, 3, 4, 5])

With these methods, you have the flexibility to add and remove elements from both ends of the deque efficiently.

Accessing Elements

In Python, accessing elements in a deque is similar to accessing elements in a list. You can use indexing and slicing to retrieve elements from the deque.

To access a specific element at a given index, you can use the square bracket notation with the index:

from collections import deque

my_deque = deque(['apple', 'banana', 'cherry', 'durian'])
print(my_deque[0])  # Output: apple
print(my_deque[2])  # Output: cherry

Similarly, you can use negative indexing to access elements from the end of the deque:

print(my_deque[-1])  # Output: durian
print(my_deque[-2])  # Output: cherry

If you want to access a range of elements, you can use slicing. Slicing allows you to extract a portion of the deque by specifying a start and end index:

print(my_deque[1:3])  # Output: ['banana', 'cherry']

You can also omit the start or end index to slice from the beginning or until the end of the deque, respectively:

print(my_deque[:2])   # Output: ['apple', 'banana']
print(my_deque[2:])   # Output: ['cherry', 'durian']

It’s important to note that indexing and slicing operations on a deque have a time complexity of O(1), which means they are very efficient regardless of the size of the deque.

If you try to access an index that is out of range, Python will raise an IndexError. To avoid this, you can use the len() function to check the length of the deque before accessing elements:

if len(my_deque) > 0:
    print(my_deque[0])  # Output: apple
else:
    print("Deque is empty.")

In addition to accessing elements, you can also modify the values of elements in a deque by assigning new values to specific indices:

my_deque[1] = 'blueberry'
print(my_deque)  # Output: deque(['apple', 'blueberry', 'cherry', 'durian'])

Remember that deques are mutable, so you can modify their elements directly.

Related Article: How to Sort a Dictionary by Key in Python

Iterating over a Deque

Iterating over a Python deque is similar to iterating over a regular list. You can use a for loop or the built-in iter() function to iterate over the elements of a deque.

Here’s an example of using a for loop to iterate over a deque:

from collections import deque

# Create a deque
my_deque = deque(['a', 'b', 'c', 'd'])

# Iterate over the deque using a for loop
for elem in my_deque:
    print(elem)

Output:

a
b
c
d

You can also use the iter() function to create an iterator object and then use the next() function to iterate over the deque:

from collections import deque

# Create a deque
my_deque = deque(['a', 'b', 'c', 'd'])

# Create an iterator object
my_iter = iter(my_deque)

# Iterate over the deque using the iterator object
print(next(my_iter))  # Output: a
print(next(my_iter))  # Output: b
print(next(my_iter))  # Output: c
print(next(my_iter))  # Output: d

Output:

a
b
c
d

You can also use a while loop to iterate over a deque. In this case, you need to handle the StopIteration exception to avoid an error when there are no more elements to iterate over:

from collections import deque

# Create a deque
my_deque = deque(['a', 'b', 'c', 'd'])

# Create an iterator object
my_iter = iter(my_deque)

# Iterate over the deque using a while loop
while True:
    try:
        elem = next(my_iter)
        print(elem)
    except StopIteration:
        break

Output:

a
b
c
d

When iterating over a deque, the elements are accessed in the order they were added. If you add or remove elements from the deque while iterating, the iteration order may be affected. Therefore, it is generally recommended to avoid modifying the deque while iterating over it.

In this chapter, you learned how to iterate over a Python deque using a for loop, the iter() function, and a while loop. It is important to remember that the iteration order of a deque is based on the order of elements added.

Understanding the Underlying Data Structure

The Python deque is built on top of a doubly ended queue data structure. A doubly ended queue, also known as a deque, is a linear data structure that allows insertions and deletions from both ends.

Internally, a deque is implemented as a doubly linked list. Each node in the linked list contains a value and two pointers: one pointing to the previous node and another pointing to the next node. This allows for efficient operations at both ends of the deque.

Let’s take a closer look at the underlying data structure of a deque. Consider the following example:

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

class Deque:
    def __init__(self):
        self.head = None
        self.tail = None

    def appendleft(self, value):
        new_node = Node(value)
        if self.head is None:
            self.head = new_node
            self.tail = new_node
        else:
            new_node.next = self.head
            self.head.prev = new_node
            self.head = new_node

    def append(self, value):
        new_node = Node(value)
        if self.tail is None:
            self.head = new_node
            self.tail = new_node
        else:
            new_node.prev = self.tail
            self.tail.next = new_node
            self.tail = new_node

    def popleft(self):
        if self.head is None:
            raise IndexError("Deque is empty")
        value = self.head.value
        self.head = self.head.next
        if self.head is None:
            self.tail = None
        else:
            self.head.prev = None
        return value

    def pop(self):
        if self.tail is None:
            raise IndexError("Deque is empty")
        value = self.tail.value
        self.tail = self.tail.prev
        if self.tail is None:
            self.head = None
        else:
            self.tail.next = None
        return value

In the above example, we define a Node class that represents a node in the doubly linked list. Each node contains a value and two pointers: prev points to the previous node and next points to the next node.

The Deque class implements the operations of a deque. The appendleft() method adds a new node to the beginning of the deque, while the append() method adds a new node to the end of the deque. The popleft() method removes and returns the value of the first node in the deque, and the pop() method removes and returns the value of the last node in the deque.

By using a doubly linked list, a deque allows for efficient insertion and deletion operations at both ends. Inserting and deleting at the beginning or end of a doubly linked list can be done in constant time, O(1), as we only need to update a few pointers.

Understanding the underlying data structure of a deque helps us appreciate its versatility and efficiency in solving various problems. Whether it’s implementing a queue, a stack, or solving problems that require efficient insertions and deletions at both ends, the Python deque is a powerful tool in our programming arsenal.

Understanding the Advantages of a Deque

Understanding the advantages of using a deque can help you write more efficient and concise code.

1. Efficient Insertion and Deletion:
One of the key advantages of a deque is its ability to perform insertion and deletion operations in constant time at both ends. This makes it a suitable choice for scenarios where you need to frequently add or remove elements at either end of the queue. For example, you can efficiently implement a stack or a queue using a deque.

Here’s an example that demonstrates how to use a deque as a stack:

from collections import deque

stack = deque()
stack.append(1)  # push operation
stack.append(2)
stack.append(3)

while stack:
    print(stack.pop())  # pop operation, prints 3, 2, 1

2. Efficient Iteration and Access:
Another advantage of a deque is its efficient iteration and access to elements. Unlike lists, deques provide constant time complexity for accessing elements at both ends. This can be beneficial when you need to process the elements in a queue-like manner, such as implementing a breadth-first search algorithm.

Here’s an example that demonstrates how to use a deque for breadth-first search:

from collections import deque

graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': [],
    'F': []
}

def bfs(graph, start):
    visited = set()
    queue = deque()
    queue.append(start)

    while queue:
        node = queue.popleft()
        if node not in visited:
            visited.add(node)
            print(node)
            queue.extend(graph[node])

bfs(graph, 'A')  # prints A, B, C, D, E, F

3. Memory Efficiency:
Deques are also memory efficient compared to lists in scenarios where you frequently need to add or remove elements at both ends. Lists in Python are implemented as dynamic arrays, which may require occasional resizing to accommodate new elements. In contrast, deques are implemented as doubly-linked lists, which allows for efficient resizing without requiring large contiguous blocks of memory.

Here’s an example that demonstrates the memory efficiency of a deque:

from collections import deque

deque_size = 10**6
deque = deque(range(deque_size))

print(deque[0])  # prints 0
print(deque[-1])  # prints 999999

In this example, we create a deque of size 1 million and access the first and last elements. The deque efficiently handles the large number of elements without requiring excessive memory.

Related Article: How to Remove a Key from a Python Dictionary

Use Case: Implementing a Queue

In this chapter, we will explore a practical use case for the Python deque data structure by implementing a queue. A queue is a common data structure that follows the First-In, First-Out (FIFO) principle. Elements are added to the end of the queue and removed from the front.

To implement a queue using the deque, we can utilize its append() and popleft() methods. The append() method adds an element to the right end of the deque, and the popleft() method removes and returns the leftmost element of the deque.

Let’s start by importing the deque module from the collections library:

from collections import deque

Next, we can create an instance of the deque to represent our queue:

queue = deque()

To add elements to the queue, we use the append() method:

queue.append("apple")
queue.append("banana")
queue.append("cherry")

Now, our queue contains three elements: “apple”, “banana”, and “cherry”. The leftmost element is “apple”, and the rightmost element is “cherry”.

To remove elements from the queue, we use the popleft() method:

first_element = queue.popleft()
second_element = queue.popleft()

After executing these lines of code, the variables first_element and second_element will hold the values “apple” and “banana”, respectively. The queue will now contain only the element “cherry”.

We can also check the length of the queue using the len() function:

queue_length = len(queue)

The variable queue_length will contain the value 1, indicating that there is only one element remaining in the queue.

By utilizing the deque data structure, we have successfully implemented a queue in Python. The deque’s append() and popleft() methods provide efficient operations for adding and removing elements at both ends of the queue.

Use Case: Implementing a Stack

In this chapter, we will explore a specific use case for the Python deque data structure – implementing a stack. A stack is a Last-In-First-Out (LIFO) data structure that allows operations such as push (add an element to the top of the stack) and pop (remove the top element from the stack).

Using a deque to implement a stack provides several advantages over using a regular list. The deque data structure allows for efficient appending and popping from both ends, making it a perfect fit for implementing a stack.

Let’s start by importing the deque class from the collections module:

from collections import deque

To implement a stack, we can simply create an instance of the deque class and use the append() method to push elements onto the stack and the pop() method to remove elements from the top of the stack:

stack = deque()

# Pushing elements onto the stack
stack.append(10)
stack.append(20)
stack.append(30)

# Popping elements from the stack
print(stack.pop())  # Output: 30
print(stack.pop())  # Output: 20
print(stack.pop())  # Output: 10

As you can see, the append() method is used to add elements to the stack, and the pop() method is used to remove elements from the stack in a Last-In-First-Out (LIFO) manner.

Additionally, the deque data structure also provides the len() function to get the size of the stack and the peek() function to get the top element without removing it:

stack = deque()

stack.append(10)
stack.append(20)
stack.append(30)

print(len(stack))   # Output: 3
print(stack.peek()) # Output: 30

The len() function returns the number of elements in the stack, and the peek() function returns the top element without modifying the stack.

Using a deque to implement a stack provides a more efficient and convenient way to perform stack operations compared to using a regular list. The deque data structure’s ability to append and pop elements from both ends makes it well-suited for implementing a stack.

Use Case: Sliding Window Problem

The sliding window problem is a common algorithmic problem where we are given an array or a list and we need to find a subarray or sublist of a fixed size k, such that the sum or average of its elements is maximum or minimum. This problem can be efficiently solved using a deque data structure in Python.

Let’s understand the problem with an example. Given an array [4, -2, 5, 3, -1, 8, -6] and a window size of 3, we need to find the maximum sum of a subarray of size 3. The subarrays are [4, -2, 5], [-2, 5, 3], [5, 3, -1], [3, -1, 8], [-1, 8, -6]. The maximum sum is 15, which is the sum of the subarray [3, -1, 8].

To solve this problem using a deque, we can iterate through the array and maintain a deque of size k. We initialize the deque with the first k elements of the array. Then, for each subsequent element, we remove the first element from the deque and add the current element to the deque. This way, the deque always represents the current window of elements.

Here’s an implementation of the sliding window problem using a deque:

from collections import deque

def max_subarray_sum(arr, k):
    window = deque(arr[:k])
    max_sum = sum(window)
    
    for i in range(k, len(arr)):
        window.popleft()
        window.append(arr[i])
        current_sum = sum(window)
        max_sum = max(max_sum, current_sum)
    
    return max_sum

arr = [4, -2, 5, 3, -1, 8, -6]
k = 3
max_sum = max_subarray_sum(arr, k)
print(f"Maximum sum of a subarray of size {k}: {max_sum}")

In this code, we define a function max_subarray_sum that takes an array arr and a window size k. We initialize the deque window with the first k elements of the array. We also initialize the max_sum variable with the sum of the window.

Then, we iterate through the remaining elements of the array starting from index k. In each iteration, we remove the first element from the deque using popleft() and add the current element to the deque using append(). We calculate the sum of the window using sum() and update the max_sum if the current sum is greater.

Finally, we return the max_sum as the result.

When we run this code, it will output: Maximum sum of a subarray of size 3: 15, which is the expected result.

The sliding window problem is just one example of how a deque can be used to efficiently solve a problem. Deque provides constant time complexity for inserting and removing elements from both ends, making it a perfect choice for solving problems that involve maintaining a window of elements.

Related Article: How to Remove an Element from a List by Index in Python

Use Case: Undo/Redo Functionality

In many applications, such as text editors or drawing programs, the ability to undo and redo actions is essential. The Python deque data structure can be used effectively to implement this functionality.

To illustrate this use case, let’s consider a simple text editor that allows the user to enter and edit text. Each time the user performs an action, such as typing or deleting characters, we can store the state of the text in a deque.

Here’s a basic implementation of the text editor using a deque:

from collections import deque

class TextEditor:
    def __init__(self):
        self.text = deque()
        self.undo_stack = deque()
        self.redo_stack = deque()

    def type(self, char):
        self.text.append(char)
        self.undo_stack.append(('type', char))
        self.redo_stack.clear()

    def delete(self):
        if self.text:
            char = self.text.pop()
            self.undo_stack.append(('delete', char))
            self.redo_stack.clear()

    def undo(self):
        if self.undo_stack:
            action, char = self.undo_stack.pop()
            if action == 'type':
                self.text.pop()
            elif action == 'delete':
                self.text.append(char)
            self.redo_stack.append((action, char))

    def redo(self):
        if self.redo_stack:
            action, char = self.redo_stack.pop()
            if action == 'type':
                self.text.append(char)
            elif action == 'delete':
                self.text.pop()
            self.undo_stack.append((action, char))

    def get_text(self):
        return ''.join(self.text)

In this implementation, the text deque stores the current state of the text. Whenever the user types a character, the character is appended to both the text deque and the undo_stack deque, along with the action ‘type’. Similarly, when the user deletes a character, the character is removed from the text deque and added to the undo_stack deque, along with the action ‘delete’.

The undo method undoes the last action performed by popping the most recent action from the undo_stack deque and applying the opposite action to the text deque. The undone action is then added to the redo_stack deque.

The redo method redoes the last undone action by popping the most recent action from the redo_stack deque and applying it to the text deque. The redone action is then added back to the undo_stack deque.

The get_text method returns the current state of the text as a string.

Using this implementation, we can easily add undo and redo functionality to our text editor:

editor = TextEditor()
editor.type('H')
editor.type('e')
editor.type('l')
editor.type('l')
editor.type('o')
print(editor.get_text())  # Output: Hello

editor.undo()
print(editor.get_text())  # Output: Hell

editor.redo()
print(editor.get_text())  # Output: Hello

As you can see, the undo method undoes the last action, while the redo method redoes the last undone action. The get_text method allows us to retrieve the current state of the text.

The deque data structure provides an efficient way to implement undo/redo functionality, as it allows for constant time insertion and deletion at both ends of the queue.

In the next section, we will explore another use case for the deque data structure: implementing a sliding window.

Real World Example: Web Scraping with Deques

Web scraping is a technique used to extract data from websites. It involves parsing the HTML of web pages and extracting the desired information. Python provides several libraries for web scraping, such as BeautifulSoup and Scrapy. In this chapter, we will explore how the Python Deque data structure can be useful in the context of web scraping.

When scraping websites, it is common to encounter situations where you need to store a collection of URLs to be visited later. For example, you might start by scraping the homepage of a website and then follow the links found on that page to scrape additional pages. To accomplish this, you can use a Deque to store the URLs in a queue-like structure.

Let’s consider a simple web scraping scenario where we want to scrape the titles of all the blog posts on a website. We can start by defining a function that takes a URL as input and returns the HTML content of that page:

import requests

def get_html(url):
    response = requests.get(url)
    return response.text

Now, let’s define a function called scrape_titles that takes a starting URL and scrapes the titles of all the blog posts on the website. We will use a Deque to store the URLs that need to be visited:

from collections import deque
from bs4 import BeautifulSoup

def scrape_titles(start_url):
    visited = set()  # to keep track of visited URLs
    queue = deque([start_url])  # initialize the Deque with the starting URL

    while queue:
        url = queue.popleft()
        visited.add(url)

        html = get_html(url)
        soup = BeautifulSoup(html, 'html.parser')

        # find all the blog post titles on the page
        titles = soup.find_all('h2', class_='post-title')

        for title in titles:
            print(title.text)

        # find all the links on the page
        links = soup.find_all('a')

        for link in links:
            href = link.get('href')

            # check if the link is not already visited and is not None
            if href not in visited and href is not None:
                queue.append(href)

In this example, we start by initializing an empty set called visited to keep track of the URLs we have already visited. We also initialize the Deque queue with the starting URL. Inside the while loop, we pop the leftmost URL from the Deque, add it to the visited set, and then scrape the HTML content of that page using the get_html function.

We use BeautifulSoup to parse the HTML and extract the desired information. In this case, we are looking for all the blog post titles on the page, which are represented by the <h2 class="post-title"> HTML tag. We iterate over the found titles and print them.

Next, we find all the links on the page using the <a> HTML tag. We check if each link is not already visited and is not None. If it meets these conditions, we add it to the Deque queue, so that it can be visited later.

By using a Deque to store the URLs, we ensure that the scraping process follows a breadth-first search approach, visiting pages in the order they were discovered. This can be useful in scenarios where you want to scrape a website systematically or limit the depth of the scraping process.

In this chapter, we explored a real-world example of using Python Deques for web scraping. We saw how Deques can be used to store URLs in a queue-like structure, allowing us to scrape web pages in a systematic manner. The code examples demonstrated how to implement a basic web scraping algorithm using Deques and the BeautifulSoup library.

Real World Example: Task Scheduling

In this chapter, we will explore a real-world example of using the Python Deque data structure for task scheduling. Task scheduling is a common problem in computer science and involves managing and executing a set of tasks in a specific order.

Imagine you are building a task management system for a team of developers. Each developer has a set of tasks to complete, and these tasks need to be executed in a specific order. Additionally, new tasks may be added to the system at any time, and existing tasks may need to be rescheduled or reassigned.

To implement this task scheduling system, we can use a deque to represent the queue of tasks. The deque allows us to efficiently add tasks to the beginning or end of the queue, as well as remove tasks from either end.

Let’s start by defining a class called TaskScheduler that will encapsulate the task scheduling logic:

from collections import deque

class TaskScheduler:
    def __init__(self):
        self.tasks = deque()

    def add_task(self, task):
        self.tasks.append(task)

    def get_next_task(self):
        return self.tasks.popleft()

    def reschedule_task(self, task):
        self.tasks.appendleft(task)

In the above code, we import the deque class from the collections module and define a TaskScheduler class. The class has an instance variable called tasks, which is initialized as an empty deque in the constructor.

The add_task method adds a new task to the end of the deque using the append method.

The get_next_task method retrieves and removes the next task from the front of the deque using the popleft method. This ensures that tasks are executed in the order they were added.

The reschedule_task method allows us to reschedule a task by moving it to the front of the deque using the appendleft method. This allows us to prioritize certain tasks over others.

Let’s see how we can use this TaskScheduler class in a real-world scenario. Suppose we have three developers, and each developer has a set of tasks to complete. We can create instances of the TaskScheduler class for each developer and add their tasks to the deque:

developer1 = TaskScheduler()
developer2 = TaskScheduler()
developer3 = TaskScheduler()

developer1.add_task("Fix bug in login module")
developer1.add_task("Implement new feature")
developer2.add_task("Refactor code")
developer3.add_task("Write unit tests")

Now, we can simulate the task execution by retrieving and printing the next task for each developer:

print("Developer 1: ", developer1.get_next_task())
print("Developer 2: ", developer2.get_next_task())
print("Developer 3: ", developer3.get_next_task())

The output will be:

Developer 1:  Fix bug in login module
Developer 2:  Refactor code
Developer 3:  Write unit tests

As you can see, the tasks are executed in the order they were added. We can also reschedule tasks by using the reschedule_task method. For example, let’s reschedule the task “Implement new feature” for developer 1:

developer1.reschedule_task("Implement new feature")

Now, if we retrieve the next task for developer 1 again, we will get the rescheduled task:

print("Developer 1: ", developer1.get_next_task())

The output will be:

Developer 1:  Implement new feature

Related Article: How to Solve a Key Error in Python

Real World Example: Maintaining History

In many real-world scenarios, we often need to maintain a history of operations or track changes made to an object. Python’s deque can be a powerful tool for implementing such functionality efficiently.

Let’s consider a simple example where we want to implement an undo-redo functionality for a text editor. Whenever a user makes a change to the text, we want to store the previous state so that it can be reverted if needed.

We can achieve this using a deque to maintain a history of text states. Each time the user makes a change, we append the current state to the deque. If the user wants to undo the last operation, we simply pop the last state from the deque and set it as the current state. For redo, we can keep another deque to store the undone states and push them back to the history deque when needed.

Here’s a basic implementation:

from collections import deque

class TextEditor:
    def __init__(self):
        self.text = ""
        self.history = deque()
        self.redo_history = deque()

    def add_text(self, new_text):
        self.history.append(self.text)  # Store previous state
        self.text += new_text

    def undo(self):
        if self.history:
            self.redo_history.append(self.text)  # Store undone state
            self.text = self.history.pop()

    def redo(self):
        if self.redo_history:
            self.history.append(self.text)  # Restore undone state
            self.text = self.redo_history.pop()

# Usage example
editor = TextEditor()
editor.add_text("Hello")
editor.add_text(" World")
print(editor.text)  # Output: Hello World

editor.undo()
print(editor.text)  # Output: Hello

editor.redo()
print(editor.text)  # Output: Hello World

In this example, we define a TextEditor class with methods to add text, undo changes, and redo changes. The text attribute stores the current state of the text, while the history and redo_history deques maintain the history of states and undone states, respectively.

When the add_text method is called, it appends the previous state to the history deque and updates the current state with the new text. The undo method pops the last state from history and sets it as the current state, while also storing the undone state in redo_history. The redo method does the opposite, restoring the last undone state.

This implementation allows us to efficiently maintain a history of text states and easily undo or redo changes as needed. By using deques, we can efficiently add and remove states from both ends, making it an ideal choice for implementing such functionality.

Advanced Technique: Implementing a Circular Deque

A circular deque is a deque in which the last element is connected to the first element, forming a circle. This allows for efficient insertion and deletion operations at both ends of the deque.

To implement a circular deque, we can use a fixed-size array and two pointers, front and rear. The front pointer keeps track of the index of the first element, while the rear pointer keeps track of the index of the last element. When either pointer reaches the end of the array, it wraps around to the beginning.

Let’s see how to implement a circular deque in Python:

class CircularDeque:
    def __init__(self, k: int):
        self.k = k
        self.deque = [None] * k
        self.front = 0
        self.rear = 0

    def insertFront(self, value: int) -> bool:
        if self.isFull():
            return False
        self.front = (self.front - 1) % self.k
        self.deque[self.front] = value
        return True

    def insertLast(self, value: int) -> bool:
        if self.isFull():
            return False
        self.deque[self.rear] = value
        self.rear = (self.rear + 1) % self.k
        return True

    def deleteFront(self) -> bool:
        if self.isEmpty():
            return False
        self.deque[self.front] = None
        self.front = (self.front + 1) % self.k
        return True

    def deleteLast(self) -> bool:
        if self.isEmpty():
            return False
        self.rear = (self.rear - 1) % self.k
        self.deque[self.rear] = None
        return True

    def getFront(self) -> int:
        return self.deque[self.front] if not self.isEmpty() else -1

    def getRear(self) -> int:
        return self.deque[(self.rear - 1) % self.k] if not self.isEmpty() else -1

    def isEmpty(self) -> bool:
        return self.front == self.rear and self.deque[self.front] is None

    def isFull(self) -> bool:
        return self.front == self.rear and self.deque[self.front] is not None

In the above code, we initialize the deque with a fixed size, represented by the variable k. The front and rear pointers are initially set to 0. The insertFront and deleteFront operations update the front pointer by decrementing it and wrapping around if necessary. Similarly, the insertLast and deleteLast operations update the rear pointer by incrementing it and wrapping around.

The getFront and getRear operations return the values at the front and rear indices, respectively. The isEmpty and isFull methods check whether the deque is empty or full, based on the positions of the front and rear pointers.

Implementing a circular deque provides efficient insertion and deletion operations, as we only need to update the pointers and wrap around the array when necessary. However, it comes with the limitation of a fixed size. If the deque becomes full, we cannot insert any more elements until some are deleted.

Advanced Technique: Performance Optimization

When working with large datasets or performance-sensitive applications, it is important to optimize the performance of your code. In this section, we will explore some advanced techniques for optimizing the performance of Python Deque.

1. Limiting the Maximum Length of the Deque

By setting a maximum length for the Deque using the maxlen parameter, you can ensure that the Deque never exceeds a certain size. This can be useful when dealing with large amounts of data, as it prevents the Deque from growing indefinitely and consuming excessive memory.

Here’s an example of creating a Deque with a maximum length of 100:

from collections import deque

my_deque = deque(maxlen=100)

2. Using the extendleft and extend Methods

The extendleft and extend methods allow you to add multiple elements to the left and right ends of the Deque, respectively. These methods are more efficient than adding elements one by one using the appendleft and append methods.

Here’s an example of using the extend method to add multiple elements to the right end of a Deque:

my_deque.extend([1, 2, 3, 4, 5])

3. Using deque as a Circular Buffer

A circular buffer is a data structure that allows efficient insertion and deletion of elements at both ends. Python Deque can be used as a circular buffer by limiting the maximum length and using the rotate method.

Here’s an example of using deque as a circular buffer:

my_deque = deque(maxlen=10)
my_deque.extend([1, 2, 3, 4, 5])

# Rotate the Deque to the right by 2 positions
my_deque.rotate(2)

# Rotate the Deque to the left by 3 positions
my_deque.rotate(-3)

4. Avoiding Unnecessary Operations

To optimize performance, it is important to avoid unnecessary operations on the Deque. For example, if you only need to access the first or last element of the Deque, use the peek methods instead of popping or appending elements.

Here’s an example of using the peek methods:

# Get the first element without removing it
first_element = my_deque[0]

# Get the last element without removing it
last_element = my_deque[-1]

By applying these advanced techniques, you can significantly improve the performance of your Python Deque code.

Related Article: How to Add New Keys to a Python Dictionary

Advanced Technique: Memory Management

Python provides automatic memory management through a process called garbage collection. However, in certain cases, you may need more control over memory allocation and deallocation. This is especially true when working with large data sets or in performance-critical applications.

In Python, the deque class from the collections module provides a doubly ended queue implementation. While it offers various convenient methods for manipulating the queue, it also allows for advanced memory management techniques.

One such technique is limiting the maximum size of the deque. By setting a maximum size, you can ensure that the deque never exceeds a certain memory footprint. When the deque reaches its maximum size, any new elements added to it will cause the oldest elements to be automatically removed.

Here’s an example of creating a deque with a maximum size of 5:

from collections import deque

my_deque = deque(maxlen=5)

In this example, if we add elements to my_deque beyond its maximum size, the oldest elements will be removed:

my_deque.append(1)
my_deque.append(2)
my_deque.append(3)
my_deque.append(4)
my_deque.append(5)
my_deque.append(6)

print(my_deque)  # Output: deque([2, 3, 4, 5, 6], maxlen=5)

As you can see, the deque automatically removes the oldest element (1) when a new element (6) is added.

Another memory management technique is manually removing elements from the deque using the pop() method. This method removes and returns the rightmost element from the deque. By selectively removing elements when they are no longer needed, you can free up memory in your program.

my_deque.pop()
print(my_deque)  # Output: deque([2, 3, 4, 5], maxlen=5)

In this example, the rightmost element (6) is removed from the deque.

By combining these memory management techniques with the powerful features of the deque, you can efficiently handle large amounts of data while maintaining control over memory usage.

It’s important to note that the deque’s maximum size is not a hard limit on memory usage. The deque will automatically adjust its memory allocation as elements are added or removed. The maximum size simply determines when elements should be removed to prevent the deque from growing too large.

Advanced Technique: Thread Safety

Thread safety is an important consideration when working with data structures in a multi-threaded environment. Python’s deque provides built-in thread safety through the use of locks, making it a suitable choice for multi-threaded applications.

When multiple threads access a shared resource concurrently, there is a possibility of data corruption or inconsistent state if proper synchronization mechanisms are not in place. In the case of deque, multiple threads may attempt to modify the queue simultaneously, leading to unexpected behavior.

To ensure thread safety, Python’s deque uses a lock mechanism. The lock is acquired before any modification operation is performed on the deque, ensuring that only one thread can modify the deque at a given time. This prevents race conditions and guarantees data integrity.

Let’s take a look at an example to understand how thread safety works with deque. Suppose we have two threads that want to add elements to a shared deque concurrently:

from collections import deque
import threading

shared_deque = deque()
lock = threading.Lock()

def add_to_deque(item):
    with lock:
        shared_deque.append(item)

thread1 = threading.Thread(target=add_to_deque, args=(1,))
thread2 = threading.Thread(target=add_to_deque, args=(2,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(shared_deque)  # Output: deque([1, 2])

In this example, we create a shared deque called shared_deque and a lock called lock. The add_to_deque function is responsible for adding an item to the deque. Before modifying the deque, we use the with lock statement to acquire the lock. This ensures that only one thread can modify the deque at a time.

By using locks, we prevent race conditions where multiple threads try to modify the deque simultaneously. In the example, both threads successfully add their items to the deque, and the final output shows that the deque contains both elements.

It’s important to note that while the lock mechanism ensures thread safety, it introduces some overhead due to the need for acquiring and releasing the lock. In scenarios where the deque is accessed frequently by multiple threads, this overhead may impact performance. Therefore, it’s recommended to analyze the specific requirements of your application and choose the appropriate synchronization mechanism accordingly.

Advanced Technique: Extending the Deque Class

Python’s deque class provides a powerful and efficient way to handle double-ended queues. However, there may be scenarios where you need additional functionality or customization beyond what the deque class offers out of the box. In such cases, you can extend the deque class to create your own custom deque with the desired features.

Extending the deque class is relatively straightforward. You can create a new class that inherits from the deque class and add your own methods or override existing ones. This allows you to tailor the deque implementation to suit your specific needs.

Let’s say we want to extend the deque class to include a method that returns the sum of all the elements in the deque. We can achieve this by creating a new class called SumDeque that inherits from deque and adds a sum() method.

from collections import deque

class SumDeque(deque):
    def sum(self):
        return sum(self)

In the example above, we import the deque class from the collections module and define a new class called SumDeque that inherits from deque. We then add a sum() method to the SumDeque class, which simply returns the sum of all the elements in the deque using the built-in sum() function.

Now, let’s create an instance of the SumDeque class and test our custom sum() method:

my_deque = SumDeque([1, 2, 3, 4, 5])
print(my_deque.sum())  # Output: 15

As you can see, we can now use the sum() method on our custom SumDeque object to get the sum of all the elements in the deque.

Extending the deque class in this way allows you to add any desired functionality to the deque, making it more versatile and tailored to your needs. You can add methods for sorting, filtering, or performing any other operations that you require.

In addition to adding new methods, you can also override existing methods in the deque class to modify their behavior. This gives you even more control over how the deque works.

Overall, extending the deque class provides a flexible and powerful way to customize the behavior of double-ended queues in Python. It allows you to create your own specialized deque implementations that meet the specific requirements of your applications.

More Articles from the Python Tutorial: From Basics to Advanced Concepts series:

How to Read a File Line by Line into a List in Python

Reading a file line by line into a list in Python is a common task for many developers. In this article, we provide a step-by-step guide on how to accomplish this using... read more

How to Check If Something Is Not In A Python List

This article provides a guide on using the 'not in' operator in Python to check if an item is absent in a list. It covers the steps for using the 'not in' operator, as... read more

How to Find a Value in a Python List

Are you struggling to find a specific value within a Python list? This guide will show you how to locate that value efficiently using different methods. Whether you... read more

How to Remove Duplicates From Lists in Python

Guide to removing duplicates from lists in Python using different methods. This article covers Method 1: Using the set() Function, Method 2: Using a List Comprehension,... read more

How to Extract Unique Values from a List in Python

Retrieving unique values from a list in Python is a common task for many programmers. This article provides a simple guide on how to accomplish this using two different... read more

How to Pretty Print Nested Dictionaries in Python

Learn how to pretty print nested dictionaries in Python using simple steps. Discover how the pprint module and json module can help you achieve clean and readable... read more