1. 栈
①栈的概念和结构
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
② 栈的实现
栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。
数组栈:
链表栈(此图为单链表,双链表栈顶可以是头也可以是尾):
1.2.1 接口实现
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; //标识栈顶位置,元素数量
int capacity; //栈的容量
}ST;
void STInit(ST* pst);
void STDestroy(ST* pst);
void STPush(ST* pst, STDataType x);
void STPop(ST* pst);
STDataType STTop(ST* pst);
bool STEmpty(ST* pst);
int STSize(ST* pst);
1.2.2 函数实现
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"Stack.h"
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->capacity = 0;
// 1.表示top指向栈顶元素的下一个位置
pst->top = 0;
// 2.表示top指向栈顶元素
//pst->top = -1;
// top为0的话不清楚是top为0还是空,因此空的时候给-1
//位置(下标) top 0 1
//数值 -1(空) 0 1
//top = 1指向栈顶元素的下一个位置
}
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
void STPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2; //如果栈的当前容量为0,将其设为4,否则将其扩大为当前容量的两倍
STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp; //将原来栈中数据的指针更新为新的内存空间的指针
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
1.2.3 调试
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"Stack.h"
int main()
{
ST s;
STInit(&s);
STPush(&s, 1);
STPush(&s, 2);
STPush(&s, 3);
printf("%d ", STTop(&s));
STPop(&s);
STPush(&s, 4);
STPush(&s, 5);
while (!STEmpty(&s))
{
printf("%d ", STTop(&s));
STPop(&s);
}
printf("\n");
return 0;
}
2. 队列
①队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
②队列的实现
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
2.2.1 接口实现
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int QDataType;
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* phead; // 头指针
QNode* ptail; // 尾指针
int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
2.2.2 函数实现
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"Queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
pq->ptail = pq->phead = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq)
{
assert(pq);
// 空节点
assert(pq->phead);
QNode* del = pq->phead;
pq->phead = pq->phead->next;
free(del);
// 一个节点的时候,phead向前走
// del被free,ptail此时和del指的是一个节点,如果free,就变成了野指针
if (pq->phead == NULL)
{
pq->ptail = NULL;
}
pq->size--;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
// 空节点
assert(pq->phead);
return pq->phead->val;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
// 空节点
assert(pq->phead);
return pq->ptail->val;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
2.2.3 调试
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"Queue.h"
int main()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
printf("%d ", QueueFront(&q));
QueuePop(&q);
printf("%d ", QueueFront(&q));
QueuePop(&q);
QueuePush(&q, 4);
QueuePush(&q, 5);
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
QueueDestroy(&q);
return 0;
}
另外扩展了解一下,实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组实现,也可以使用循环链表实现。
生产者消费者模型是一种用于解决多线程或多进程并发编程中的经典问题。在这个模型中,有两种角色:生产者和消费者,它们共享一个有限大小的缓冲区(也称为队列)。生产者负责生成数据并将其放入缓冲区,而消费者则负责从缓冲区中取出数据并进行处理。
这种模型的目标是确保生产者和消费者之间的协调,以避免以下问题:
- 生产者速度过快:如果生产者生成数据的速度比消费者处理数据的速度快,缓冲区可能会溢出。
- 消费者速度过快:如果消费者处理数据的速度比生产者生成数据的速度快,可能会导致消费者试图从空的缓冲区中读取数据。
- 竞争条件:如果多个生产者或消费者同时访问缓冲区,可能会导致数据损坏或丢失。
为了解决这些问题,可以采用各种同步机制,如互斥锁、条件变量或信号量等。通常,生产者在向缓冲区添加数据之前会获取锁,并在添加完毕后释放锁;消费者在从缓冲区取出数据之前也会获取锁,并在取出完毕后释放锁。这样可以确保在任何时刻只有一个线程可以访问缓冲区,从而避免竞争条件。