【浅学数据结构】数据结构之“栈与队列”

一、 栈的定义

栈(stack)是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶(Top),另一端称为栈底(Bottom),没有数据元素的栈称为空栈,栈有称为后进先出LIFO(Last in First out)的线性表。
栈的插入操作,叫做进栈,也称压栈、入栈。
栈的删除操作,叫做出栈。也称弹出

如图:
栈结构图

注意:

  1. 栈是基于线性表的,也就是它有前驱后继关系。只不过它是特殊的线性表而已。在线性表的表尾进行插入和删除操作,这里表尾是栈顶,而不是栈底。
  2. 它的特殊之处在于限制了只在栈顶进行插入和删除。栈底是固定的,最先进栈的元素只能在栈底。
  3. 最先进栈的不一定最后出栈,怎么理解这句话呢?简单说就是栈对线性表的插入和删除位置做了限制,但并没有对元素的进出的时间做限制。也就是说。在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈即可。

栈的顺序存储结构

既然栈是线性表的特例,那么栈的顺序存储其实也是线性表顺序存储的简称,我们简称为顺序栈。线性表是用数组来实现的,想想看,对于栈这种只能一头插入删除的线性表来说,用数组那一端来作为栈顶和栈底比较好呢?用下标为0的一端作为栈底比较好,因为首元素都存在栈底,变化最小。
栈的代码结构如下:

typedef int ElemType;
typedef struct
{
	ElemType data[MAXSIZE];
	int top;
}SqStack;

若现在有一个栈,栈大小是5,则栈普通情况、空栈和栈满的情况示意图如下:
栈结构图
进栈出栈操作push代码如下:

/*进栈*/
Status push(SqStack *s,ElemType e)
{
	if (s->top == MAXSIZE - 1)/*栈满*/
	{
		return ERROR;
	}
	s->top++; /*栈顶指针增加1*/
	s->data[s->top]=e;/*插入新元素,赋值栈顶空间*/
	return OK;
}
/*出栈*/
Status pop(SqStack *s,ElemType *e)
{
	if(s->top==-1)
		return ERROR;
	*e =s->data[s->top]; /*将要删除的栈顶元素赋值给e*/
	s->top--; /*栈顶指针减1*/
	return OK:
}

由于两者没有涉及到任何循环语句,因此时间复杂度均为O(1)。

栈的链式存储结构

说完了栈的顺序存储结构,我们现在来看看栈的链式存储结构,简称为链栈。
想想看,栈只是栈顶来插入和删除操作,栈顶放在链表的头部还是尾部呢?由于单链表有头指针,而栈顶指针也是必须的,所以比较好的办法是把栈顶放在单链表的头部。另外,都已经有了栈顶的头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的。
如图:
在这里插入图片描述
对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间了!但对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top=NULL的时候。

对比一下顺序栈与链栈,对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开学,但它的长度无限制。所以他们的区别和线性表中讨论的一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈。

栈的作用

栈的引入简化了程序设计的问题,划分了不同关注层次,使用思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去考虑数组的下标增减等细节问题,反而掩盖了问题的本质。

所以现在很多高级语言,比如Java、C#等都有对栈结构的封装,你可以不用关注实现细节,就可以直接使用stack的push和pop方法,非常方便。

栈的应用之递归

在高级语言中,调用自己和其他函数并没有本质的不同。我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。

当然,写递归程序员最怕的就是陷入永不结束的无穷递归中,所以,每个递归定义必须至少有一个条件,满足时递归结束。

那么我们讲了递归和栈有什么关系呢?这得从计算机内部说起。

简单的说,就是前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压如栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复调用状态。

栈的应用之四则运算表达式求值

我们小学数学的时候,老师反复强调,“先乘除,后加减,从左算到右,先括号内后括号外”。我想大家都不陌生;

而计算机是如何实现这个的呢?答案是后缀表示法,规则是从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。

举个例子:
标准四则运算表达式 7+(2-1)×3+10÷2 转为后缀表达式为"721-3*+102/+"。
规则:从左到右遍历表达式的每个数字和符号,若是数字直接输出,若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。

步骤如下:

  1. 初始化一空栈,用来对符号进出栈使用。
  2. 第一个字符数字7,输出“7”,后面是符号“+”,进栈。
  3. 第三个字符是“(”,依然是符合,因其至少做括号,还未配对,故进栈。
  4. 第四个字符是数字2,输出,总表达式为“72”,接着是“-”,进栈。
  5. 接下来是数字“1”,输出,总表达式为“721”,后面是符号“)”,此时,我们需要去配对此前的“(”,所以栈顶依次出栈,并输出,直到“(”出栈为止。此时左括号上方只有“-”,因此输出“-”。总输出表达式为“721-”。
  6. 接着是数字3,输出,总表达式为721-3。接着是符号“×”,因为此时的栈顶符号为“+”号,优先级低于“×”,因此不输出,“*”进栈。
  7. 之后是符号“+”,此时当前栈顶元素’’ * “比这个“+”的优先级别高,因此栈中元素出栈并输出(没有比“+”号更低的优先级,所以全部出栈),总输出表达式为 " 721-3*+”。然后将当前这个符号“+”进栈。
  8. 接着是数字10,输出,总表达式为"721-3*+10"。后是符号“÷”,所以“/”进栈。
  9. 最后一个数字2,输出总表达式为“721-3*+102”
  10. 因已经到最后,所以将栈中符号全部出栈并输出。最终输出的后缀表达式结果为721-3*+102/+

整个过程,充分利用了栈的后进先出特性来处理,理解好它其实而已就理解了栈这个数据结构。

二、队列的定义

背景描述,就好比运营商的客服人员与客户的关系,在所有客服人员都占线的情况下,客户会被要求等待。直到有某个客服人员空下来,才能让最新等待的客户接通电话。这里也是将所有当前拨打客服电话的客户进行了排队处理。

从这个客户系统中,应用了一种数据结构来实现先进先出的排队功能,这就是队列。
队列(queue)是一种先进先出(First in first out)简称FIFO。只允许在一端(队尾)进行插入操作,而在另一端(队头)进行删除操作的线性表。
队列在程序设计中用得非常频繁。

循环队列

线性表有顺序存储和链式存储,栈是线性表,所以有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。即首尾相连;

队列的链式存储结构

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,简称为链队列。为了操作上方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端节点。
如图:
链队列结构
总体来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列

猜你喜欢

转载自blog.csdn.net/xia296/article/details/86467312