栈和队列:从原理到应用全解析

栈和队列的数据结构

学习目标

01 栈的工作原理和基本操作

02 栈的顺序存储

03 栈的链式存储

04 栈的应用场景

05 队列的基本概念和类型

06 循环队列和链式队列实操


PART.01 栈的工作原理和基本操作

栈的定义

在数据结构中,栈(Stack)是一种遵循**"后进先出"(Last‑In‑First‑Out, LIFO)**原则的线性数据结构,可类比为"堆叠的盘子"------只能从顶部放新盘子,也只能从顶部取走盘子。

栈(Stack)是只允许在一端进行插入或删除操作的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。

栈的基本概念

  • 栈顶:栈中最活跃的一端,所有插入(进栈) 和删除(出栈)操作均在此进行。
  • 栈底:栈中固定的一端,数据一旦压入栈底,只有当上方所有数据出栈后才能被访问。
  • 空栈:栈中没有任何元素时,栈顶与栈底重合。

栈的工作原理

  • 栈的工作流程完全围绕LIFO原则
  • 进栈(Push):新元素从栈顶加入,栈顶指针向上移动(栈的容量+ 1)。
  • 出栈(Pop) :仅栈顶元素可被删除,栈顶指针向下移动(栈的容量‑ 1)。 例:栈内元素a、b、c,执行出栈,先删除C,再删除B,最后删除A,出栈顺序为C→B→A。
  • 访问限制:无法直接访问栈中间或栈底的元素,必须通过出栈操作"暴露"目标元素。

栈的存储实现方式

实现方式 核心结构 优点 缺点 适用场景
顺序栈 数组 + 栈顶指针 操作效率高(数组下标访问,O (1) ) 容量固定,可能溢出(需动态扩容) 元素数量已知、操作频繁的场景
链式栈 链表(无头节点) + 栈顶指针 容量动态扩展,无溢出风险 每个元素需额外存储指针,空间开销略大 元素数量不确定、需灵活扩容的场景

栈的基本操作

  • InitStack(&S):初始化一个空栈S。
  • StackEmpty(S):判断一个栈是否为空,若栈S为空则返回true,否则返回false。
  • Push(&S,x):进栈,若栈S未满,则将x加入使之成为新栈顶。
  • Pop(&S,&x):出栈,若栈S非空,则弹出栈顶元素,并用x返回。
  • GetTop(S,&x):读栈顶元素,但不出栈,若栈S非空,则用x返回栈顶元素。
  • DestroyStack(&S):销毁栈,并释放栈S占用的存储空间(&表示引用调用)。

练习题

  1. 3个不同元素依次进栈,能得到( )种不同的出栈序列。
    A. 4 B. 5 C. 6 D. 7

解析

  1. 第一个出栈元素:A(A 进栈后立即出栈)
    剩余元素:B、C(需依次进栈)
  • B 进→ B 出→ C 进→ C 出 序列:ABC
  • B 进→ C 进→ C 出→ B 出 序列:ACB
  1. 第一个出栈元素:B(A 进→B 进,B 先出)
    剩余元素:A(栈内)、C(未进栈)
  • A 出→ C 进→ C 出 序列:BAC
  • C 进→ C 出→ A 出 序列:BCA
  1. 第一个出栈元素:C(A 进→B 进→C 进,C 先出)
    剩余元素:A、B(均在栈内,需按"后进先出" 出栈)
  • B 出→ A 出 序列:CBA

共5种:ABC、ACB、BAC、BCA、CBA

n 个元素的合法出栈序列数符合卡特兰数公式:

C_{n}=\\frac{1}{n+1}\\binom{2n}{n}

答案:B

  1. 若栈的输入序列是1,2,3,...,n,输出序列的第一个元素是n,则第i个输出元素是( )。
    A. 不确定 B.n‑i C. n‑i‑1 D. n‑i+1

解析

当i=1时,输出元素为n。

当i=2时,输出元素为n‑1。

当i=3时,输出元素为n‑2。

依此类推,第i个输出元素为n‑i+1。

答案:D

【2020统考真题】对空栈S进行Push和Pop操作,入栈序列为a,b,c,d,e,经过Push、Push、Pop、Push、Pop、Push、Push、Pop操作后得到的出栈序列是( )。

A. b,a,c B. b,a,e C. b,c,a D. b,c,e

解析:

Push(a)、Push(b) → Pop输出b;Push© → Pop输出c;Push(d)、Push(e) → Pop输出e;出栈序列:b c e。

答案:D


PART.02 栈的顺序存储

如何实现栈的顺序存储

进栈:top加1 栈空:top=‑1 出栈:top减1 栈满:top= MAX_SIZE‑1

c 复制代码
#define MaxSize 50 //定义栈中元素的最大个数
typedef struct{ 
    Elemtype data [MaxSize]; //存放栈中元素
    int top; //栈顶指针
}Stack;
  • 栈顶指针:S.top
  • 栈顶元素:S.data[S.top]
  • 栈空:S.top=-1;
  • 栈满:S.top=MaxSize‑1;
  • 栈内元素个数:S.top+1;
栈的基本操作1
c 复制代码
// 1. 初始化栈
void initStack(Stack *stack) { 
    stack->top = -1; // 初始化栈顶指针为‑1,表示空栈
}

// 2. 判断栈是否为空
bool isEmpty(Stack *stack) { 
    return stack->top == -1; // 栈顶指针为‑1则为空
}

// 3. 判断栈是否已满
bool isFull(Stack *stack) { 
    return stack->top == MAX_SIZE - 1; // 栈顶指针达到最大索引则栈满
}
栈的基本操作2
c 复制代码
// 4. 入栈操作
bool push(Stack *stack, ElementType element) { 
    if (isFull(stack)) { 
        printf("栈已满,无法入栈!\n");
        return false; // 入栈失败
    }
    stack->top++; // 栈顶指针上移 
    stack->data[stack->top] = element; // 将元素放入栈顶
    return true; // 入栈成功
}

// 5. 出栈操作 
bool pop(Stack *stack, ElementType *element) { 
    if (isEmpty(stack)) {
        printf("栈为空,无法出栈!\n");
        return false; // 出栈失败
    }
    *element = stack->data[stack->top]; // 将栈顶元素赋值给输出参数 
    stack->top--; // 栈顶指针下移
    return true; // 出栈成功 
}
栈的基本操作3
c 复制代码
// 6. 获取栈顶元素(不删除)
bool getTop(Stack *stack, ElementType *element) {
    if (isEmpty(stack)) { 
        printf("栈为空,无栈顶元素!\n");
        return false; // 获取失败
    }
    *element = stack->data[stack->top]; // 将栈顶元素赋值给输出参数
    return true; // 获取成功
}

// 7. 打印栈中所有元素 
void printStack(Stack *stack) { 
    if (isEmpty(stack)) { 
        printf("栈为空!\n");
        return; 
    }
    printf("栈中元素(从栈底到栈顶):"); 
    for (int i = 0; i <= stack->top; i++) {
        printf("%d ", stack->data[i]);
    }
    printf("\n"); 
}

PART.03 栈的链式存储

采用链式存储的栈称为链栈 ,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行。规定链栈没有头结点 ,top指向栈顶元素。

c 复制代码
// 定义栈节点结构体
typedef int ElementType; // 栈中元素的类型
typedef struct Node {
	int data; // 节点数据
	struct Node* next; // 指向下一个节点的指针
} Node;

// 定义链式栈结构体
typedef struct {
	Node* top; // 栈顶指针,指向栈顶节点
	int size; // 栈的大小(元素个数)
} LinkedStack;

栈的链式存储初始化

c 复制代码
// 1. 初始化栈
void initStack(LinkedStack* stack) {
    stack->top = NULL; // 栈顶指针初始化为NULL,表示空栈
    stack->size = 0; // 栈大小初始化为0
}

栈的链式存储基本操作1

c 复制代码
// 2. 判断栈是否为空
bool isEmpty(LinkedStack* stack){
	return stack->top==NULL;
}
// 3. 获取栈的大小
int getSize(LinkedStack* stack){
	return stack->size;
}
// 4. 获取栈顶元素(不删除)
bool getTop(LinkedStack* stack,int *value){
	if (isEmpty(stack)) {
		printf("栈为空,无栈顶元素!\n");
		return false;
	}
	*value = stack->top->data; // 获取栈顶元素的值
	return true;
}

栈的链式存储基本操作2

c 复制代码
// 5. 入栈操作
bool push(LinkedStack* stack, int value){
	Node* newNode=(Node* )malloc(sizeof(Node));
	if(newNode==NULL){
		printf("内存分配失败!");
		return false;
	}
	newNode->data=value;
	newNode->next=stack->top;
	
	stack->top=newNode;
	stack->size++;
	return true;
}

栈的链式存储基本操作3

c 复制代码
// 6. 出栈操作
bool Pop(LinkedStack* stack, int *value){
	Node *p=stack->top;
	if(isEmpty(stack)){
		printf("空栈\n");
		return false;
	}
	*value=stack->top->data;
	stack->top=stack->top->next;
	free(p);
	stack->size--;
	return true;
};

栈的链式存储基本操作4

c 复制代码
// 7. 打印栈中所有元素
void printStack(LinkedStack* stack){
	Node* p=stack->top;
	if(isEmpty(stack)){
		printf("空栈");
		return;
	}
	while(p!=NULL){
		printf("%d ",p->data);
		p=p->next;
	}
}
// 8. 销毁栈,释放所有节点内存
void destroyStack(LinkedStack* stack){
	Node* temp;
	while (stack->top != NULL) {
		temp = stack->top;
		stack->top = stack->top->next;
		free(temp);
	}
	stack->size = 0;
	printf("栈已销毁!\n");
}

链式栈的实现与顺序栈的区别

  1. 存储结构:链式栈使用链表节点存储元素,每个节点包含数据域和指针域。
  2. 栈顶指针:链式栈的栈顶指针指向链表的首节点(第一个节点)。
  3. 入栈操作:新元素总是插入在链表的头部(栈顶),时间复杂度为 O(1)。
  4. 出栈操作:总是删除链表的头部节点(栈顶),时间复杂度为 O(1)。
  5. 栈满判断:链式栈理论上没有容量限制(除非内存耗尽),所以不需要判断栈满。
  6. 内存管理:需要手动管理每个节点的内存分配与释放,避免内存泄漏。

PART.04 栈的应用场景

栈的"LIFO" 特性使其在诸多场景中发挥关键作用:

  1. 函数调用栈 :程序执行时,函数的调用顺序(如main()→A()→B())通过栈存储,函数返回时按B()→A()→main()出栈,恢复上下文。
  2. 表达式求值 :计算算术表达式(如3+(4*2)-5)时,用栈存储运算符和操作数,处理优先级(括号、乘除先于加减)。
  3. 括号匹配 :判断代码中括号是否成对(如{[()]}合法,{[)}非法),遇到左括号进栈,遇到右括号则与栈顶左括号匹配并出栈。
  4. 撤销操作(Undo):文本编辑器、绘图软件中,将每一步操作压入栈,撤销时出栈恢复上一步状态。

栈与递归调用

递归 :子程序(或函数)直接调用自己或通过一系列调用语句间接调用自己。

递归基本思想:把一个不能或不好求解的大问题转化为一个或几个小问题,再把这些小问题进一步分解成更小的小问题,直至每个小问题都可以直接求解。

递归的要素

  • 递归边界条件(递归出口):确定递归到何时终止。
  • 递归模式(递归体):大问题是如何分解为小问题。

示例:求n!

c 复制代码
long fact ( int n) {
    if ( n == 0 ) return 1; 
    else return n * fact (n-1);
}

递归调用工作栈原理:

  1. 运行开始时,首先为递归调用建立一个工作栈,其结构包括值参、局部变量和返回地址;
  2. 每次执行递归调用之前,把递归函数的值参和局部变量的当前值以及调用后的返回地址压栈;
  3. 每次递归调用结束后,将栈顶元素出栈,使相应的值参和局部变量恢复为调用前的值,然后转向返回地址指定的位置继续执行。

表达式求值

表达式分为:

  • 前缀表达式(波兰式)
  • 中缀表达式:人日常书写,计算机不好处理
  • 后缀表达式(逆波兰式) :计算机容易处理,不需要括号

示例:(a+b)∗(a−b)(a+b)*(a-b)(a+b)∗(a−b)

  • 前缀:* + a b - a b
  • 中缀:(a+b)*(a‑b)
  • 后缀:a b + a b - *
将中缀表达式转换成后缀表达式
  1. 从左到右读取中缀表达式中的每一个字符。
  2. 如果读取的是操作数 ,直接将其添加到输出队列。
    如果读取的是左括号'(' ,将其推入操作符栈。
    如果读取的是右括号')' ,则从操作符栈中弹出操作符并添加到输出队列,直到遇到左括号'(',弹出并丢弃左括号。
    如果读取的是操作符(+、‑、*、/) : 当操作符栈非空且栈顶操作符的优先级高于或等于当前操作符时,从操作符栈中弹出操作符并添加到输出队列。将当前操作符推入操作符栈。
  3. 清理栈。当表达式处理完毕后,将操作符栈中剩余的所有操作符依次弹出并添加到输出队列。
由后缀表达式计算表达式的值

后缀表达式(逆波兰表达式)是一个没有括号的表达式,计算过程按照操作数和操作符的顺序进行,操作数压栈,操作符将栈顶的两个操作数弹出进行计算并将结果压栈。

  1. 从左到右读取后缀表达式中的每一个元素。
  2. 如果读取的是操作数(数字),将其压入栈中。
  3. 如果读取的是操作符,从栈中弹出两个操作数。根据操作符进行相应的运算(如加、减、乘、除)。将运算结果压入栈中。
  4. 遍历完整个后缀表达式后,栈中应该只剩下一个元素,即最终的计算结果。

示例 a=5,b=3;后缀a b + a b - *

  1. 5+3=8
  2. 5‑3=2
  3. 8 * 2 =16,最终结果16。

PART.05 队列的基本概念和基本操作

队列的定义

队列(Queue)简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。

  • 向队列中插入元素称为入队/进队;
  • 删除元素称为出队/离队。

队列操作特性:先进先出(First In First Out,FIFO),类比生活排队。

  • 队尾:允许插入(入队)的一端。
  • 队首:允许删除(出队)的一端。
  • 空队列:不含任何数据元素的队列。

队列常见的基本操作

  • InitQueue(&Q):初始化队列,构造一个空队列Q。
  • QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
  • EnQueue(&Q, x):入队,若队列Q未满,将x加入,使之成为新的队尾。
  • DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回。
  • GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x。

注意:栈和队列是操作受限的线性表,不能随便读取栈或队列中间的某个数据。

队列的特点

  1. 元素按顺序排列
  2. 只能在队尾添加,队头删除
  3. 访问操作通常只能查看队头元素

常见实现方式

  1. 顺序队列 :用数组实现,需要处理队列满和假溢出问题。
  2. 链式队列:用链表实现,不存在假溢出问题。
  3. 循环队列:优化的顺序队列,空间利用率更高。
  4. 双端队列:两端都可以插入和删除,更加灵活。

顺序队列

c 复制代码
#define MaxSize 50
typedef struct{ 
    ElemType data[MaxSize];//存放队列元素 
    int front,rear;//队头指针和队尾指针 
}SqQueue;
  • 初始时:Q.front=Q.rear=0。
  • 进队操作:队不满时,先送值到队尾元素,再将队尾指针加1。
  • 出队操作:队不空时,先取队头元素值,再将队头指针加1。

假溢出:当元素被插入到数组中下标最大的位置上之后,队列的空间就用尽了,但此时数组的低端还有空闲空间,这种现象叫做假溢出。

循环队列

把存储队列元素的表从逻辑上视为一个环,称为循环队列。

当队尾指针Q.rear=MaxSize‑1后,再前进一个位置就自动到0,利用取余运算%实现。

  • 队首指针进1:Q.front=(Q.front+1)%MaxSize
  • 队尾指针进1:Q.rear=(Q.rear+1)%MaxSize
  • 队列长度:(Q.rear+MaxSize‑Q.front)%MaxSize

双端队列

双端队列是指允许两端都可以进行插入和删除操作的线性表。

  1. 输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入。
  2. 输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除。

若限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈。

链式队列

c 复制代码
typedef struct LinkNode{
    ElemType data;
    struct LinkNode *next;
}LinkNode; //链式队列结点

typedef struct{
    LinkNode *front,*rear; //队列的队头和队尾指针
}*LinkQueue; //链式队列头结点指针
  • 带头结点:队首指针即为链表的头结点指针,增加一个指向队尾结点的指针。
  • 不带头结点:当Q.front==NULL && Q.rear==NULL时,链式队列为空。

队列的应用场景

队列遵循**先进先出(FIFO)**原则:

  1. 任务调度
    • 操作系统进程、线程调度;打印任务队列。
  2. 数据缓冲
    • 网络缓冲区;视频播放缓冲。
  3. 广度优先搜索(BFS)
    • 树、图的广度优先遍历。
  4. 消息队列系统
    • Kafka、RabbitMQ,异步通信、模块解耦。
  5. 现实模拟:银行排队、超市结账、车辆排队模拟。
  6. 计算机硬件:处理器指令队列、IO设备请求队列。
  7. Web服务器:客户端请求排队处理。

PART.06 链式队列的基本操作实操

链式队列的结构体设计

c 复制代码
// 队列节点结构体 
typedef struct Node { 
    int data; 
    struct Node* next; 
} Node; 

// 队列结构体 
typedef struct Queue { 
    Node* front; // 队头指针 
    Node* rear; // 队尾指针 
    int size; // 队列大小 
} Queue;
c 复制代码
// 初始化队列 
Queue* initQueue() {
    Queue* queue = (Queue*)malloc(sizeof(Queue));
    if (queue == NULL) {
        printf("内存分配失败\n");
        return NULL;
    }
    queue->front = NULL; 
    queue->rear = NULL;
    queue->size = 0;
    return queue;
}

链式队列的基本操作

c 复制代码
// 判断队列是否为空
int isEmpty(Queue* queue) {
    if (queue == NULL) return 1;
    return queue->size == 0;
}

// 获取队列大小
int getSize(Queue* queue) {
    if (queue == NULL) return 0;
    return queue->size;
}

// 获取队头元素
int getFront(Queue* queue, int* data) {
    if (isEmpty(queue)) {
        printf("队列为空\n");
        return 0;
    } 
    *data = queue->front->data;
    return 1;
}

链式队列的入队出队操作

c 复制代码
// 入队操作 
int enqueue(Queue* queue, int data) {
    if (queue == NULL) return 0; 
    // 创建新节点 
    Node* newNode = (Node*)malloc(sizeof(Node)); 
    if (newNode == NULL) {
        printf("内存分配失败\n");
        return 0;
    }
    newNode->data = data; 
    newNode->next = NULL; 
    // 如果队列为空,新节点既是队头也是队尾 
    if (isEmpty(queue)) {
        queue->front = newNode;
    } else { 
        queue->rear->next = newNode;
    }
    queue->rear = newNode;
    queue->size++;
    return 1;
}

// 出队操作
int dequeue(Queue* queue, int* data) {
    if (queue == NULL || isEmpty(queue)) {
        printf("队列为空,无法出队\n");
        return 0;
    }
    // 获取队头节点数据 
    Node* temp = queue->front;
    *data = temp->data;
    // 更新队头指针
    queue->front = queue->front->next;
    free(temp);
    queue->size--;
    // 如果队列变为空,更新队尾指针 
    if (isEmpty(queue)) {
        queue->rear = NULL;
    }
    return 1;
}

链式队列的打印和销毁操作

c 复制代码
// 打印队列元素 
void printQueue(Queue* queue) {
    if (queue == NULL || isEmpty(queue)) {
        printf("队列为空\n"); 
        return;
    }
    printf("队列元素: ");
    Node* current = queue->front;
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n"); 
}

// 销毁队列 
void destroyQueue(Queue* queue) {
    if (queue == NULL) return;
    Node* current = queue->front;
    Node* next; 
    while (current != NULL) {
        next = current->next;
        free(current);
        current = next;
    }
    free(queue);
}

PART.07 循环队列的基本操作实操

循环队列队满判断

为了区分是队空还是队满的情况,有三种处理方式:

  1. 牺牲一个单元区分(最常用):约定"队头指针在队尾指针的下一位置作为队满的标志"。

    • 队满条件:(Q.rear+1)%MaxSize==Q.front
    • 队空条件:Q.front==Q.rear
    • 队列中元素个数:(Q.rear‑Q.front+MaxSize) %MaxSize
  2. 结构体增设size成员,记录实际元素个数。

    • 队空:Q.size==0
    • 队满:Q.size==MaxSize

    此时队空、队满都满足Q.front==Q.rear,依靠size区分。

  3. 增设tag标记成员。

    • 插入成功置tag=1;删除成功置tag=0。
    • front==rear时:tag=1代表队满;tag=0代表队空。

循环队列(动态顺序)结构体与基础函数

c 复制代码
// 循环队列结构体定义
typedef struct {
    int *base; // 动态分配的数组基地址 
    int front; // 头指针,指向队头元素
    int rear; // 尾指针,指向队尾元素的下一个位置
    int capacity; // 队列的最大容量
} CircularQueue;

// 初始化队列 
bool InitQueue(CircularQueue *q, int capacity) {
    // 检查容量是否合法 
    if (capacity <= 0) {
        printf("初始化失败:容量必须为正数\n");
        return false;
    }
    // 分配内存空间
    q->base = (int *)malloc(capacity * sizeof(int)); 
    if (q->base == NULL) {
        printf("初始化失败:内存分配失败\n");
        return false;
    }
    q->front = 0;
    q->rear = 0;
    q->capacity = capacity;
    return true;
}

// 判断队列是否为空
bool IsEmpty(CircularQueue *q) {
    return q->front == q->rear;
}

// 判断队列是否已满,牺牲一个空间区分队满队空
bool IsFull(CircularQueue *q) {
    return (q->rear + 1) % q->capacity == q->front;
}

// 获取队列长度 
int QueueLength(CircularQueue *q) {
    return (q->rear - q->front + q->capacity) % q->capacity;
}

循环队列的入队出队操作(动态顺序)

c 复制代码
// 入队操作
bool EnQueue(CircularQueue *q, int value) {
    if (IsFull(q)) {
        printf("入队失败:队列已满\n");
        return false;
    }
    q->base[q->rear] = value;
    q->rear = (q->rear + 1) % q->capacity; // 循环移动尾指针 
    return true;
}

// 出队操作
bool DeQueue(CircularQueue *q, int *value) {
    if (IsEmpty(q)) {
        printf("出队失败:队列为空\n"); 
        return false;
    }
    *value = q->base[q->front];
    q->front = (q->front + 1) % q->capacity; // 循环移动头指针
    return true;
}

// 获取队头元素
bool GetHead(CircularQueue *q, int *value) {
    if (IsEmpty(q)) {
        printf("获取队头失败:队列为空\n");
        return false;
    }
    *value = q->base[q->front];
    return true;
}

// 打印队列元素
void PrintQueue(CircularQueue *q) {
    if (IsEmpty(q)) {
        printf("队列为空\n");
        return;
    }
    printf("队列元素:");
    int i = q->front;
    while (i != q->rear) {
        printf("%d ", q->base[i]);
        i = (i + 1) % q->capacity;
    }
    printf("\n");
}

// 销毁队列 
void DestroyQueue(CircularQueue *q) {
    if (q->base != NULL) {
        free(q->base);
        q->base = NULL;
    }
    q->front = q->rear = 0; 
    q->capacity = 0; 
}
相关推荐
Want5951 小时前
Python大雪纷飞①
开发语言·python·数学建模
代钦塔拉1 小时前
【C++/Qt 进阶】设计模式系列(4):原型模式——用“克隆“代替“重新构造“
c++·qt·设计模式
yujunl1 小时前
重写了一个BOM查询的脚本
开发语言
事圆则缓2 小时前
Kotlin 集合与序列:给页面一份清楚的数据
开发语言·windows·kotlin
事圆则缓2 小时前
Kotlin 异步与并发:从一次搜索请求走到状态流
android·开发语言·kotlin
遇印记2 小时前
应用层,网络管理
运维·网络·windows·学习·安全
从小白到软工2 小时前
4-3 项目-学生成绩管理系统(上)
c语言·开发语言·数据结构·经验分享·ai编程
撸串-研究生2 小时前
基于Spring Boot的智慧社区垃圾分类与回收系统的设计与实现【附源码】
spring boot·后端·分类·垃圾分类·社区管理