目录
栈
一、栈的概念及结构
1. 栈(stack)—— 先进后出
栈(stack)又名堆栈,它是一种特殊的线性表。限定仅在一端进行插入和删除操作的线性表,这一端称为栈顶,另一端称为栈底。
栈中的元素遵循先进后出原则,即插入与删除都只能采用尾插和尾删。故根据这一特色,栈结构的设计我们采用顺序表(顺序表一般采用动态在堆上开辟空间)。
2. 栈、栈帧、栈区 和 堆
- 栈:一般指数据结构中的栈结构(后进先出的线性结构)
- 栈区:一块特殊的内存空间,该区域存储的是与函数调用相关的信息(线程)
- 栈帧:存放在栈区的一种特殊数据结构,编辑器自动分配释放 ,每次函数调用时,都会在内存中的栈区开辟出一个栈帧用来存放函数调用的相关信息
- 堆区:一块特殊的内存空间,需手动分配释放 ,用来存放进程动态申请的空间
3. 三种开辟堆空间函数 malloc、calloc、realloc
相同点:
- 三种函数都是用来在堆上开辟空间;
- 堆上开辟有可能申请空间失败,故申请后需要判空;
- 堆上开辟的空间使用结束后均需调用 free() 函数释放 ;
- 三者的返回值都为void* ,所以在使用时需要强转为自己所需的类型。
malloc 函数
参数:表示在堆上开辟空间的字节数;
功能:直接在堆上开辟空间(最直接);
calloc 函数
参数:num 表示申请的元素个数、size 表示元素类型大小;
功能:从堆上开辟num个size大小的元素;开辟后会将每个字节初始化为0;
realloc 函数
参数:ptr为开辟堆空间的起始位置、size为开辟字节大小
功能:若ptr指针为空,则该函数等价与malloc;若不为空,则会将ptr所指向的空间调整到size字节大小(若ptr指向的原空间满足size字节的空间则直接开辟,若不满足则在堆上重新开辟并拷贝ptr原空间数据后并更新ptr)
二、栈的实现
1.栈的开辟
栈本质上就是只能尾插尾删的线性表,故采用顺序表的方式开辟栈结构(因为不涉及头插、头删,且顺序表查找结点优于链表)
因此栈的代码与顺序表基本一致,不过有的代码会将顺序表结构中的size(储存实际大小)表示为top(栈顶),从数值上一样
//采用动态开辟顺序表结构
typedef struct Stack
{
STDataType* array;
int capacity; //空间总大小
int size; //栈顶(储存实际大小)
}Stack;
2.栈的函数(与顺序表类似)
- 初始化顺序表
一般初始为顺序表开辟三个元素
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (STDataType*)malloc(sizeof(STDataType) * 3); //初始化默认开辟三个单位
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
- 销毁链表
堆用完不销毁,error迟早来找你
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->array)
{
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
- 检测栈是否为空
若为空,返回1
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->size;
}
- 入栈 (尾插)
这里耦合出检测空间是否已满的static void CheckCapacity(Stack* ps)
函数,每次插入新元素需判断堆空间是否足够
//检测空间是否已满
static void CheckCapacity(Stack* ps)
{
assert(ps);
int newCapacity = 2 * ps->capacity;
if (ps->size == ps->capacity)
{
//1.申请新空间
STDataType* newArray = (STDataType*)malloc(sizeof(STDataType) * newCapacity);
if (NULL == newArray)
{
assert(0);
return;
}
//2.拷贝原空间至新空间
memcpy(newArray, ps->array, ps->size * sizeof(STDataType));
//3.释放原空间
free(ps->array);
ps->array = newArray;
ps->capacity = newCapacity;
}
}
// 入栈 (尾插)
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
//1.判断栈空间
CheckCapacity(ps);
//2.直接插入
ps->array[ps->size] = data;
ps->size++;
}
- 出栈(尾删)
void StackPop(Stack* ps)
{
assert(ps);
//1.检测栈是否为空
if (StackEmpty(ps))
{
printf("删除错误!栈为空\n");
return;
}
//2.直接减值
ps->size--;
}
- 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
//1.检测栈是否为空
if (StackEmpty(ps))
{
printf("获取错误!栈为空\n");
return -1;
}
//2.返回数组最后一位元素
return ps->array[ps->size - 1];
}
- 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(ps);
return ps->size;
}
源代码
队列
一、队列的概念及结构
1.队列(queue)—— 先进先出
队列(queue)又名队,它是一种特殊的线性表。限定仅在一端(队尾)进行插入,另一端(队头)删除的线性表。
队列中的元素遵循先进先出原则,即插入采用尾插、删除采用头删。故根据这一特色,栈结构的设计我们一般采用链表来实现。
2.循环队列
前面我们提到队列由于有头删操作,故采取了链表结构设计队列,但链表寻找结点的复杂度较高,因此我们亦可以尝试通过特殊的顺序表来避免其头删的缺点。
1、所以现在我们采用一种顺序表设计队列(头删尾插),且这个顺序表能够避免头删移动后面元素的缺点,我们首先想到的是让后面的元素不移动就行,即当头元素删除时,头指针直接移向下一位即可。
【问题】假溢出问题,头指针向后移动也意味着开辟堆的总空间虽然不变,但当尾指针走到空时,到达队列上界不能在入队,可这时队列前部仍有空间切不能利用,造成空间浪费。
2、为了解决假溢出问题,我们采用循环结构即可,循环顺序表头尾相接,头指针虽然向后移,但并未在堆上释放头结点空间,循环结构意味着尾部指针也可指向原来头部空间。
【问题】循环队列空状态与满状态无法区分
3、循环队列:一个头尾相接的顺序表,且尾指针永远指向空,队列中需要冗余一位空结点,这样的队列即可满足上述多有问题。
二、队列的实现(链表)
1.队列的开辟
队列结构的构造包含两个指针:front 指向队头(类似头指针)出队使用,rear 指向队尾,进队使用;
typedef int QDataType;
// 定义单链表结点
typedef struct QNode
{
QDataType data;
struct QNode* next;
}QNode;
// 定义队列结构体
typedef struct Queue
{
QNode* front; //指向队头结点 队头:出队
QNode* rear; //指向队尾结点 队尾:入队
int size; //队列元素个数
}Queue;
2.队列的函数(与链表类似)
【注意】由于定义了队列的结构,指向链表结点的指针被封装在其结构内部,故队列函数的参数采用一级指针即可
- 初始化队列
void QueueInit(Queue* q)
{
assert(q);
q->front = NULL;
q->rear = NULL;
q->size = 0;
}
- 销毁队列
销毁不能忘!
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->front;
while (cur)
{
q->front = cur->next;
free(cur);
cur = q->front;
}
q->front = NULL;
q->rear = NULL;
q->size = 0;
}
- 入队列
耦合出申请新链表结点的函数QNode* buyQNode(QDataType data)
QNode* buyQNode(QDataType data)
{
QNode* newQNode = (QNode*)malloc(sizeof(QNode));
if (NULL == newQNode)
{
assert(0);
return NULL;
}
newQNode->data = data;
newQNode->next = NULL;
return newQNode;
}
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* newQNode = buyQNode(data);
//1.队列为空
if (QueueEmpty(q))
{
q->front = newQNode;
}
else
{
//2.队列已有元素
q->rear->next = newQNode;
}
//3.调整队列其余元素
q->rear = newQNode;
q->size++;
return;
}
- 检测队列是否为空
int QueueEmpty(Queue* q)
{
assert(q);
return 0 == q->size;
}
- 出队列
void QueuePop(Queue* q)
{
assert(q);
//1.队列为空
if (QueueEmpty(q))
{
assert(0);
return;
}
else
{
//2.队列已有元素
QNode* delNode = q->front;
q->front = q->front->next;
q->size--;
free(delNode);
if (NULL == q->front)
{
q->rear = NULL;
}
}
}
- 获取队头、尾元素
// 获取队头元素
QDataType QueueFront(Queue* q)
{
//队列为空情况设为违法
assert(!QueueEmpty(q));
return q->front->data;
}
// 获取队尾元素
QDataType QueueBack(Queue* q)
{
//队列为空情况设为违法
assert(!QueueEmpty(q));
return q->rear->data;
}
- 获取队列中有效元素个数
int QueueSize(Queue* q)
{
assert(q);
return q->size;
}
源代码
三、循环队列的实现
循环队列结构 初始化
注意循环队列实际申请空间是其实际容量+1
//循环队列以定长数组构成的顺序表为基础
//注意队列特性 前出后进(头删与尾插)
//循环队列结构
typedef struct
{
int* array; // 指向存储元素空间的起始位置
int front; // 队首元素
int rear; // 队尾元素
int size; // 队列可存放元素
} MyCircularQueue;
//初始化队列
MyCircularQueue* myCircularQueueCreate(int k)
{
if (k < 0)
{
return NULL;
}
MyCircularQueue* mcq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if (NULL == mcq)
{
assert(0);
return NULL;
}
//循环队列实际申请空间是他的容量+1
//这样可以区分空队列与满队列
mcq->array = (int*)malloc(sizeof(int) * (k + 1));
mcq->front = 0;
mcq->rear = 0;
mcq->size = k;
return mcq;
}