栈和队列是两种重要的线性结构,从数据结构来看,他们也是线性表的一部分,属于操作受限的线性表,也被称为限定性的数据结构。
但从数据类型角度来看,它们是和线性表不大相同,有些时候它们被当作一种管理数据的规则。
栈
栈的介绍
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
不管是入栈还是出栈都只能从栈顶操作,遵循“先进后出,后进先出的”原则。
栈的实现
栈有两种,分别为用顺序表存储的顺序栈和用链表存储的链栈,最常见的栈是顺序栈
创建和初始化
typedef char STDataType;//重命名数据类型
typedef struct Stack
{
STDataType* a;//栈底指针
int top;//栈顶位置
int capacity;//栈的最大容量
}ST;//重命名
//初始化栈
void StackInit(ST* ps)
{
ps->a = NULL;
ps->capacity = ps->top = 0;
}
入栈
入栈时要先检查栈的空间,如果空间满了就动态开辟一个新的更大的空间用来存数据
void StackPush(ST* ps, STDataType x)
{
assert(ps);
//检查扩容
if (ps->top == ps->capacity)
{
//为空就增加四个空间,不为空就扩充二倍
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapacity;
}
//添加数据
ps->a[ps->top] = x;
ps->top++;
}
出栈
因为用顺序表存储,所以删除非常简单,只需要把记录栈顶位置的数字减少一就可以,不需要删除记录的内容,查找的时候也是按照栈顶查找,添加的时候就会把旧的数据覆盖掉,不会存在问题。
void StackPop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));//栈为空就报错
ps->top--;
}
栈的销毁
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
栈的其他操作
这些操作非常简单,但是推荐单独写一个函数用来调用,保证代码的耦合度。
判断栈是否为空
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
栈顶数据
STDataType StackTop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
栈存储的数据个数
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
队列
队列的介绍
队列(queue)也是一种特殊的线性表,特殊之处在于它只允许在队列的前端进行删除操作,而在队列的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头(先入先出)。一般应用于业务处理,例如:银行叫号系统,12306购票系统,电商的订单处理等。
队列的实现
创建和初始化
根据队列先入先出的特性,采用链表的方式创建队列最为合适。
typedef int QDataType;//重命名数据类型
typedef struct QDataNode
{
struct QDataNode* next;//指向下一个节点
QDataType data;
}QNode;
//封装一个结构体用来保存指向头和尾的指针并记录队列的个数
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
入队
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
//创建新的节点
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->next = NULL;
//队列为空时单独处理
if (pq->head == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
newnode->data = x;
pq->size++;
}
出队
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));//判断队列是否为空
QNode* del = pq->head;
pq->head = pq->head->next;
free(del);
del = NULL;
if (pq->head == NULL)
pq->tail = NULL;
pq->size--;
}
队列的销毁
void QueueDstroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur != NULL)
{
QNode* del = cur;
cur = cur->next;
free(del);
}
pq->head = pq->tail = NULL;
}
队列的其他操作
队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
判断队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL && pq->tail == NULL;
}
队列中的数据个数
int QueueSize(Queue* pq)
{
assert(pq);
//若创建队列时没有添加记录队列个数的类型时遍历队列
//QNode* cur = pq->head;
//int n = 0;
//while (cur != NULL)
//{
// cur = cur->next;
// n++;
//}
//return n;
return pq->size;
}
循环队列的思路
循环队列是指首尾相连可以循环使用的队列
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
循环队列判断是否为空时需要格外注意,有两个方法判断循环列表是否为空,一是假设需要一个长度为k的循环数组,则设总节点个数为k+1个,当tail+1 == head时数组为满。二是只用头节点在头节点内记录循环队列的个数或者单独维护一个记录队列的数据
如果用顺序表创建循环队列时,循环队列尾指针到顺序表最后一个位置还能加入数据时需要变到顺序表第一个位置,此时可以用(tail+1)%N来调整tail的位置(N == K + 1),head同理。