[Learn to understand data structure] stack and queue (introductory learning)

Event address: CSDN 21-day learning challenge

foreword

Stacks and queues are the basic knowledge of data structures. This article is to share a wave of the author's learning experience on stacks and queues. Due to the limited level, it is inevitable that there will be mistakes. Please forgive me.

the stack

1. The concept and structure of the stack


​ Stack: A special linear table that only allows insertion and deletion of elements at a fixed end . The end where data insertion and deletion operations are performed is called the top of the stack, and the other end is called the bottom of the stack.

​ The data elements in the stack follow the principle of LIFO (Last In First Out).

  • Push stack: The insertion operation of the stack is called push/push/push, and the incoming data is at the top of the stack.
  • Popping: The deletion operation of the stack is called popping. The output data is also on the top of the stack

image-20220804150614099

sum up

​ The implementation of a stack can generally be implemented using an array or a linked list. Relatively speaking, the structure of an array is better. Because the cost of inserting and deleting data at the end of the array
is relatively small.

image-20220804152255873

You can also use a linked list. If you want to use a two-way circular linked list with a head, you can consider using the tail of the linked list as the bottom of the stack and the head of the linked list as the top of the stack if you use a single linked list. Linked list implementation can be considered when saving space is required, but this article mainly uses sequence list to implement stack.

image-20220804152303509

The difference between the stack and the system stack area

​ There is a stack area in the process address space. Although it also has some characteristics similar to the stack in the data structure, such as last-in-first-out, it is a division of the memory area, and the stack in the data structure is a data structure. Organization and storage, they are not the same thing at all, don't confuse them!

multiple choice questions

1. The initial state of a stack is empty. Now put the elements 1, 2, 3, 4, 5, A, B, C, D, E into the stack in sequence, and then pop them out in sequence, then
the order in which the elements are popped out is ( ).
A. 12345ABCDE
B. EDCBA54321
C. ABCDE12345
D. 54321EDCBA

This is very simple. The characteristics of the stack are first-in-last-out and last-in-first-out. The stack is loaded in order, and when it is popped out, it is in reverse order, and B can be selected directly.

2. If the push sequence is 1, 2, 3, 4, and the stack can be popped during the push process, then the following impossible pop sequence is ()
A. 1, 4, 3, 2
B. 2, 3, 4,1
C. 3,1,4,2
D. 3,4,2,1

The question is only given the order of pushing into the stack, and the stack can be popped during the process of pushing into the stack. Let's look at these situations separately:

For A, enter 1 and immediately exit 1, then enter 2, 3, 4 continuously, and finally exit 4, 3, 2 continuously, so it is possible.

For B, advance 1, 2, exit 2, enter 3, then exit 3, then enter 4, then exit 4, and finally exit 1, so it is possible.

For C, advance 1, 2, 3, and get 3. At this time, the top element of the stack is 2. To get out of the stack, you can only get 2, and it is impossible to get 1, so it is impossible. Choose C.

For D, it is possible to continuously enter 1, 2, 3, exit 3, enter 4, exit 4, and then exit 2, 1 in sequence.


2. Implementation of the stack


Declaration of the stack structure

​ Because the stack implemented by the sequence table is used, there will be many "shadows" of the sequence table. If you are not familiar with the sequence table, you can first look at the implementation of the sequence table I wrote. Basically similar to the sequence table, the structure uses a dynamic sequence table to store stack elements, sets a stack top subscript, and a stack capacity variable.

typedef int STDataType;
typedef struct stack
{
    
    
    STDataType* arr;
    int top;
    size_t capacity;
}ST;

stack initialization

​ There is nothing to say, the same idea as the dynamic sequence table, do not open up space when initializing, and consider it together when pushing.

void StackInit(ST* pS)
{
    
    
    assert(pS);
    
    pS->arr = NULL;
    pS->top = pS->capacity = 0;
}

stack destruction

​ The idea is the same as the dynamic sequence table. The dynamic memory is released, the pointer is set to NULL, and other members are set to 0.

void StackDestory(ST* pS)
{
    
    
    assert(pS);
    
    free(pS->arr);
    pS->arr = NULL;
    pS->top = pS->capacity = 0;
}

push operation

​ Due to the characteristics of the stack, elements can only be entered from the top of the stack, and there is only one function to add elements, so the check capacity and expansion operations do not need to be encapsulated into a separate function, and can be directly included in the stack operation function.

​ There are two schemes for using the top control element. One scheme is that top corresponds to the next position of the top element of the stack, so its initial value is 0, and the other scheme is that top corresponds to the top element of the stack, so its initial value is -1. Here we choose The first option is used.

image-20220804161123856

​ By the way, it is recommended to use function encapsulation to implement all operations of the data structure, no matter how simple the operation is, because in this case, the user does not need to consider the implementation details at all, and can use it directly. The simplest examples are the above two solutions Some people choose the first one, and some people choose the second one. They are different in the specific details of the implementation, but the functions and effects of the implementation are exactly the same, and there is no need to consider multiple situations by directly providing the function interface.

​ After checking the capacity or expanding the capacity, the next step is to push the element into the stack. Since top corresponds to the next position on the top of the stack, put the element directly at this position, and then let top increment by 1.

image-20220804163022662

void StackPush(ST* pS, STDataType tar)
{
    
    
    assert(pS);

    //检查是否满容以及扩容
    if (pS->top == pS->capacity)
    {
    
    
        int newCapacity = (pS->capacity == 0) ? 4 : pS->capacity * 2;
        ST* tmp = (STDataType*)realloc(pS->arr, newCapacity * sizeof(STDataType));
        pS->capacity = newCapacity;
        pS->arr = tmp;
    }
    //元素入栈
    pS->arr[pS->top] = tar;
    pS->top++;
}

pop operation

​ To delete an element, you must first check whether the stack is empty. You can encapsulate a function that detects whether the stack is empty.

bool StackEmpty(ST* pS)
{
    
    
    assert(pS);
    
    return pS->top == 0;
}

image-20220804170526104

​ The stack is not empty, there is data that can be deleted, and the top can be directly decremented by 1. In the above figure, it seems that the elements are cleared, but in fact it is not. The original data is still there, just waiting for the next push operation to be completed. its covered.

void StackPop(ST* pS)
{
    
    
    assert(pS);
    assert(!StackEmpty(pS));
    
    --pS->top;
}

Access the top element of the stack

​ According to our design here, the subscript corresponding to the top element of the stack is top-1.

STDataType StackTop(ST* pS)
{
    
    
    assert(pS);
    assert(!StackEmpty(pS));
    
    return pS->arr[pS->top-1];
}

Find the number of elements in the stack

​ According to our design, top can not only indicate the next position of the top element of the stack, but also indicate the number of elements in the stack.

int StackSize(ST* pS)
{
    
    
    assert(pS);
    
    return pS->top;
}

queue

1. The concept and structure of the queue


​ Queue: A special linear table that only allows inserting data at one end and deleting data at the other end. The queue has the characteristics of a first-in-first-out
FIFO (First In First Out) queue. The end of the insertion operation is called the tail of the queue , and the end of the deletion operation is called the head of the queue .

image-20220804203943076


2. Implementation of the queue


​ Queues can also be implemented in the structure of arrays and linked lists. It is better to use the structure of linked lists, because if the structure of arrays is used,
the efficiency of dequeueing and outputting data at the head of the array will be relatively low. Here we use a singly linked list to implement the queue.

Declaration of Queue Structure

​ Since we implemented it with a single-linked list, each queue element corresponds to a single-linked list node, so there will be a variable to store the value and a pointer to the next node.

typedef int QDataType;
typedef struct QueueNode
{
    
    
    struct QueueNode* next;
    QDataType data;
}QNode;

​ The operation of the queue is inseparable from the head and tail pointers, so you might as well encapsulate the two pointers into the structure. Let's add another member here - size is used to record the queue length in real time. Why is it designed this way? Designing such a member will be very efficient when we want to implement the function to calculate the length of the queue. If there is no such member, it is estimated that we have to traverse the queue to find the length, and the time complexity is O(n). If there is such a member If you record the length of the queue, you can just return the size directly, and the time complexity is directly O(1). Isn’t this tulle traversal?

typedef struct Queue
{
    
    
    QNode* front;
    QNode* rear;
    size_t size;
}Que;

Initialize the queue

​ First, set the two pointers of the queue to NULL as initialization. The assert assertion here is to prevent the incoming pQ from being NULL, because if the incoming pQ is normal, it is a pointer to the structure, which cannot be NULL. If it is NULL, it is likely to be If there is an error in the input, assert is used here to detect it.

void QueueInit(Que* pQ)
{
    
    
	assert(pQ);

	pQ->front = pQ->rear = NULL;
    pQ->size = 0;
}

image-20220804212149799

queue

​ In fact, it is the tail insertion of the singly linked list, because there is no sentinel head node, we need to distinguish between the case of inserting the first element and the case of not being the first element.

image-20220804212204874

image-20220804212214032

void QueuePush(Que* pQ, QDataType tar)
{
    
    
	assert(pQ);

	QNode* newNode = (QNode*)malloc(sizeof(QNode));
	if (newNode == NULL)
	{
    
    
		perror("malloc fail");
		exit(-1);
	}
	else
	{
    
      //不为空就初始化结点
		newNode->data = tar;
		newNode->next = NULL;
	}

	if (pQ->rear == NULL)
	{
    
    
		pQ->front = pQ->rear = newNode;
	}
	else
	{
    
    
		pQ->rear->next = newNode;
		pQ->rear = newNode;
	}
	++pQ->size;
}

whether the queue is empty

​ Before implementing the queue, first implement a very simple function, which is to determine whether the queue is empty. How easy is it? When is the queue empty? When the head and tail pointers are both NULL, it means that there is no element in the queue. Here the return value is bool type, if the queue is empty return true, if not empty return false.

bool QueueEmpty(Que* pQ)
{
    
    
    assert(pQ);
    
    return pQ->front == NULL && pQ->rear == NULL;
}

out of queue

​ Why do we first realize whether the queue is empty? Think about it, the queue is implemented using a single linked list, and dequeueing is essentially a head deletion operation. Can the linked list be deleted again when it is empty? It cannot be deleted, so it is necessary to detect the situation to prevent the linked list from being empty first.

​ How to get out of the queue? Think about deleting the head of a single-linked list. First, temporarily store a copy of the address of the current head node, and then point the head pointer to the next node. At this time, use the previously temporarily stored address of the head node to free to release the previous head node.

​ But there is another point to pay attention to, that is, after the last element is out of the queue, remember to set the tail pointer to NULL, otherwise there will be a wild pointer problem!

image-20220804214853943

void QueuePop(Que* pQ)
{
    
    
	assert(pQ);
	assert(!QueueEmpty(pQ));

	if (pQ->front->next == NULL)
	{
    
    
		free(pQ->front);
		pQ->front = pQ->rear = NULL;
	}
	else
	{
    
    
		QNode* del = pQ->front;
		pQ->front = pQ->front->next;
		free(del);
	}
	--pQ->size;
}

Get the head element of the queue

​ This is very simple, just take out the value of the element pointed to by the queue head pointer, but pay attention to checking whether the queue is empty first.

QDataType QueueFront(Que* pQ)
{
    
    
	assert(pQ);
	assert(!QueueEmpty(pQ));
	
	return pQ->front->data;
}

Get the end of the queue element

​ This is similar to the above one, which is basically the same idea.

QDataType QueueRear(Que* pQ)
{
    
    
	assert(pQ);
	assert(!QueueEmpty(pQ));

	return pQ->rear->data;
}

get queue length

​ Hey, isn't it simple? Wouldn't it be enough to return size directly? What else to traverse, this wave, this wave is tulle traversal.

int QueueSize(Que* pQ)
{
    
    
	assert(pQ);

	return pQ->size;
}

destroy queue

​ In essence, it is to destroy the singly-linked list. There is no other way to traverse the queue, release the nodes one by one, and then set the pointer to NULL.

void QueueDestory(Que* pQ)
{
    
    
	assert(pQ);

	QNode* cur = pQ->front;
	while (cur)
	{
    
    
		QNode* del = cur;
		cur = cur->next;
		free(del);
	}
	pQ->front = pQ->rear = NULL;
}

3. Circular Queue


image-20220805124029917

implement the structure used

​ It is not very good to use a single linked list. For example, if you want to get the elements at the end of the queue, you have to traverse it again, but you can directly use the subscript -1 at the end of the queue with the sequence table. Also, the creation of a single linked list is more troublesome than a sequence table, and you need one Create a node and link them all. It is better to use the dynamic sequence table.

​ The insertion or deletion of elements in the circular queue does not change the space, and the strategy of overwriting is adopted. In fact, the key to this question is to accurately determine whether it is empty or full. Here we let the rear point to the next position of the element at the end of the queue, and then the problem arises. We cannot distinguish the two states of empty and full. Why?

​ As shown in the figure, both the queue head pointer and the queue tail pointer point to the same position in both empty and full states.

img

Solution:

  1. Increment a size to count
  2. Add a space, and always leave a place when it is full. For example, if the circular queue has 4 elements, 5 spaces will be opened.

The next position of the rear position is the front, which is the full state, and the front and rear point to the same position, which is the empty state. However, it should be noted that this is the conclusion of the logical structure. As shown in the figure, the queue of the logical structure is full, but we use a dynamic sequence table to realize the circular queue, but the physical structure is another form. Our operations must be based on physics. structure.image-20220805124214047

​ As shown in the picture, we should pay attention to how to deal with it, let the rear subscript + 1 %N, N represents the total number of spaces (including an extra space), and judge whether the result is equal to the front subscript.

img

multiple choice practice

There is a circular queue, the queue head pointer is front, and the tail pointer is rear; the length of the circular queue is N-1. What is the effective length in the team? (Assuming that one more space is opened, the actual space size is N)
A. (rear - front + N) % N + 1
B. (rear - front + N) % N
C. (rear - front) % (N + 1)
D. (rear - front + N) % (N - 1)

To calculate the effective length, there are two situations. If the rear is smaller than the front, the rear-front is a negative number, so it cannot be directly subtracted. How to unify the two situations into one formula? The difference between rear and front can be added to the total number of spaces N, and then the result can be modulo N, which can prevent the subscript from crossing the boundary and realize a logical cycle. So choose B.

image-20220808154204249


OJ practice questions

insert image description here

Guess you like

Origin blog.csdn.net/weixin_61561736/article/details/126237320