Review: data structure and algorithm – 07 queue

Time:2022-5-28
Review: data structure and algorithm - 07 queue
xwzz.jpg

Review: data structures and algorithms – Opening
Review: data structure and algorithm – complexity analysis (I)
Review: data structure and algorithm – complexity analysis (II)
Review: data structures and algorithms – arrays
Review: data structure and algorithm – linked list (I)
Review: data structure and algorithm – linked list (2)
Review: data structures and algorithms – stack

Say something

Recently, I mentioned leaving the company and interviewed many people to find a handover person for the company. The last time I was an interviewer was two years ago;
The biggest feeling is that the environment is too impetuous. It was the same five years ago, and it is the same today. Fubao 996, middle-aged crisis, King rolling, and all kinds of negative information are entwined around;
As an individual, he is not qualified to comment on others’ career plans and instill chicken soup into others, which is more harmful. Just tell yourself: meditate, learn, introspect and move forward.

What is a queue

To get back to business, this chapter introduces another “operation Limited” linear table data structure:queue

AndStackThe structure is the same. It only supports add and delete operations:

  • Add operation (enqueue)
  • Delete operation (dequeue)

The difference is that, contrary to the first in and last out of the stack, it has“First in, first out”Characteristics:

Line up Png

In the figure, queuing to buy tickets is a typical queue structure. The people who line up first have the priority to buy tickets.

Queue structure Png

We call the team entry operation“enqueue()”, for outbound operation“dequeue()”A simple queue operation constraint is as follows:

//Queue basic operation constraint interface
public interface Queue<T> {

    //Join the team
    boolean enqueue(T item);

    //Leave the team
    T dequeue();

    //Is it empty
    boolean isEmpty();

    //Number of data in the queue
    int size();

    //Empty
    void clear();
}

One satisfactionFirst in first outThe key isenqueue()anddequeue()Function, and the selection of basic containers is the same as that in the previous chapterStack structureSimilarly, you can use arrays or linked lists. Using arrays to implement queues is called:Sequential queue, using a linked list:Chain queue
For scenarios with strict resource constraints, it is recommended to use sequential queues, but there is no requirement for resource constraints. For scenarios that only need the first in last out feature, chain queues can be used.

Sequential queue (array)

The following is the idea of implementing sequential queues with arrays. You may wish to have a look:

Sequential queue:

  • 1. Define an items[] array to store data;

  • 2. Define a head pointer and a tail pointer, which respectively point to the first and last subscripts of the current queue;

  • 3. In enqueue(), check whether the queue is full:

    • Full, return false, failed to join the team;
    • If it is not full, the data is stored at the end of the queue (tail pointer position), and the tail pointer moves one bit backward;
  • 4. In dequeue(), check whether the queue is empty:

    • Empty: return null
    • Non null: returns the queue head data (head pointer position) and moves the head pointer back one bit.

According to this idea, I pasted the implementation code below:

public class ArrayQueue<T> implements Queue<T> {

    private static final int DEFAULT_SIZE = 10;

    //Number of data in the queue
    private int count;

    private Object[] items;

    //Maximum capacity that the current queue can store
    private int maxSize;

    //Head points to the team leader subscript and tail points to the team leader subscript
    private int head = 0, tail = 0;

    public ArrayQueue() {
        this(DEFAULT_SIZE);
    }

    public ArrayQueue(int intSize) {
        items = new Object[intSize];
        this.maxSize = intSize;
    }

    @Override
    public boolean enqueue(T item) {
        //Queue full, storage failed
        if (tail == maxSize) return false;

        //Store, end of queue subscript moves back one bit
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

    @Override
    public T dequeue() {
        //Queue empty, return null
        if (isEmpty()) return null;

        //Take out the data and move the first subscript to the next position
        Object item = items[head];
        ++head;
        --count;
        return (T) item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

    @Override
    public int size() {
        return count;
    }

    @Override
    public void clear() {
        if (isEmpty()) return;
        for (int i = 0; i < size(); i++) {
            items[i] = null;
        }
        count = 0;
    }


    public void print() {
        for (int i = head; i < tail; i++) {
            System.out.print(items[i] + "\t");
        }
        System.out.println();
    }
}

Test:

ArrayQueue<String> queue = new ArrayQueue<>(3);
queue.enqueue("1");
queue.enqueue("2");
queue.enqueue("3");
queue.enqueue("4");
queue.enqueue("5");
queue.print();
String data1 = queue.dequeue();
System Out Println ("outgoing:" + data1);
String data2 = queue.dequeue();
System Out Println ("outgoing:" + data2);
queue.print();
System Out Println ("size:" + queue. Size());

//Problem: the queue has free space but cannot add data
System.out.println(queue.enqueue("6"));
queue.print();
System Out Println ("size:" + queue. Size());

Output:

1   2   3   
Outgoing: 1
Outgoing: 2
3   
Size: 1

false
3   
Size: 1

Through the test, we created a queue with a size of 3 and added 5 elements at one go. Because the queue can only hold 3 elements, the subsequent 4 and 5 elements failed; Then two elements 1 and 2 are left out of the queue, and only element 3 is left in the queue, meeting the requirements ofFirst in first outPrinciples.

However, when I tried to continue adding element 6, I failed! The current size of the queue is still 1, but we know that the total size of the queue is obviously 3, but we cannot continue to add data to the queue. Obviously, this is not the result I want. We can see the reason from the following figure:

Failed to join the team Png

When we enter the queue in the first step, the tail pointer has reached the end of the queue. No matter how many elements are out of the queue in the second step, the tail pointer has not been reset. Obviously, this is the root cause of this problem.

How to solve it?
Data movement!Yes, inArray chapterAs mentioned, to insert a data into an array, you need to move the insertion position and all subsequent data back one bit; Similarly, when the first element in the queue leaves the queue, move all the elements after the first element forward by one bit and set the tail–, so as to ensure that the tail pointer always points to the actual tail element subscript in the team, rather than the tail subscript of the actual size of the queue.

According to the above idea, the problem is indeed solved. However, data movement is required every time the queue is out, which makes the time complexity of the dequeue() function change from the original o (1) – “O (n)

Is there a better plan?
From another angle, let’s take a look at enqueue() queue function. When we execute step 3, queue element 6, the queue fails because the tail pointer points to the end of the queue. However, the above figure shows that there is free space in front of the head pointer. Can we move the data?

As shown in the above figure, when the third step is executed, the tail pointer points to the end of the queue, but the head pointer is not at the head of the queue. Then move 3 to the position with the subscript 0, reset the head and tail pointer positions, and then enter the queue element 6. Modify the enqueue() function according to this idea as follows:

public boolean enqueue(T item) {
        //Queue reached end
        if (tail == maxSize) {
            //Queue full, storage failed
            if (head == 0) return false;

            //There is free space at the head of the queue to move data
            for (int i = head; i < tail; i++) {
                items[i - head] = items[i];
            }
            //Correct the head and tail Subscripts
            tail = tail - head;
            head = 0;
        }

        //Store, end of queue subscript moves backward one bit
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

ModifiedEnqueue() function time complexityHow much is it? The analysis is as follows:

  • When the tail does not reach the tail, you can join the team directly. Time complexity: O (1)
  • When the tail reaches the tail, the team leader has free space to move the data. The time complexity is O (n)
  • For each queue operation, the tail may be at 0N anywhere, 0The time complexity at position n-1 is O (1), and the time complexity at position n is O (n). These n moves can be shared equally with the previous n-1 moves, so the final average time complexity is:O(1)

Chained queue (linked list)

OK. Next, learn about the chain queue:
Chained queues are much simpler than sequential queues and do not need to worry about insufficient storage space or data movement.

Chained queue:

  • 1. Define a head pointer and a tail pointer, which are used to point to the first and last nodes of the current queue;
  • 2. When enqueue() is queued, only tail Next – > newnode, tail – > tail Next
  • 3. When dequeue() is queued, head – > head Next
public class LinkedQueue<T> implements Queue<T> {

    private Node<T> head, tail;
    private int count = 0;

    @Override
    public boolean enqueue(T item) {
        Node<T> newNode = new Node<>(item);
        if (head == null) {
            head = tail = newNode;
        } else {
            tail.next = newNode;
            tail = tail.next;
        }
        count++;
        return true;
    }

    @Override
    public T dequeue() {
        if (head == null) return null;
        T item = head.data;
        head = head.next;
        count--;
        return item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

    @Override
    public int size() {
        return count;
    }

    @Override
    public void clear() {
        head = null;
    }

    public void print() {
        Node<T> temp = head;
        while (temp != null) {
            System.out.print(temp.data + "\t");
            temp = temp.next;
        }
        System.out.println();
    }

    static class Node<T> {

        Public t data// Store data
        Public node<t> next// Next node

        public Node(T data) {
            this.data = data;
        }

    }
}

Test:

LinkedQueue<String> queue = new LinkedQueue<>();
        queue.enqueue("1");
        queue.enqueue("2");
        queue.enqueue("3");
        queue.enqueue("4");
        queue.enqueue("5");

        queue.print();

        System Out Println ("outgoing:" + queue. Dequeue());
        System Out Println ("outgoing:" + queue. Dequeue());
        System Out Println ("outgoing:" + queue. Dequeue());

        queue.print();

        queue.enqueue("6");

        queue.print();

Output:

Outgoing: 1
Outgoing: 2
Outgoing: 3
4   5   
4   5   6

You can see that the chain queue, whether enqueue() or dequeue(), can directly access the corresponding node through the pointer, so the time complexity of the chain queue is:O(1)

Advanced

Circular queue

As mentioned earlier, it is recommended to use sequential queues in scenarios with limited memory resources. On the contrary, chain queues can be used. If you want to have the same efficient queue entry operation as the chain queue under limited resources, can such a queue be realized?

Circular queue Png

As shown in the figure, we connect the array end to end:

  • When joining the queue, when the data is filled to the tail and continues to be filled, the reset tail pointer points to the memory space with the subscript of 0. If the space is idle, joining the queue succeeds; otherwise, joining the queue fails when it is full;
  • When leaving the queue, the actual head pointer position is calculated by the formula head = (head + 1)% maxsize.
/**
 *Circular queue
 */
public class CircularQueue<T> implements Queue<T> {

    private static final int DEFAULT_SIZE = 10;

    //Number of data in the queue
    private int count;

    private Object[] items;

    //Maximum capacity that the current queue can store
    private int maxSize;

    //Head points to the queue header subscript, and tail points to the tail subscript
    private int head = 0, tail = 0;

    public CircularQueue() {
        this(DEFAULT_SIZE);
    }

    public CircularQueue(int intSize) {
        items = new Object[intSize];
        this.maxSize = intSize;
    }

    @Override
    public boolean enqueue(T item) {
        //Team full
        if (count == maxSize) return false;
        //Tail arrives at the end of the team,
        if (tail == maxSize) tail = 0;
        //Store, end of queue subscript moves backward one bit
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

    @Override
    public T dequeue() {
        //Queue empty, return null
        if (isEmpty()) return null;
        //Take out the data and move the subscript of the team head backward by one bit
        Object item = items[head];
        head = (head + 1) % maxSize;
        --count;
        return (T) item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

    @Override
    public int size() {
        return count;
    }

    @Override
    public void clear() {
        if (isEmpty()) return;
        for (int i = 0; i < size(); i++) {
            items[i] = null;
        }
        count = 0;
    }


    public void print() {
        for (int i = head, j = 0; j < count; i = (i + 1) % maxSize, j++) {
            System.out.print(items[i] + "\t");
        }
        System.out.println();
    }
}

Test:

CircularQueue<String> queue = new CircularQueue<>(5);
        queue.enqueue("1");
        queue.enqueue("2");
        queue.enqueue("3");
        queue.enqueue("4");
        queue.enqueue("5");
        queue.enqueue("6");

        queue.print();

        System Out Println ("outgoing:" + queue. Dequeue());
        System Out Println ("outgoing:" + queue. Dequeue());

        queue.print();

        queue.enqueue("7");

        queue.print();

Output:

1   2   3   4   5   
Outgoing: 1
Outgoing: 2
3   4   5   
3   4   5   7

Since enqueue() and dequeue() correct the tail pointer and head pointer respectively, there will be no data movement. The time complexity is:O(1).