Stacks and Queues of Data Structures - Introductory Notes on Algorithms and Data Structures (4)

CSDNlogoPost

This article is the fourth part of the study notes on algorithms and data structures, and it will be updated continuously. Friends are welcome to read and study. If there is something that I don’t understand or is wrong, please communicate

the stack

A stack is a linear data structure that only allows insertion and deletion of elements at one end. The end of the data insertion and deletion operations is called the top of the stack (Top), and the other end is called the bottom of the stack (Bottom). The data elements in the stack follow the principle of last in first out LIFO (Last In First Out), that is, the last entered element is the first to be accessed.

Push (push): The insertion operation of the stack is called push/push/push, and the incoming data is at the top of the stack.
Stack (pop): The deletion operation of the stack is called popping, and the data is also on the top of the stack.

The following animation can more intuitively understand the in and out of the stack

insert image description here

The characteristics of the stack

  1. Last-in-first-out (LIFO): The element that entered the stack last is accessed first, and the element that entered the stack first is accessed last.
  2. Insertion and deletion are only allowed at one end: Stacks generally only allow insertion and deletion at the top of the stack, but not elsewhere.
  3. Stack top pointer: The top pointer of the stack points to the top element of the stack, which changes as elements are inserted and deleted.

stack application

  1. Function call : The function call process in the programming language uses the stack to store information such as the return address, parameters, and local variables of the function. Whenever a function is called, its related information is pushed onto the stack; when the function finishes executing, the information is popped off the stack, and control returns to the calling function.
  2. Expression evaluation : The stack plays an important role in expression evaluation. Expressions are evaluated by converting infix expressions to postfix expressions and using the stack to store operands and operators.
  3. Bracket Matching : The stack is often used to check whether the brackets in the expression match. When traversing an expression, when it encounters a left parenthesis, it is pushed onto the stack. When a right parenthesis is encountered, it is matched with the top element of the stack. If it matches, the top element of the stack is popped and the traversal continues; if it does not match, the parentheses do not match.
  4. Undo operation : In text editors or graphics processing software, stacks can be used to implement undo operations. Every time an operation is performed, relevant information is pushed onto the stack, and when the user needs to undo the operation, the top element of the stack is popped and returned to the previous state.
  5. Interrupt handling and context saving : Interrupts are common events in computer systems, such as hardware failures, external device requests, etc. When the system is interrupted, the program currently being executed needs to be suspended to handle the interrupt event. The stack plays an important role in interrupt handling and is used to save and restore the execution context of the program. When an interrupt occurs, the system will automatically save the execution site of the current program (including status information such as the program counter and registers) to the stack. Then, the Interrupt Service Routine (Interrupt Service Routine, ISR) is executed to handle the interrupt event. After the processing is completed, the system restores the previously saved execution site from the stack, and continues the execution of the interrupted program. Through the save and restore operation of the stack, it is ensured that the interrupt processing flow is correct and the original program execution will not be destroyed.

Basic operation of the stack

  1. Initialize the stack.
  2. Push to add an element to the stack.
  3. Pop, remove an element from the top of the stack.
  4. Get the top element of the stack.
  5. Determine whether the stack is empty or full.
  6. Destruction of the stack.

Notice: When operating the stack, avoid "overflow" and " underflow
"
The stack is empty, if you continue to fetch data, it will underflow and an error will be reported (underflow will occur if the stack is empty and fetching again)

C language

There are two ways to implement a stack, one is implemented using an array, and the other is implemented using a linked list. The following is a summary of the advantages and disadvantages of implementing arrays and linked lists.

Advantages of using arrays
1. Arrays are stored contiguously in memory, so accessing elements is faster. The CPU cache hit ratio will be higher.
2. Random access to subscripts. The efficiency of tail insertion and tail deletion is good.
3. The implementation of the array is relatively simple, and no additional pointers are needed to maintain the relationship between elements.
Disadvantages of array implementation
1. The size of the array is fixed, so it needs to be expanded when the stack space is insufficient, which may lead to performance degradation.
2. When deleting an element, other elements in the array need to be moved, which may also cause performance degradation.

Advantages of using linked list
1. The size of the linked list can be adjusted dynamically, so the space can be better utilized.
2. Insertion and deletion at any position is O(1), and the performance of the linked list is higher because no other elements need to be moved.
Disadvantages of linked list implementation
1. The CPU cache hit rate will be lower, and it is not stored continuously, so the speed of accessing elements is slower.
2. Random access to subscripts is not supported.

Whether to use a linked list or an array structure, the answer to this question depends on the specific application scenario and requirements. Below we give the C language implementation of the array stack and the linked list stack.

array stack

#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100

// 定义栈结构
typedef struct {
    int data[MAX_SIZE]; // 用数组存储栈的元素
    int top; // 栈顶指针
} Stack;

// 初始化栈
void init(Stack *stack) {
    stack->top = -1; // 初始化栈顶指针为-1,表示栈为空
}

// 判断栈是否为空
int isEmpty(Stack *stack) {
    return stack->top == -1;
}

// 判断栈是否已满
int isFull(Stack *stack) {
    return stack->top == MAX_SIZE - 1;
}

// 入栈操作
void push(Stack *stack, int item) {
    if (isFull(stack)) {
        printf("Stack overflow\n");
        return;
    }
    stack->top++; // 栈顶指针加1
    stack->data[stack->top] = item; // 将元素入栈
}

// 出栈操作
int pop(Stack *stack) {
    int item;
    if (isEmpty(stack)) {
        printf("Stack underflow\n");
        return -1;
    }
    item = stack->data[stack->top]; // 获取栈顶元素
    stack->top--; // 栈顶指针减1
    return item;
}

// 获取栈顶元素
int peek(Stack *stack) {
    if (isEmpty(stack)) {
        printf("Stack is empty\n");
        return -1;
    }
    return stack->data[stack->top];
}

// 销毁栈
void destroy(Stack *stack) {
    stack->top = -1; // 将栈顶指针重置为-1,表示栈为空
}

int main() {
    Stack stack;
    init(&stack);

    push(&stack, 1);
    push(&stack, 2);
    push(&stack, 3);

    printf("Top element: %d\n", peek(&stack));

    printf("Popped element: %d\n", pop(&stack));
    printf("Popped element: %d\n", pop(&stack));

    printf("Top element: %d\n", peek(&stack));

    destroy(&stack);

    return 0;
}

linked list stack

#include <stdio.h>
#include <stdlib.h>

// 定义链表节点
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 定义栈结构
typedef struct {
    Node* top; // 栈顶指针
} Stack;

// 初始化栈
void init(Stack* stack) {
    stack->top = NULL; // 初始化栈顶指针为空
}

// 判断栈是否为空
int isEmpty(Stack* stack) {
    return stack->top == NULL;
}

/* 由于链表实现的栈理论上没有大小限制,因此不存在“栈满”的情况。在入栈操作时只需要创建新节点,并将其插入到链表头部即可。
如需限制栈的大小,可以通过设置一个变量来记录当前栈中存储的元素个数,然后在入栈时进行判断,若已满则不允许再次入栈。*/

// 入栈操作
void push(Stack* stack, int item) {
    Node* newNode = (Node*)malloc(sizeof(Node)); // 创建新节点
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        return;
    }
    newNode->data = item; // 设置新节点的数据为要入栈的元素
    newNode->next = stack->top; // 将新节点插入到栈顶
    stack->top = newNode; // 更新栈顶指针
}

// 出栈操作
int pop(Stack* stack) {
    if (isEmpty(stack)) {
        printf("Stack underflow\n");
        return -1;
    }
    Node* topNode = stack->top; // 获取栈顶节点
    int item = topNode->data; // 获取栈顶元素
    stack->top = topNode->next; // 更新栈顶指针
    free(topNode); // 释放栈顶节点的内存
    return item;
}

// 获取栈顶元素
int peek(Stack* stack) {
    if (isEmpty(stack)) {
        printf("Stack is empty\n");
        return -1;
    }
    return stack->top->data;
}

// 销毁栈
void destroy(Stack* stack) {
    while (!isEmpty(stack)) {
        pop(stack);
    }
}

int main() {
    Stack stack;
    init(&stack);

    push(&stack, 1);
    push(&stack, 2);
    push(&stack, 3);

    printf("Top element: %d\n", peek(&stack));

    printf("Popped element: %d\n", pop(&stack));
    printf("Popped element: %d\n", pop(&stack));

    printf("Top element: %d\n", peek(&stack));

    destroy(&stack);

    return 0;
}

queue

The queue is a sibling structure of the stack, and it is a linear data structure that only allows elements to be inserted at one end and deleted at the other end . 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. The data elements in the queue follow the principle of FIFO (First In First Out), that is, the element that enters first is accessed first.

Please add a picture description

Characteristics of the queue

  1. First-in-first-out (FIFO): The first element inserted is the first element to be removed, thus appearing as a first-in-first-out order.
  2. Elements can only be inserted from the end of the queue (into the queue) and removed from the head of the queue (out of the queue).

queue application

  1. Message Passing : In the message passing model, messages are sent to queues to be processed by the receiver. The sender can send messages to the queue through the enqueue operation, and the receiver obtains the message from the queue through the dequeue operation.
  2. Buffer area management : In scenarios such as network communication and disk I/O, queues are used to manage data buffers. The received data is put into the queue, and then taken out of the queue according to certain rules to ensure that the data is transmitted in order.
  3. Task scheduling : Task scheduling in the operating system usually uses queues to manage tasks to be executed, and schedules according to the principle of First-Come-First-Served (FCFS).
  4. Breadth-first search : The breadth-first search algorithm (BFS) of a graph uses a queue to hold nodes to be visited. Starting from the starting node, put it into the queue, then keep taking nodes out of the queue and putting its neighbors into the queue until the queue is empty.

Basic operation of the queue

  1. Enqueue: Insert an element at the end of the queue.
  2. Dequeue: Remove an element from the head of the queue and return.
  3. Get the value of the element at the head of the queue.
  4. Get the number of elements in the queue.
  5. Determine whether the queue is empty or full.
  6. queue destruction

C language

There are two ways to implement a queue, one is implemented using an array, and the other is implemented using a linked list. The following is a summary of the advantages and disadvantages of implementing arrays and linked lists.

Advantages of using arrays
1. Arrays are stored contiguously in memory, so accessing elements is faster. The CPU cache hit ratio will be higher.
2. The implementation of the array is relatively simple, and no additional pointers are required to maintain the relationship between elements.
Disadvantages of array implementation
1. The maximum length of the queue needs to be determined in advance, which may cause performance degradation.
2. Elements need to be moved to maintain the order of the queue.

Advantages of using a linked list
1. There is no need to determine the maximum length of the queue in advance, and it can be dynamically expanded.
2. Insertion and deletion operations only need to modify pointers, and do not need to move elements.
3. Multiple queues can share a linked list.
Disadvantages of linked list implementation
1. The CPU cache hit rate will be lower, and it is not stored continuously, so the speed of accessing elements is slower.
2. The implementation is relatively complicated.

Whether to use a linked list or an array structure, the answer to this question depends on the specific application scenarios and requirements. Below we give the C language implementation of the array queue and the linked list queue.

array queue

#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100 // 队列的最大大小

// 定义队列结构体
struct queue {
    int* arr; // 数组指针
    int front; // 队首位置
    int rear; // 队尾位置
    int size; // 当前队列中存储的元素个数
};

// 初始化队列
struct queue* init() {
    struct queue* q = (struct queue*)malloc(sizeof(struct queue));
    q->arr = (int*)malloc(MAX_SIZE * sizeof(int));
    q->front = 0;
    q->rear = -1;
    q->size = 0;
    return q;
}

// 判断队列是否为空
int is_empty(struct queue* q) {
    return q->size == 0;
}

// 判断队列是否已满
int is_full(struct queue* q) {
    return q->size == MAX_SIZE;
}

// 入队
void enqueue(struct queue* q, int value) {
    if (is_full(q)) {
        printf("Queue Overflow\n");
        return;
    }
    q->rear = (q->rear + 1) % MAX_SIZE;
    q->arr[q->rear] = value;
    q->size++;
}

// 出队
int dequeue(struct queue* q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }
    int value = q->arr[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    q->size--;
    return value;
}

// 获取队首元素
int front(struct queue* q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }
    return q->arr[q->front];
}

// 获取队列长度
int size(struct queue* q) {
    return q->size;
}

// 销毁队列
void destroy(struct queue* q) {
    free(q->arr);
    free(q);
}

int main() {
    struct queue* q = init();
    enqueue(q, 10);
    enqueue(q, 20);
    enqueue(q, 30);
    printf("%d\n", dequeue(q)); // 输出10
    printf("%d\n", front(q)); // 输出20
    enqueue(q, 40);
    printf("%d\n", dequeue(q)); // 输出20
    printf("%d\n", dequeue(q)); // 输出30
    printf("%d\n", dequeue(q)); // 输出40
    printf("%d\n", dequeue(q)); // 输出Queue Underflow
    destroy(q); // 销毁队列
    return 0;
}

linked list queue

#include <stdio.h>
#include <stdlib.h>

// 定义队列节点结构体
struct queue_node {
    int data;
    struct queue_node* next;
};

// 定义队列结构体
struct queue {
    struct queue_node* front; // 队首指针
    struct queue_node* rear; // 队尾指针
    int size; // 当前队列中存储的元素个数
};

// 初始化队列
struct queue* init() {
    struct queue* q = (struct queue*)malloc(sizeof(struct queue));
    q->front = NULL;
    q->rear = NULL;
    q->size = 0;
    return q;
}

// 判断队列是否为空
int is_empty(struct queue* q) {
    return q->size == 0;
}

// 同链表栈,链表队列没有固定的大小限制,因此不需要判断队列是否已满

// 入队
void enqueue(struct queue* q, int value) {
    // 创建新节点
    struct queue_node* new_node = (struct queue_node*)malloc(sizeof(struct queue_node));
    new_node->data = value;
    new_node->next = NULL;

    if (is_empty(q)) {
        q->front = new_node;
        q->rear = new_node;
    } else {
        q->rear->next = new_node;
        q->rear = new_node;
    }

    q->size++;
}

// 出队
int dequeue(struct queue* q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }

    struct queue_node* temp = q->front;
    int value = temp->data;

    if (q->front == q->rear) {
        q->front = NULL;
        q->rear = NULL;
    } else {
        q->front = q->front->next;
    }

    free(temp);
    q->size--;

    return value;
}

// 获取队首元素
int front(struct queue* q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }
    return q->front->data;
}

// 获取队列长度
int size(struct queue* q) {
    return q->size;
}

// 销毁队列
void destroy(struct queue* q) {
    while (!is_empty(q)) {
        dequeue(q);
    }
    free(q);
}

int main() {
    struct queue* q = init();
    enqueue(q, 10);
    enqueue(q, 20);
    enqueue(q, 30);
    printf("%d\n", dequeue(q)); // 输出10
    printf("%d\n", front(q)); // 输出20
    enqueue(q, 40);
    printf("%d\n", dequeue(q)); // 输出20
    printf("%d\n", dequeue(q)); // 输出30
    printf("%d\n", dequeue(q)); // 输出40
    printf("%d\n", dequeue(q)); // 输出Queue Underflow
    destroy(q); // 销毁队列
    return 0;
}

in conclusion

Stacks and queues, as common data structures, play an important role in algorithms and programming. This article summarizes the characteristics, application scenarios and C language implementation of stacks and queues. By deeply understanding their principles and applications, we can better solve problems and optimize algorithms. I hope this article can help readers learn and apply stacks and queues.

Guess you like

Origin blog.csdn.net/a2360051431/article/details/130978787