一.栈(stack)
1.栈的定义
首先,栈是一种线性表,一种只允许在表尾进行插入和删除操作的特殊线性表
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据的栈称为空栈。遵循后进先出LIFO(Last In First Out)的原则
2.栈的相关操作
- 压栈:栈的插入操作叫做进栈/压栈/入栈,如同子弹入弹夹,其入数据在栈顶。
- 出栈:栈的删除操作叫做出栈,如同弹夹中的子弹出夹,其出数据也在栈顶。
它的结构如下图所示:
3.栈的实现
前面提到,栈是一种特殊的线性表,所以,栈应该就可以通过类似顺序表,用数组来实现,这种用数组来实现的栈,被称为顺序栈。 还有一种栈是用链式结构来实现的,被称为链栈,这里不做介绍。
先定义栈:
typedef char STDataType;
typedef struct Stack
{
STDataType* a;
int top; // 栈顶
int capacity; // 容量
}Stack;
3.1 初始化栈
接下来在初始化栈的时候会有一个小问题:栈为空时,如何设置栈顶(top)的值?
既然是数组,那么 top 的值就应该为0,因为数组下标就是从0开始加的,但其实这里 top 的值应设置成 -1
top 的值为 0 和 -1 的区别:
- 0:当 top 的值为 0 时,容易引发歧义。top == 0的时候,栈里是一个元素还是空?因为数组下标是从0开始算的。
- -1:而当 top 的值为 -1 时,先 ++top ,再压栈,意思就很明了,a[top] = x。
此时:
栈空条件:top == -1;
栈满条件:top == capacity-1;
栈容量: top+1;
所以初始化栈:
void StackInit(Stack* ps) {
assert(ps);
ps->a = NULL;
ps->top = -1;
ps->capacity = 0;
}
解决了这个问题,下面开始实现栈
3.2 入栈和出栈
// 入栈/压栈/进栈
void StackPush(Stack* ps, STDataType data) {
assert(ps); //保证栈不是NULL
if (ps->capacity == ps->top+1) { //判断栈是否已满
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity); //扩容用realloc
if (tmp == NULL) //防止开辟失败
{
perror("realloc");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->top++;
ps->a[ps->top] = data;
}
// 出栈
void StackPop(Stack* ps) {
assert(ps);
assert(ps->top >= 0); //保证不是空栈
ps->top--;
}
3.3 获取栈顶元素及栈中元素个数
操作比较简易。
// 获取栈顶元素
STDataType StackTop(Stack* ps) {
assert(ps);
assert(ps->top >= 0); //确保栈内有元素
return ps->a[ps->top];
}
// 获取栈中有效元素个数
int StackSize(Stack* ps) {
assert(ps);
return (ps->top) + 1;
}
3.4销毁栈
直接释放,置空,数据复原即可。
// 销毁栈
void StackDestroy(Stack* ps) {
assert(ps);
free(ps->a);
ps->a = NULL; //别忘了置空
ps->top = -1;
ps->capacity = 0;
}
二.队列(queue)
1.队列的定义
和栈一样,队列也是一种线性表,一种只允许在一端进行插入操作,而在另一端进行删除操作的特殊线性表
我们把允许插入的一端称为队尾(rear),允许删除的一端称为队头(front)。队列遵循后进先出FIFO(First In First Out)的原则
2.栈的相关操作
- 入队列:队列的插入操作叫做入队,其入数据在队尾。
- 出队列:队列的删除操作叫做出队,其出数据在队头。
它的结构如下图所示:
3.队列的实现
前面提到的,和栈一样,队列也是一种特殊的线性表,那么,队列是否适合用数组来实现呢?
答案是否定的(但是循环队列是用数组实现的!!!)
使用数组实现队列可能会有以下问题:
-
静态大小:数组需要事先声明大小,当队列超过数组大小时,无法继续入队,这限制了队列的大小。
-
内存浪费:如果数组大小过大,而实际队列元素数量较少,会造成内存浪费。例如,声明一个大小为100的数组,但实际队列元素数量只有10个,就浪费了90个位置的内存。
-
入队和出队效率低:使用数组实现队列时,入队操作(将元素放在队尾)需要移动已有元素来腾出位置,出队操作(从队头取出元素)需要将整个数组向前移动,这样的操作会导致时间复杂度为O(n)的开销
所以,队列采用链式结构存储。
定义队列:
//结构体嵌套,Queue是队列本身
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* next;
QDataType data;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* front;
QNode* rear;
}Queue;
3.1 初始化队列
// 初始化队列
void QueueInit(Queue* q) {
assert(q);
q->front = q->rear = NULL;
}
3.2 入队列,出队列
需要注意的是,在每次入队时都需要更新队尾,在每次出队时都需要更新队头
// 队尾入队列
void QueuePush(Queue* q, QDataType data) {
assert(q);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = data;
newnode->next = NULL;
if (q->rear == NULL) //检查队列是否为空队列
q->rear = q->front = newnode;
else {
q->rear->next = newnode;
q->rear = newnode; //更新队尾
}
}
// 队头出队列
void QueuePop(Queue* q) {
assert(q);
assert(q->front);
QNode* del = q->front;
q->front = q->front->next; //更新队头
free(del);
del = NULL; //释放,置空
if (q->front == NULL) //这时,如果队头为空,就说明队列里没有元素了,队尾也应为空。
q->rear = NULL;
}
3.3 获取队头,队尾元素及队列中元素个数
// 获取队列头部元素
QDataType QueueFront(Queue* q) {
assert(q);
assert(q->front);
return q->front->data;
}
// 获取队列队尾元素
QDataType QueueBack(Queue* q) {
assert(q);
if (q->rear == NULL)
return 0;
else
return q->rear->data;
}
// 获取队列中元素个数
int QueueSize(Queue* q) {
assert(q);
int count = 0; //计数
QNode* cur = q->front; //创建一个指向队头的指针
while (cur) {
cur = cur->next;
count++; //累加起来就是队列的元素个数
}
return count;
}
3.4 销毁队列
如果循环没结束,就代表队列里还有元素,就调用 QueuePop 函数。
// 销毁队列
void QueueDestroy(Queue* q) {
assert(q);
while (q->rear)
QueuePop(q);
}
三.总结
栈和队列它们都是特殊的线性表,只不过对插入和删除操作做了限制。
- 栈(stack)只允许在表尾进行插入和删除操作。
- 队列(queue)只允许队尾(rear)进行插入操作,而在队头(front)进行删除操作。
对于二者来说,都可以使用链式存储结构来实现,顺序结构中,队列引入了循环队列(circular queue)。
而它们二者又可以相互实现。用队列实现栈,用栈实现队列。