It smells so good! 20 pictures reveal the fog of "queue" at a glance

The concept of queue

First of all, let's think of a linked list. In a singly linked list, we can only insert the end of the linked list and delete the node at the head of the linked list. Such a restrictive linked list is what we call a queue.

In other words, the queue is a data structure that is limited to insert at one end of the table and delete at the other end of the table.

As shown in the figure below, if you line up to buy tickets, each line has a tail and opponent. Those who come first will buy tickets first, and those who buy later will go out from the opponent. The end of the line continues to line up.

Typically, said one end of the data into the tail end of the data queue head , the data elements into the queue is called enqueue , dequeue process is referred to as a team .

We can summarize as follows

The queue is a linear data structure, and this data structure only allows insertion at one end and deletion at the other end. Direct access to all data except these two ends is prohibited, and the queue is a first-in first-out data structure.

As shown in the picture above, the queue is like a water pipe with two ends connected. Only one end is allowed to be inserted and the other end is taken out. The taken out ball will not be in the water pipe.

There are two ways to implement the queue storage structure

  • Sequence queue: a queue structure implemented on the basis of the sequence table

  • Chain queue: a queue structure implemented on the basis of a linked list

The difference between the two is only the difference between the sequence table and the linked list, that is, in the actual physical space, the queue stored in the data center is the sequential queue, and the queue stored in the scattered storage is the chain queue.

Node design and initialization of queue

The queue has only a chain design method, which itself is divided into multiple queues, such as sequential queues and circular queues , as well as derivative priority queues, etc. We take the design of sequential queues as an example.

The first is the node design of the queue. We can design two structures, one structure Node represents the node, which contains a data field and next pointer, as shown in the figure:

Among them, data represents data, which can be a simple type or a complex structure.

The next pointer indicates that the next pointer points to the next node, and each node is linked through the next pointer.

Then we add another structure, which includes two pointers that always point to the end of the queue and the head of the queue , do you think it looks like a stack?

Our main operation only operates on these two pointers, as shown in the figure:

The code of its structure design can be expressed as:

//结点定义
typedef struct node{
    int data;
    struct node *next;
}node;
//队列定义,队首指针和队尾指针
typedef struct queue{
    node *front;    //头指针
    node *rear;     //尾指针
}queue;

For initialization, two types need to be initialized, one is the initialization node and the other is the initialization queue.

We see the description in the code. The initialization queue is somewhat different. When the queue is initialized, the contents pointed to by the two nodes at the head and the end need to be set to empty, which means it is an empty queue. The two created function codes can be expressed as :

//初始化结点
node *init_node(){
    node *n=(node*)malloc(sizeof(node));
    if(n==NULL){    //建立失败,退出
        exit(0);
    }
    return n;
}
//初始化队列
queue *init_queue(){
    queue *q=(queue*)malloc(sizeof(queue));
    if(q==NULL){    //建立失败,退出
        exit(0);
    }
    //头尾结点均赋值NULL
    q->front=NULL;  
    q->rear=NULL;
    return q;
}

Determine whether the queue is empty

This is a simple and important operation. To judge whether the queue is empty is to judge whether the head of the queue is empty. It is a common operation to judge whether the queue is empty. Don't forget it.

The code can be expressed as:

//队列判空
int empty(queue *q){
    if(q->front==NULL){
        return 1;   //1--表示真,说明队列非空
    }else{
        return 0;   //0--表示假,说明队列为空
    }
}

Or directly use the return value to make simpler judgments, the code is as follows:

int empty(queue *q){
    return q->front==NULL;
}

Enqueue operation

The changes in the enqueue operation are as follows:

When performing the push operation, the same, we first need to judge whether the queue is empty, if the queue is empty, we need to point the head pointer and tail pointer to the first node together , the code is as follows

front=n;
rear=n;

as the picture shows:

Unique node n

When the queue is not empty, then we only need to move the tail node back, and nextform the queue by constantly moving the pointer to the new node. as the picture shows:

The code can be expressed as:

//入队操作
void push(queue *q,int data){
    node *n =init_node();
    n->data=data;
    n->next=NULL;   //采用尾插入法
    //if(q->rear==NULL){     //使用此方法也可以
    if(empty(q)){
        q->front=n;
        q->rear=n;
    }else{
        q->rear->next=n;    //n成为当前尾结点的下一结点
        q->rear=n;  //让尾指针指向n
    }
}

Dequeue operation

The dequeue operation changes are as follows:

The pop operation refers to a judgment made when the queue is not empty. Of course, we must also perform the operation of emptying the queue here, you know.

As shown in the figure, if the queue has only one element, that is to say, the head and tail pointers all point to the same node , then we directly make the head and tail pointers empty NULLand release this node, as shown in the following figure:

When the queue contains the above elements, we need to point the head pointer of the queue to the next element currently pointed to by the head pointer , and release the current element, as shown in the following figure

The code can be expressed as:

//出队操作
void pop(queue *q){
    node *n=q->front;
    if(empty(q)){
        return ;    //此时队列为空,直接返回函数结束
    }
    if(q->front==q->rear){
        q->front=NULL;  //只有一个元素时直接将两端指向置为空即可
        q->rear=NULL;
        free(n);        //记得归还内存空间
    }else{
        q->front=q->front->next;
        free(n);
    }
}

Print queue elements (traverse)

Printing all the elements of the queue can help us debug and see the specific data in the queue. When the queue is not empty, it is enough nextto traverse and output the elements in turn through the point of the node .

Its code can be expressed as

//打印队列元素
void print_queue(queue *q){
    node *n = init_node();
    n=q->front;
    if(empty(q)){
        return ;    //此时队列为空,直接返回函数结束
    }
    while (n!=NULL){
        printf("%d\t",n->data);
        n=n->next;
    }
    printf("\n");   //记得换行
}

There are many other ways to express the traversal operation, such as calculating how many elements are in the queue, the code is as follows:

int calac(queue *q){
    node *n = init_node();
    n=q->front;
    int cnt=0;    //计数器设计
    if(empty(q)){
        return 0;    //此时队列为空,直接返回函数结束
    }
    while (n!=NULL)
    {
        n=n->next;
        cnt++;
    }
    return cnt;
}

False overflow of sequential queue

What is false overflow? We may have questions, overflow and fake!

Here we also need to consider the shortcomings of sequential queues. For sequential queues, its existence is enough to solve most of the design problems, but it still has some defects and deficiencies.

From the above analysis, we can see that the enqueue and dequeue operations are to link and delete nodes directly behind them. This kind of operation will cause its usage space to continuously shift to the side of the dequeue, resulting in false overflow .

Let's make an analogy, first look at the following picture:

Example sequential queue

As shown in the figure above, there is a sequential queue, the size of this queue is 5, which already contains four elements data1,data2,data3,data4.

Next, we dequeue this queue, dequeue 2 elements, the queue becomes like this, as shown in the following figure:

Seen from the picture, there seems to be no problem, but when we then proceed to enqueue operation, for example, we enqueue 2 elements, which are data5and respectively data6.

At this point, we have found the problem. The tail pointer has moved beyond the range where we can perform queue operations. Have you found it?

We call this phenomenon that the storage area used as a queue is not full, but the queue overflows. We call this phenomenon a false overflow . As shown below:

False overflow

So what can we do to solve this problem? This involves the nature of circular queues!

The concept of circular queue

A question may arise at this time, isn't the queue we are learning a dynamic queue implemented with a linked list?

When there is no space, it will open up space. Does this still produce false overflow?

Indeed, when dynamically creating a queue, it is just continuing to continue to apply for memory space backwards;

Even if the previous dequeue operation releases the previous space, the pointer will still move backward until it reaches the upper bound of the memory reserved by the system for the program and is forcibly terminated;

This is fatal for extremely frequent queue operations and programs. At this time, we need to optimize our queue and use a more excellent structure- circular queue .

The circular queue is to turn the last position of the queue storage space to the first position to form a logical ring space , which is used by the queue circularly, as shown in the figure below.

The circular queue is the size range of our queue. On the basis of the original queue, as long as the back of the queue is full, insertion will be started from the front of the queue to achieve the effect of reusing space;

Since the design thinking of the circular queue is more like a ring, it is often represented by a ring diagram, but we need to note that in fact, the circular queue is not a real ring, it is still single linear.

Structure design of circular queue

Since the loop gives the column the size of the data range, there is no need to use the chained dynamic creation method.

Because if you use chain storage, you will not be able to determine when you will return to the head of the queue to perform the insertion operation, so we use the simulation method , as shown in the figure:

Among them, data represents a data field, int is a type, which can be modified to any custom type, such as a simple char, float type, etc., or a complex structure type.

  • maxsizeRepresents the maximum capacity of the circular queue, which represents the entire operational space of the queue.

  • rearRepresents the tail pointer, which moves when entering the queue.

  • frontRepresents the head pointer, which moves when leaving the team.

The code can be expressed as:

#define maxsize 10      //表示循环队列的最大容量
  
//循环队列的结构设计
typedef struct cir_queue{
    int data[maxsize];
    int rear;
    int front;
}cir_queue;

Initialization of circular queue

The core of the initialization of the circular queue is to apply for space, and assign the contents of the front pointer and rear pointer to 0, that is, to point to the 0th element. Here, please note that the 0th element is empty, as shown in the following figure:

The code can be expressed as:

//初始化
cir_queue *init(){
    cir_queue *q = (cir_queue*)malloc(sizeof(cir_queue));
    if(q==NULL){
        exit(0);   //申请内存失败,退出程序
    }
    q->front=0;
    q->rear=0;
    return q;
}

Enqueue operation

The method of enqueue operation is the same as that of sequential queue, just move rear directly.

But pay attention to judgment, if the rear reaches the queue space and goes online, it will continue to move from the beginning.

It is recommended to use the remainder method here, that is, the remainder is to be operated in this space in any case, to prevent an error execution from directly crashing the whole, and it is relatively more concise. The if statement is not recommended, which is more cumbersome.

Enqueue operation

Note that q->rear++this operation cannot be done directly when adding one to move the position , so that the computer judges the priority will have unexpected consequences.

In addition, it is necessary to judge whether the queue is full. When the next position of our rear pointer is the position of front, the circular queue is full.

As shown:

Queue is full

The code can be expressed as:


//入队操作push
void push(cir_queue *q,int data){
    if((q->rear+1)%maxsize==q->front){
        printf("溢出,无法入队\n");
        return;
    }else{
        q->rear=(q->rear+1)%maxsize;
        q->data[q->rear]=data;
    }
}

Dequeue operation

If the sequence queue is dequeued, just move the front one bit back.

As mentioned in many places above, there is one thing to pay attention to, that is, whether the queue is empty. When the queue is empty, the dequeue operation cannot be performed.

Dequeue operation

The code can be expressed as:

//出队操作pop
void pop(cir_queue *q){
    if(q->rear==q->front){
        printf("队列为空,无法出队\n");
        return;
    }else{
        q->data[q->front]=0;
        q->front=(q->front+1)%maxsize;
    }
}

Traverse operation

The traversal operation needs to use a temporary variable to store the position information of the position front, and use i to gradually move backward until i reaches the position of rear to declare the end of the traversal.

//遍历队列
void print(cir_queue *q){
    int i=q->front;
    while(i!=q->rear){
        i=(i+1)%maxsize;
        printf("%d\t",q->data[i]);   
    }
    printf("\n");       //记得换行
}

Summary about the queue

Please keep this sentence in mind: Queue is a first-in first-out data structure.

That's it for today's queue basics. See you in the next issue!

Guess you like

Origin blog.csdn.net/u012846795/article/details/108613463