栈和队列
前言
前面一些天学了栈和队列,想反过头来复习复习,加深下印象,有什么不对的还望指正。
一、栈
1.栈的概念
栈(Stack):是一种特殊的线性表,它是限定仅在
表尾
\color{red}{表尾}
表尾进行插入和删除操作的线性表。栈又称为
后进先出
\color{red}{后进先出}
后进先出的线性表。我们把
允许插入和删除的一端称为栈顶
\color{red}{允许插入和删除的一端称为栈顶}
允许插入和删除的一端称为栈顶,把另一端称为栈底,不含任何元素的栈称为空栈,栈又称为后进先出的线性表
压栈:栈的插入操作叫做进栈/压栈/入栈,
入数据在栈顶
\color{red}{入数据在栈顶}
入数据在栈顶。
出栈:栈的删除(取出)操作叫做出栈,
出数据也在栈顶
\color{red}{出数据也在栈顶}
出数据也在栈顶
2.栈的主要实现函数
void StackInit(ST* ps):栈的初始化
void StackDestroy(ST* ps):栈的销毁
void StackPush(ST* ps, STDataType x):入栈
void StackPop(ST* ps):每次调用删除栈内的一个数据
STDataType StackTop(ST* ps):出栈,即取出一个数据,但是不负责删除数据
int StackSize(ST* ps):求栈的数据个数
bool StackEmpty(ST* ps):判断栈是不是空栈,若是,则返回true,若否,则返回false
3.栈的实现方式
栈主要用
数组或者链表
\color{red}{数组或者链表}
数组或者链表来实现,相比于链表来说,数组栈(顺序栈)优势更大
原因:
数组栈尾插尾删和链表头插头删效率都高,但是数组栈的缓存利用率更高,虽然有时可能存在一点空间浪费,但是整体而言更优,并且链表每次插入还得消耗一定的指针存储。
而链表栈的话
有两种实现方式:
单链表:以头为栈顶,尾为栈底,进行头插头删(反过来的话,需要找前一个数据,效率低)
双链表:以尾为栈顶,头为栈底直接尾来尾插尾删
下面的代码我使用数组(顺序表)来实现栈
4.栈的代码实现
(1)栈的基本结构
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;//当前数据个数
int capacity;
}ST;
(2)栈的初始化
这个图片的初始化top给的是-1,因此当栈中有数据时它的 t o p 指向的是当前位置 \color{red}{top指向的是当前位置} top指向的是当前位置,而top=0的话,指向的位置则是 当前位置的下一个位置 \color{red}{当前位置的下一个位置} 当前位置的下一个位置。
void StackInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;//top给0,指向栈顶数据的下一个;top=-1,指向栈顶数据(要先加1,再赋值)
ps->capacity = 0;//栈的容量
}
(3)栈的销毁
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
(4)入栈
void StackPush(ST* ps, STDataType x)//入栈
{
assert(ps);
if (ps->top == ps->capacity)//相等有两个原因1.栈为空 2.栈满了
{
//先判断栈是满还是空,然后一般开辟两倍的空间
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = realloc(ps->a, sizeof(STDataType)*newcapacity);
if (tmp == NULL)
{
printf("开辟失败\n");
exit(-1);
}
ps->capacity = newcapacity;//把开好的给给它
ps->a = tmp;
}
ps->a[ps->top] = x;//入栈
ps->top++;//往后走一步
}
(5)栈中数据的删除
删除数据就是让它的下标访问不到,下次入栈又可以直接覆盖,不会造成影响
但是要注意的是当栈为空时,就不能够删了。
void StackPop(ST* ps)//删除
{
assert(ps);
assert(!StackEmpty(ps));//防止一直删,删过头了
ps->top--;
}
(6)出栈
只出数据,不负责删除
STDataType StackTop(ST* ps)//出栈
{
assert(ps);
assert(!StackEmpty(ps));//防止都为空了,还在调用;assert为假,就会报错
return ps->a[ps->top-1];//初始化是top=0,所以top此时是下一个位置,-1才是最后的数据下标
//只负责出栈,不负责删除
}
(7)求栈的个数
int StackSize(ST* ps)//里面的数据个数
{
assert(ps);
return ps->top;
}
(8)判断是否为空栈
返回的方法很巧妙
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;//=0,为真,返回true,不为0,为假
}
5.完整代码
(1)Stack.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void StackInit(ST* ps);
void StackDestroy(ST* ps);
void StackPush(ST* ps, STDataType x);
void StackPop(ST* ps);
STDataType StackTop(ST* ps);
int StackSize(ST* ps);
bool StackEmpty(ST* ps);
(2)Stack.c
#include"Stack.h"
void test1()
{
ST st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
printf("%d ", StackTop(&st));
StackPop(&st);
printf("%d ", StackTop(&st));
StackPop(&st);
StackPush(&st, 5);
StackPush(&st, 6);
while (!StackEmpty(&st))
{
printf("%d ", StackTop(&st));
StackPop(&st);
}
StackDestroy(&st);//每次
}
int main()
{
test1();
return 0;
}
(3)Stack1.c
#include"Stack.h"
void StackInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;//top给0,指向栈顶数据的下一个,top=-1,指向栈顶数据(要先加1)
ps->capacity = 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 = realloc(ps->a, sizeof(STDataType)*newcapacity);
if (tmp == NULL)
{
printf("开辟失败\n");
exit(-1);
}
ps->capacity = newcapacity;
ps->a = tmp;
}
ps->a[ps->top] = x;
ps->top++;
}
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
void StackPop(ST* ps)//删除
{
assert(ps);
assert(!StackEmpty(ps));//防止一直删,删过头了
ps->top--;
}
STDataType StackTop(ST* ps)//出栈
{
assert(ps);
assert(!StackEmpty(ps));//防止都为空了,还在调用;assert为假,就会报错
return ps->a[ps->top-1];//初始话是top=0,所以top此时是下一个位置,-1才是最后的数据
//只负责出栈,不负责删除
}
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;//=0,为真,返回true,不为0,为假
}
int StackSize(ST* ps)//里面的数据个数
{
assert(ps);
return ps->top;
}
6.栈的(递归)应用
栈的可以用来实现递归
每递归一层,就会在栈(操作系统中的栈)中建立栈帧,递归次数多了,就会导致栈帧深度太深,栈空间不够,可能会溢出
对于递归改非递归:1.简单的用循环 2.复杂的借助栈来实现
像一般的斐波那契和阶乘使用循环就能解决,之前在学快排时,学习过快排改非递归的实现,我在这里就简单介绍一下吧:
快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,以第一个为关键字和其他数进行比较,分成一边比它小,一边比它大,之后再分别对这两部分记录继续进行排序,直到分解到只剩下左边一个右边一个,这时达到整个序列有序的目的。
栈的使用和递归相似,我们将边界下标入栈,栈是先进后出,如果我们要先排左边的话,就得先入右边的下标,我们每入一次栈就出一次栈,然后挖坑排序,重复操作,直到分解到左右边界相等,这时说明已经到底了,不用再分了,当栈最终为空时,排序就排好了。
这里我只是浅显地说几句,主要还得看各位下去后仔细研究。
void QuickSortNonR(int*a,int n)
{
ST st;
StackInit(&st);
StackPush(&st, n - 1);//先进后出
StackPush(&st, 0);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int keyindex = partsort(a, left, right);//挖坑法得出中间的下标
//[left,keyindex-1] keyindex [keyindex+1,right]
//当两边的边际相等时,说明已经递归到最低部了,上去一层,左右区间都只有一个数
//即都是有序的了
if (keyindex + 1 < right)//注意入栈顺序,先出左,那就要先入右
{
StackPush(&st, right);
StackPush(&st, keyindex + 1);
}
if (left < keyindex - 1)//
{
StackPush(&st, keyindex - 1);
StackPush(&st,left );
}
}
StackDestroy(&st);
}
挖坑法:每次以左第一个为关键字,左边有坑,到右边找小的,找到后放到坑里,坑更新到右边,继续到左边找大的,当两边边界相等时,这时左右区间分成了一大一小的区间。
int partsort(int* a, int left, int right)//将挖坑法提了出来
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int begin = left, end = right;
int pivot = begin, key = a[begin];
while (begin < end)
{
//右边找小,放到左边的坑
while (begin < end && a[end] >= key)
{
end--;
}
//填坑,更新坑
a[pivot] = a[end];
pivot = end;
//左边找大,放到右边
while (begin < end && a[begin] <= key)
{
begin++;
}
a[pivot] = a[begin];
pivot = begin;
}
a[pivot] = key;//循环出来时,那个坑就是放key
return pivot;
}
二、队列
1.队列的定义
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出的特点。(这里和栈是相反的)
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
实际中我们有时还会使用一种队列叫循环队列,长这样:
但是由于这种的队列我没怎么学过,
还有看其他的资料还有双端队列,这个我也没学,因此在这里我也是能力有限,无法帮到大家了。
2.队列的实现
队列也可以用数组和链表的结构实现,使用链表的结构实现更优一些
如果使用数组的话,出入数据时,需要挪动数据,因此效率上比较低,而链表就蛮适合的,具体使用双链表还是单链表就看自己的了,我使用的是单链表。
入队列从队尾入,直接让尾的next指向新开的节点,尾指针rear往后走走
出队列从队头出,取出数据后,断开front,让它往后移,成为新的头
3.队列的基本结构
typedef int QDataType;
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next;
}QueueNode;
typedef struct Queue
{
QueueNode* head;
QueueNode* tail;
}Queue;
4.队列的主要实现函数
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):取队尾的数据
int QueueSize(Queue* pq):队列的数据个数
bool QueueEmpty(Queue* pq):判断队列是否为空
5.玩转队列代码
(1)队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
(2)(队列的销毁)
销毁数据得从头到尾一个一个释放,直到cur为空,最后置空指针
void QueueDestroy(Queue* pq)
{
assert(pq);
QueueNode* cur = pq->head;
while (cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = NULL;
pq->tail = NULL;
}
(3)队列的插入函数
void QueuePush(Queue* pq, QDataType x)//从队尾插入
{
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
printf("开辟新节点失败");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
//以上是开辟新节点并赋值x
if (pq->head == NULL)//不要忘记
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;//链接新节点
pq->tail = newnode;
}
}
(4)队列的删除函数
这个函数还是有很多细节要我们注意的
队列不能为空
\color{red}{队列不能为空}
队列不能为空,当删除完一个数后
还得判断是不是删完了
\color{red}{还得判断是不是删完了}
还得判断是不是删完了,删完了还要
将最后一个指针置空
\color{red}{将最后一个指针置空}
将最后一个指针置空
细节很多
void QueuePop(Queue* pq)//只能从队头开始删
{
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* next = pq->head->next;//这里要注意的是pq->head不能为空
//如果为空,还去->next,那么程序就崩溃了,所以上面加了断言
free(pq->head);
pq->head = next;
if (pq->head == NULL)//说明队列已经删完了,此时pq->tail被free,但是为野指针
{
pq->tail = NULL;//应当置空
}
}
(5)取队头数据
QDataType QueueFront(Queue* pq)//取头的数据
{
assert(pq);
assert(!QueueEmpty(pq));//!!!
return pq->head->data;//同样注意pq->head为不为空
}
(6)取队尾数据
QDataType QueueBack(Queue* pq)//取队尾的数据
{
assert(pq);
assert(!QueueEmpty(pq));//
return pq->tail->data;
}
(7)求队列的数据个数
int QueueSize(Queue* pq)//采用计数的方法来
{
assert(pq);
int n = 0;
QueueNode* cur = pq->head;
while (cur)
{
n++;
cur = cur->next;
}
return n;
}
(8)判断队列为不为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;//
}
6.完整代码
(1)头文件Queue.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int QDataType;
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next;
}QueueNode;
typedef struct Queue
{
QueueNode* head;
QueueNode* tail;
}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);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
(2)Queue.c
#include "Queue.h"
//队列先进先出
//队头出,队尾入
//删除-》出队列
//插入-》入队列
void test1()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
printf("%d\n", QueueFront(&q));
QueuePop(&q);
QueuePush(&q, 4);
//printf("%d\n", QueueBack(&q));
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
QueueDestroy(&q);
}
int main()
{
test1();
return 0;
}
(3)Queue1.c(主要函数实现)
#include "Queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QueueNode* cur = pq->head;
while (cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = NULL;
pq->tail = NULL;
}
void QueuePush(Queue* pq, QDataType x)//从队尾插入
{
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
printf("开辟新节点失败");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
if (pq->head == NULL)//不要忘记
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
}
void QueuePop(Queue* pq)//只能从队头开始删
{
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
if (pq->head == NULL)//说明队列已经删完了,此时pq->tail被free,但是为野指针
{
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;
}
int QueueSize(Queue* pq)//采用计数的方法来
{
assert(pq);
int n = 0;
QueueNode* cur = pq->head;
while (cur)
{
n++;
cur = cur->next;
}
return n;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;//
}
总结及相关题目小试
栈和队列两种结构相反,但是主要的实现函数差不多都是相同的,另外这里还有两道题
用队列实现栈
用栈实现队列
大家可以去试试,这样更好的理解这两种结构。好了今天的讲解也就结束了,希望对大家有帮助。