栈和队列是两种重要的线性结构,从数据结构的角度看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表的子集。他们是操作受限的线性表,因此,可称为限定性的数据结构。但从数据类型角度看,他们是和线性表大不相同的两类重要的的抽象数据类型。
笔者本周粗略学习了栈与队列的知识及其应用,特此总结记录自己的经验。
(学完之后cpu烧了/(ㄒoㄒ)/~~)
提示:以下是本篇文章正文内容,下面案例可供参考
栈(Stack):
一种可以实现“先进后出”的存储结构。可以将其想象为箱子,先放进箱子的东西后拿出来。
一种只允许在一段进行插入或删除的线性表。首先栈是一种线性表,但限定这种线性表只能在某一段进行插入和删除操作。
补:线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。数据元素是一个抽象的符号,其具体含义在不同的情况下一般不同。
(笔者还未具体学习过线性表,此段为百度copy)
栈顶(top):线性表允许进行插入与删除的那一端。
栈底(bottom):固定的,不允许进行插入与删除。
空栈:不含任何元素的空表。
简称: LIFO结构(last in first out)
分类:分为静态栈与动态栈。还可分为链栈,顺序栈,共享栈
附:链栈的实现操作与链表大同小异,因此在此不过多讲述, 可参考以下博客C语言实现链栈的创建、入栈、出栈、取栈顶、遍历…等基本操作(小白版详解)
在学习栈遇到的困难:博客上与网课的讲述内容例如创建栈方法有冲突,不知选择何种方法才是最优。
首先我们需要定义一个栈所需要的变量。
typedef struct
{
int* top;//栈顶指针
int* base;//栈底指针
int StackSize;//顺序栈的容量
}Sqstack;
然后我们开始初始化一个栈
void InitStack(Sqstack* s, int n)
{
s->base = (int*)malloc(sizeof(int) * n);//n的含义是栈长度
if (!s->base)//未初始化完成
{
return 0;
}
s->top = s->base;//使栈顶与栈底相同
s->StackSize = n;
许多参考资料上对于栈的容量是在开头定义了一个全局变量。笔者在此处的方法是可以让栈的长度可以根据我们的需求改变。
int EmptyStack(Sqstack* s)
{
return s->top == s->base;//如果栈顶等于栈底则为空栈
return 1;
}
int PushStack(Sqstack* s, ElemType e)//赋值e到顺序栈中,再让
{
if (s->top - s->base >= s->stack_size)//指针与指针相减,得出的结果为:两者之间的距离!
{
printf("栈满");
}
*s->top = e;//先赋值
s->top++;//使栈顶指针始终指向栈顶元素的下一个位置上
return 1;
}
需要始终记得一个原则:非空栈中的栈顶指针始终在栈顶元素的下一个位置上。(这点对于理解栈很重要)
当然,创建栈的方式有许多,我们在使用单调栈时,将栈顶指针指向栈顶元素是更加方便的,这里笔者只是单独写出了自己写入栈代码时的习惯
此处我们还需注意:指针与指针之间的相减。如果两个指针指向同一个数组,它们就可以相减,其结果为两个指针之间的元素数目。
void PopStack(Sqstack* s, int* e)//可以将进栈与出栈联合记忆:进栈先赋值后加,出栈与遍历先减后输出
{
if (s->top == s->base)//栈空
{
return 0;
}
s->top--;//先将栈顶指针移动到有元素的地方
*e = s->top; 栈顶指针先下移,后赋值
}
//获得栈顶的元素,参数e用来存放栈顶的元素
int GetTopStack(Sqstack* s, ElemType* e)
{
if (s->base == s->top)
return 0;
*e = *(s->top - 1);//top实际上是一个数组,*(s.top-1)表示的是栈顶元素,因为top指针始终指向栈顶元素的下一个位置
return 1;
}
取栈顶元素与出栈元素的不同就是取栈顶元素不需要改变栈顶指针,只需要移动位置
//销毁栈,释放栈空间,将栈顶栈底指针设置为NULL,长度置为0
int DestoryStack(Sqstack* s)
{
free(s->base);
s->base = s->top = NULL;
s->stack_size = 0;
return 1;
}
//遍历栈,依次打印每个元素
int StackTraverse(Sqstack* s)
{
if (s->top == s->base)
{
printf("空栈");
return 0;
}
ElemType* p;
p = s->top;//设立一个新指针从栈顶开始往下遍历
while (p > s->base)//因为在循环里面是先减的,所以不能是等于,只能是大于,最后一次循环时已经是栈顶指针下标为0
//base与top指针实质上是指向同一个数组
{
--p;
printf("%d ", *p);
}
}
int main()
{
Sqstack s;
int n;
int e;
printf("请输入栈的长度:");
scanf_s("%d", &n);
InitStack(&s, n);
for(int i = 0; i < n; ++i)
{
scanf_s("%d", &e);
PushStack(&s, e);
}
PopStack(&s, &e);//出栈栈顶元素
StackTraverse(&s);
}
我们对于单调栈可以将其分为两种
第一种:单增栈
从栈底到栈顶的元素逐步递增
第二种:单减栈
从栈底到栈顶的元素逐步递减
为了更好地理解单调栈,笔者在此给出单增栈的伪代码
int *num = int *(malloc)(sizeof(int)*len);//首先定义一个存放元素数组,len为其长度
int *s = int *(malloc)(sizeof(int)*len);//在c语言中,我们定义一个数组来模拟单调栈会更加方便,但在其他语言如c++中对于栈有着特定的实现方式。目前牧者只学习了c语言,后续学习其他语言将会不借用数组的单调栈的用法
//在这里我们需要明白数组与栈分别存放的是什么值。
//对于num,我们存放的是数组的元素,在此时也许读者并不明白其含义,在下面我将会用例题讲解
//对于单调栈s,我们存放的是数组num的下标
int top = -1//初始化栈
s[++top] = 0;//将第一个数组元素的下标入栈
int
for(int i=1; i<n; ++i)
{
if(当前元素大于等于栈顶元素)
{
将当前元素入栈;
}
else
{
while(当前栈不为空栈并且当前元素小于或等于栈顶元素)
{
栈顶元素出栈;
更新结果;
}
当前元素入栈;
}
}
下面笔者将使用单调栈的一道例题讲解单调栈的具体使用,如有不足,请不吝指出
题目: 柱状图中最大的矩形(题目地址)
首先我们需要明白我们需要创建一个单增栈
为什么是一个单增栈呢?
在我们不断将元素入栈时,假如有元素破坏了单调栈的单调性,那么我们需要对于出栈的元素依次向左与向右遍历,直到找到第一个比它小的值。
注意:对于c语言来说,我们入栈的元素是数组的下标
在我们做此题过程中,我们可以将其分为两大步来做
第一大步:
我们依次将不破坏单调栈的数组下标入栈,当出现会破坏单调栈的情况时,笔者用以下文字说明:
因为我们维护了一个单增栈,所以出栈元素的左边第一个元素一定比它小,而右边第一个元素的一定是即将入栈的i, 因为是数组下标为i的破坏了单调栈的单调性
注意:在这里的大小比较实际上是对于高度的大小比较
由此我们可以得到计算每个出栈的元素的面积的公式:
s = (i - stack[top-1] - 1) heights[top]
在此处:
i表示右边的一个比出栈元素大的值的下标
stack[–top]表示左边第一个比出栈元素小的值的下标
height[top]表示当前出栈元素所得长方体的高
以下粘贴代码说明
for (int i = 1; i < heightsSize; i++)//遍历数组
{
if (heights[i] >= heights[stack[top]])//入栈
{
stack[++top] = i;//先运算后赋值
}
else
{
while (top != -1 /*这是栈不是空栈的意思*/ && heights[i] < heights[stack[top]])//出栈并计算面积,维护递增性,需要对小于的元素全部出栈
{
if (top - 1 == -1)//最后一个栈顶元素,出栈计算面积需要包含一下前面和后面,因为矩形可以延伸,这里需要好好想一想
{
max = fmax(max, i * heights[stack[top]]);
}
else
{//stack[top-1]是出栈元素左边第一个比他小的元素下标 i是出栈元素右边第一个比他小的元素下标,二者之间的距离再减一便是宽
max = fmax(max, (i - stack[top - 1] - 1) * heights[stack[top]]);//栈中元素,计算面积与需要延伸,能延伸多长就延伸多长
}//出栈的元素的高就是要计算的长方形的高,出栈元素的左边一定是一定是比他小的值,右边比他小的元素一定是打破单调栈的新元素,因为我们维护了一个单增栈
--top;//将大于当前元素的弹出栈
}
stack[++top] = i;//当小于当前元素的栈元素全部都弹出后放入新的栈元素下标
}//将数组下标赋值给新元素。 尤其需要记得进入栈中的元素是下标,出栈的元素也是下标
}
第二大步:
当所有数组下标全部入栈后,我们可以开始依次将剩余的元素出栈,并计算其面积
while (top != -1)//数组元素全部遍历完了,单是栈还有元素,进行清空栈
{
if (top - 1 == -1)
{
max = fmax(max, (heightsSize)*heights[stack[top]]);//最小的元素向右延申面积的宽就是数组长度
}
else
{
max = fmax(max, (heightsSize - 1 - stack[top - 1]) * heights[stack[top]]);
}
--top;
}
对于if语句的情况,因为此时他是所有数组元素中最小的元素,所以它可以任意向左向右延申计算面积,因此宽为heightsSzie
对于else语句的情况,heightSize实际上就表示在出栈元素右边第一个比出栈元素小的下标值,stack[–top]依旧表示左边第一个比出栈元素小的下标
在最后附上完整实现的代码
int main(void)
{
int heightsSize;
printf("请输入数组长度");
scanf_s("%d", &heightsSize);
int* heights = (int*)malloc(sizeof(int) * heightsSize);
for (int i = 0; i < heightsSize; ++i)
{
printf("请输入第%d个元素", i + 1);
scanf_s("%d", &heights[i]);
}
int* stack = (int*)malloc(sizeof(int) * heightsSize);
int top = -1;
stack[++top] = 0;//将第一个元素入栈
int max = -1;
for (int i = 1; i < heightsSize; i++)//遍历数组
{
if (heights[i] >= heights[stack[top]])//入栈
{
stack[++top] = i;//先运算后赋值
}
else
{
while (top != -1 /*这是栈不是空栈的意思*/ && heights[i] < heights[stack[top]])//出栈并计算面积,维护递增性,需要对小于的元素全部出栈
{
if (top - 1 == -1)//最后一个栈顶元素,出栈计算面积需要包含一下前面和后面,因为矩形可以延伸,这里需要好好想一想
{
max = fmax(max, i * heights[stack[top]]);
}
else
{//stack[top-1]是出栈元素左边第一个比他小的元素下标 i是出栈元素右边第一个比他小的元素下标,二者之间的距离再减一便是宽
max = fmax(max, (i - stack[top] + stack[top] - stack[top - 1] - 1) * heights[stack[top]]);//栈中元素,计算面积与需要延伸,能延伸多长就延伸多长
}//出栈的元素的高就是要计算的长方形的高,出栈元素的左边一定是一定是比他小的值,右边比他小的元素一定是打破单调栈的新元素,因为我们维护了一个单增栈
--top;//将大于当前元素的弹出栈
}
stack[++top] = i;//当小于当前元素的栈元素全部都弹出后放入新的栈元素下标
}//将数组下标赋值给新元素。 尤其需要记得进入栈中的元素是下标,出栈的元素也是下标
}
while (top != -1)//数组元素全部遍历完了,单是栈还有元素,进行清空栈
{
if (top - 1 == -1)
{
max = fmax(max, (heightsSize)*heights[stack[top]]);//最小的元素向右延申面积的宽就是数组长度
}
else
{
max = fmax(max, (heightsSize - 1 - stack[top - 1]) * heights[stack[top]]);
}
--top;
}
printf("%d", max);
}
//总结:此题可以将他分为两种情况
//第一种:在维护单调栈的过程中有元素弹出,那么需要不断计算弹出元素的最大面积。
//因为我们维护的是一个单增栈,所以弹出元素的左边的所有元素的值都是比他大的,又因为我们入栈的元素是一个下标,这样便可得到出栈的元素的最大面积的宽,而长便是出栈元素本身的数值
//第二种:当维护单调栈过程中并无元素弹出或是所有元素已经遍历完,那么需要将每个元素依次弹出计算最大面积
//top是栈的下标,i是数组的下标, 栈中元素实际上是数组的下标(!!!!重点)
笔者对于单调栈的问题理解并不深刻,后续将会补上更多关于单调栈的例题,如接雨水等问题
队列不同于栈,是一种先进先出的线性表,简称FIFO,允许插入的一段成为队尾,允许删除的一端称为队头
对于栈的现实举例:
当电脑死机时,我们将他重启,电脑会将我们重启前未执行的操作执行一遍,这其实是因为操作系统中的多个程序因需要通过一个通道输出,而按先后次序排队等待造成的。
又如移动联通等客服电话占线时的等待,在有客服空置时,最先等待的客户会最先得到客服的回应。
InitQueue(&Q):初始化队列,构造一个空队列Q。
QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
EnQueue(&Q, x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q, &x):出队,若队列Q非空,删除队头元素,并用x返回。
GetHead(Q, &x):读队头元素,若队列Q非空,则将队头元素赋值给x。
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针。
队列的顺序存储类型可描述为:
typedef struct//尾指针永远在新元素要放置的下一个位置上
{
int data[MAXSIZE];
int front;//头指针
int rear;//尾指针
}SqQueue;
初始状态(队空条件):Q->front == Q->rear== 0。
进队操作:队不满时,先送值到队尾,再将队尾指针加1。
出队操作:队不空时,先取队头元素值,再将队头指针加1
我们使用队尾指针进栈,头指针出栈。
注意:
尾指针永远在新元素要放置的下一个位置上
但是顺序队列有缺点:用以下图来解释:
如图d,队列出现“上溢出”,然而却又不是真正的溢出,所以是一种“假溢出”。
我们用通俗的语言来讲解,那么就像是前面有空座位,但是后排坐满了,你不可能不坐前排而下车,一定会将空的位置坐满。
解决假溢出的方法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
当队首指针Q->front = MAXSIZE-1后,再前进一个位置就自动到0,这可以利用除法取余运算(%)来实现。(对于队尾指针的原理也相同)
初始时:Q->front = Q->rear=0。
队首指针进1:Q->front = (Q->front + 1) % MAXSIZE。
队尾指针进1:Q->rear = (Q->rear + 1) % MAXSIZE。
队列长度:(Q->rear - Q->front + MAXSIZE) % MAXSIZE。
那么循环队列判断栈空与栈满的条件是什么呢?
如果我们入队的速度快于出队的速度,那么栈满的条件也是Q->front == Q->rare。那么便会产生冲突,为了避免这种情况,我们有两个方法取避免。
办法一:设置一个标志变量flag,当front == rare时,且flag0栈为空,当frontrear,且flag为1时为队列满。
办法二:当队列空时,条件就是front == rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,我们还有一个空闲单元。
通俗的理解便是我们在上公交车时如果发现只剩一个座位了,我们不会去坐。当然现实中这种情况并不会出现,这样举例是为了更好地理解。
此时我们便认为队列已满。
以下笔者重点来讨论第二种方法 :
由于rear既可能比front大,也可能比front小,所以他们只相差一个位置时便是满的情况,但也可能相差整整一圈。在这里我们需要理解尾指针总是指向下一个要执行操作的位置,所以在进行操作时可以先判断栈空或栈满。
所以
队满条件: (Q->rear + 1)%Maxsize == Q->front
队空条件仍: Q->front == Q->rear
接下来我们计算队列元素的个数,因为尾指针初始总是指向下一个需要执行操作的位置,头指针初始,总是指向当前操作元素的位置,在计算过程中rear与front的大小关系也无法确定,那么我们便可以加上数组长度来避免出现有rear-front可以为正也可以负的情况,所以我们得到:
队列中元素的个数: (Q->rear - Q ->front + Maxsize)% Maxsize
有了这些讲解,我们接下来开始实现循环队列
typedef struct//尾指针永远在新元素要放置的下一个位置上
{
int data[MAXSIZE];
int front;//头指针
int rear;//尾指针
}SqQueue;
int InitQueue(SqQueue* Q)//初始化队列
{
Q->front = 0;
Q->rear = 0;
return 1;
}
int QueueLength(SqQueue* Q)
{
return (Q->rear - Q->front + MAXSIZE) % MAXSIZE;
}
int EnQueue(SqQueue* Q, int e)
{
if ((Q->rear + 1) % MAXSIZE == Q->front)//判断栈满的条件
{
return 0;
}
Q->data[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAXSIZE;
return 1;
}
int DeQueue(SqQueue* Q, int* e)//int *e 用于返回其值
{
if (Q->front == Q->rear)
{
return 0;
}
*e = Q->data[Q->front];
Q->front = (Q->front + 1) % MAXSIZE;
return 1;
}
int main(void)
{
SqQueue Q;
int e ;
int* q = &e;
InitQueue(&Q);
for (int i = 0; i < 5; i++)
{
printf("请输入第%d个队列元素", i + 1);
scanf_s("%d", &e);
EnQueue(&Q, e);
}
DeQueue(&Q, &e);
printf("%d ", e);
DeQueue(&Q, &e);
printf("%d ", e);
return 0;
}
1.此周的学习内容比较多,但只是学到了上述知识的皮毛,之后笔者仍需对此处知识进行更加深层次的学习,如多刷有关单调栈的相关题目如接雨水等问题。
2.且在后续学习过程中随着知识广度的扩展还需明白优先队列的原理,还需要刷有关队列的题。