Data structure - queue

Posted by skeener on Sun, 27 Feb 2022 13:19:52 +0100

Characteristics of queue

First in first out (FIFO)

Queue is called queue because of its characteristics. It's very similar to the queue in the supermarket, isn't it? The front keeps walking and the back keeps up

In the queue, you can only insert at the back and delete at the front. The insert operation is also called enqueue, and the delete operation is also called dequeue

Preliminary implementation queue

As mentioned above, the queue should support two operations: queue in and queue out (insert and delete).

Joining the queue will add a new element to the end of the queue. We need to know where the end of the queue is; And out of the team will delete the first element. So we need to know where the queue starts.

The implementation is as follows:

class MyQueue {
    private:
        // store elements
        vector<int> data;       
        // a pointer to indicate the start position
        int p_start;            
    public:
        MyQueue() {p_start = 0;}
        /** Insert an element into the queue. Return true if the operation is successful. */
        bool enQueue(int x) {
            data.push_back(x);
            return true;
        }
        /** Delete an element from the queue. Return true if the operation is successful. */
        bool deQueue() {
            if (isEmpty()) {
                return false;
            }
            p_start++;
            return true;
        };
        /** Get the front item from the queue. */
        int Front() {
            return data[p_start];
        };
        /** Checks whether the queue is empty or not. */
        bool isEmpty()  {
            return p_start >= data.size();
        }
};

int main() {
    MyQueue q;
    q.enQueue(5);
    q.enQueue(3);
    if (!q.isEmpty()) {
        cout << q.Front() << endl;
    }
    q.deQueue();
    if (!q.isEmpty()) {
        cout << q.Front() << endl;
    }
    q.deQueue();
    if (!q.isEmpty()) {
        cout << q.Front() << endl;
    }
}

Here, we use vector < int > data to store the elements in the queue. The advantages of using vector are: the front and back elements are connected, so it is convenient to get the latter element of an element in the queue; It's convenient to add elements at the end, just push_back is OK; An integer representing a subscript can easily point to the beginning

In addition to inserting and deleting, notice that the subscript pointing to the beginning also allows us to use q.Front() to get the first element of the queue and q.isEmpty() to judge whether the queue is empty

The above implementation is simple, but in some cases, the space utilization will be very low. You know, the space in the vector where the subscript is smaller than the subscript pointing to the beginning does not store queue elements in theory. Isn't this space wasted? As the start pointer moves, more and more space is wasted. When we have space constraints, this will be unacceptable.

As shown in the following figure, the space in front of the head does not store any queue elements:

In fact, in this case, we should be able to accept another element.

Achieve more efficient queues

In the previous section, the queue we implemented will waste space and low space utilization. Now we find ways to improve this

A more effective approach is to use circular queues. Specifically, if there is space in front of the head, put elements in the front space instead of constantly asking for space in the back in the previous section

If there is no space behind this picture, there is no way to push with vector_ Back, but there is another element in front of the head

The implementation is as follows:

class MyCircularQueue {
private:
    vector<int> data;
    int head;
    int tail;
    int size;
public:
    /** Initialize your data structure here. Set the size of the queue to be k. */
    MyCircularQueue(int k) {
        data.resize(k);
        head = -1;
        tail = -1;
        size = k;
    }
    
    /** Insert an element into the circular queue. Return true if the operation is 
 successful. */
    bool enQueue(int value) {
        if (isFull()) {
            return false;
        }
        if (isEmpty()) {
            head = 0;
        }
        tail = (tail + 1) % size;
        data[tail] = value;
        return true;
    }
    
    /** Delete an element from the circular queue. Return true if the operation is 
 successful. */
    bool deQueue() {
        if (isEmpty()) {
            return false;
        }
        if (head == tail) {
            head = -1;
            tail = -1;
            return true;
        }
        head = (head + 1) % size;
        return true;
    }
    
    /** Get the front item from the queue. */
    int Front() {
        if (isEmpty()) {
            return -1;
        }
        return data[head];
    }
    
    /** Get the last item from the queue. */
    int Rear() {
        if (isEmpty()) {
            return -1;
        }
        return data[tail];
    }
    
    /** Checks whether the circular queue is empty or not. */
    bool isEmpty() {
        return head == -1;
    }
    
    /** Checks whether the circular queue is full or not. */
    bool isFull() {
        return ((tail + 1) % size) == head;
    }
};

I suggest reading the above code carefully. It's best to draw a queue with several elements

Use queue

Because queues are commonly used, most languages provide built-in queue libraries. You don't have to copy the above implementation code every time you use it

Please familiarize yourself with push, empty, pop, front, size and back functions

#include <iostream>
#include <queue>

int main() {
    // 1. Initialize a queue.
    queue<int> q;
    // 2. Push new element.
    q.push(5);
    q.push(13);
    q.push(8);
    q.push(6);
    // 3. Check if queue is empty.
    if (q.empty()) {
        cout << "Queue is empty!" << endl;
        return 0;
    }
    // 4. Pop an element.
    q.pop();
    // 5. Get the first element.
    cout << "The first element is: " << q.front() << endl;
    // 6. Get the last element.
    cout << "The last element is: " << q.back() << endl;
    // 7. Get the size of the queue.
    cout << "The size is: " << q.size() << endl;
}

Queue implementation breadth first search (BFS)

BFS, people who often read the problem solution should look familiar, and many problems can be used. One common application is to find the shortest path from the root node to the target node

The implementation of BFS often requires the participation of queues

We first queue the root node. Then, in each round, we process the nodes already in the queue one by one, and add all neighbors to the queue. It is worth noting that the newly added nodes will not be traversed immediately, but will be processed in the next round.  

Sometimes, a node cannot be accessed twice. If so, we can add a hash set to solve this problem.

/**
 * Return the length of the shortest path between root and target node.
 */
int BFS(Node root, Node target) {
    queue<Node> queue;  // store all nodes which are waiting to be processed
    Unordered_set<Node> used;     // store all the used nodes
    int step = 0;       // number of steps neeeded from root to current node

    // initialize
    add root to queue;
    add root to used;

    // BFS
    while (queue.size()) {
        step = step + 1;

        // iterate the nodes which are already in the queue
        int size = queue.size();
        for (int i = 0; i < size; ++i) {
            Node cur = the first node in queue;
            if cur is target{
                return step;
            } 
            for (Node next : the neighbors of cur) {
                if (next is not in used) {
                    add next to queue;
                    add next to used;
                }
            }
            remove the first node from queue;
        }
    }
    return -1;          // there is no path from root to target
}

Examples

200. Number of islands This problem theoretically requires a hash set. Is there a clever way not to use it? (hint: the hash set is used to identify the nodes that have been accessed)

752. Open the turntable lock The difficulty lies only in thinking about how to get the next set of strings from the current string. Students who are not familiar with the string may be at a loss

Complete square 279 You can write it against the code template above

Topics: C++ data structure leetcode queue