数据结构(C++)笔记:03.栈和队

3.1 栈

3.1.1 栈的逻辑结构

  1. 栈的定义
    栈是限定仅在表尾进行插入和删除操作的线性表。允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。

生活中栈的例子:
弹夹中的子弹,先进去的要后出来
计算机中栈的例子:
Word、PS的undo操作
首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,这:
里表尾是指栈顶,而不是栈底。
它的特殊之处就在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行。这也就使得:栈底是固定的,最先进栈的只能在栈底。
栈的插入操作,叫作进栈,也称压栈、入栈。类似子弹入弹夹。
栈的删除操作,叫作出栈,也有的叫作弹栈。如同弹夹中的子弹出夹。
2. 栈的抽象数据类型定义
ADT Stack
Data
栈中元素具有相同类型及后进先出特性,相邻元素具有前驱和后继关系
Operation
InitStack
前置条件:栈不存在
输入:无
功能:栈的初始化
输出:无
后置条件:构造一个空栈
DestroyStack
前置条件:栈已存在
输入:无
功能:销毁栈
输出:无
后置条件:释放栈所占用的存储空间
Push
前置条件:栈已存在
输入:元素值x
功能:在栈顶插入一个元素x
输出:如果插入不成功,抛出异常
后置条件:如果插入成功,栈顶增加了一个元素
Pop
前置条件:栈已存在
输入:无
功能:删除栈顶元素
输出:如果删除成功,返回被删元素值,否则,抛出异常
后置条件:如果删除成功,栈顶减少了一个元素
GetTop
前置条件:栈已存在
输入:无
功能:读取当前的栈顶元素
输出:若栈不空,返回当前的栈顶元素值
后置条件:栈不变
Empty
前置条件:栈已存在
输入:无
功能:判断栈是否为空
输出:如果栈为空,返回1,否则,返回0
后置条件:栈不变
endADT

3.1.2 栈的顺序存储结构及实现

  1. 栈的顺序存储结构——顺序栈
    栈的顺序存储结构称为顺序栈。
    a.把数组中下标为0的一端作为栈底,同时附设指针top指示栈顶元素在数组中的位置。
    b. 设存储栈元素的数组长度为StackSize,则栈空时栈顶指针top=-1;
    栈满时栈顶指针top=StackSize-1。
    c.入栈时,栈顶指针top加1;出栈时,栈顶指针top减1。
  2. 顺序栈的实现
    声明
const int StackSize=10;  //10只是示例性的数据,可以根据实际问题具体定义
template <class T>   //定义模板类SeqStack
class SeqStack
{
public:
    SeqStack( ) {top=-1;}  //构造函数,栈的初始化
    ~SeqStack( ) { }      //析构函数
    void Push( T x );      //将元素x入栈
    T Pop( );            //将栈顶元素弹出
    T GetTop( ) {if (top!=-1) return data[top];}  //取栈顶元素(并不删除)
    bool Empty( ) {top==-1? return 1: return 0;}  //判断栈是否为空
private:
    T data[StackSize];  //存放栈元素的数组
    int top;           //栈顶指针,指示栈顶元素在数组中的下标
};

入栈(插入)

template <class T>
void SeqStack::Push(T x)
{
  if (top== StackSize-1) throw "上溢";
  top++;
  data[top]=x;
}

出栈(删除)

template <class T>
T SeqStack:: Pop( )
{
  if (top==-1) throw "下溢";
  return data[top--];
}
  1. 两栈共享空间
    提出问题:在一个程序中如果需要同时使用具有相同数据类型的两个栈时,如何处理呢?
    解决方案一:为每个栈开辟一个数组空间;
    解决方案二:使用一个数组来存储两个栈,让一个栈的栈底为该数组的始端,另一个栈的栈底为该数组的末端,每个栈从各自的端点向中间延伸,如图所示。
    在这里插入图片描述
    其中,top1和top2分别为栈1和栈2的栈顶指针,StackSize为整个数组空间的大小,栈1的底固定在下标为0的一端;栈2的底固定在下标为StackSize-1的一端。
    其实栈的顺序存储还是很方便的,因为它只准栈顶进出元素,所以不存在线性表插入和删除时需要移动元素的问题。不过它有一个很大的缺陷,就是必须事先确定数组存储空间大小,万一不够用了,就需要编程手段来扩展数组的容量,非常麻烦。对于一个栈,我们也只能尽量考虑周全,设计出合适大小的数组来处理,但对于两个相同类型的栈,我们却可以做到最大限度地利用其事先开辟的存储空间来进行操作。
    打个比方,两个大学室友毕业同时到北京工作,开始时,他们觉得住了这么多年学校的集体宿舍,现在工作了一定要有自己的私密空间。于是他们都希望租房时能找到独住的一居室,可找来找去却发现,最便宜的一居室也要每月1500元,地段还不好,实在是承受不起,最终他俩还是合租了一套两居室,一共2000元,各出一半,还不错。
    对于两个一居室,都有独立的卫生间和厨房,是私密了,但大部分空间的利用率却不高。而两居室,两个人各有卧室,还共享了客厅、厨房和卫生间,房间的利用率就显著提高,而且租房成本也大大下降了。
    同样的道理,如果我们有两个相同类型的栈,我们为它们各自开辟了数组空间,极有可能是第一个栈已经满了,再进栈就溢出了,而另一个栈还有很多存储空间空闲。这又何必呢?我们完全可以用一个数组来存储两个栈,只不过需要点小技巧。
    其实关键思路是:它们是在数组的两端,向中间靠拢。top1和top2是栈1和栈2的栈顶指针,可以想象,只要它们俩不见面,两个栈就可以一直使用。
    从这里也就可以分析出来,栈1为空时,就是top1等于-1时;而当top2等于n时,即是栈2为空时,那什么时候栈满呢?
    想想极端的情况,若栈2是空栈,栈1的top1等于n-1时,就是栈1满了。反之,当栈1为空栈时,top2等于0时,为栈2满。但更多的情况,其实就是我刚才说的,两个栈见面之时,也就是两个指针之间相差1时,即top1+1==top2为栈事实上,使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。就像买卖股票一样,你买入时,一定是有一个你不知道的人在做卖出操作。有人赚钱,就一定是有人赔钱。这样使用两栈共享空间存储方法才有比较大的意义。否则两个栈都在不停地增长,那很快就会因栈满而溢出了。

3.1.3 栈的链接存储结构及实现

  1. 栈的链接存储结构——链栈
    栈的链接存储结构称为链栈。
    想想看,栈只是栈顶来做插入和删除操作,栈顶放在链表的头部还是尾部呢?由于单链表有头指针,而栈顶指针也是必须的,那干吗不让它俩合二为一呢,所以比较好的办法是把栈顶放在单链表的头部。另外,都已经有了栈顶在头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的。
    PS:第三版的ppt和书上p75的图3-5中,一定要注意,普通单链表中的第一个元素是从a1开始到an,而这里则调换了一个方向,栈顶指向的是an,如果不注意还以为插入和删除的一端在普通单链表的尾部,这是不对的。
  2. 链栈的实现
template <class T>
class LinkStack
{
public:
    LinkStack( ) {top=NULL;}  //构造函数,置空链栈
    ~LinkStack( );            //析构函数,释放链栈中各结点的存储空间
    void Push(T x);           //将元素x入栈
    T Pop( );                //将栈顶元素出栈
    T GetTop( ) {if (top!=NULL) return top->data;}  //取栈顶元素(并不删除)
    bool Empty( ) {top==NULL? return 1: return 0;}  //判断链栈是否为空栈
private:
    Node<T> *top;  //栈顶指针即链栈的头指针
};

链栈的基本操作示意图如下图所示:
在这里插入图片描述

3.1.4 顺序栈和链栈的比较

顺序栈和链栈的基本操作的时间性能比较
顺序栈和链栈的进栈push和出栈pop操作都很简单,没有任何循环操作,时间复杂度均,为O(1)
顺序栈和链栈的基本操作的空间性能比较
对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。

补充:栈的作用

有的同学可能会觉得,用数组或链表直接实现功能不就行了吗?干吗要引入栈这样的数据结构呢?这个问题问得好。
其实这和我们明明有两只脚可以走路,干吗还要乘汽车、火车、飞机一样。理论上,陆地上的任何地方,你都是可以靠双脚走到的,可那需要多少时间和精力呢?我们更关注的是到达而不是如何去的过程。
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去考虑数组的下标增减等细节问题,反而掩盖了问题的本质。
所以现在的许多高级语言,比如Java、C#等都有对栈结构的封装,你可以不用关注它的实现细节,就可以直接使用Stack的push和pop方法,非常方便。

3.2 队列

3.2.1 队列的逻辑结构

  1. 队列的定义
    队列是只允许在一端进行插入操作,而另一端进行删除操作的线性表。允许插入(也称入队)的一端称为队尾,允许删除(也称出队)的一端称为队头。
  2. 队列的抽象数据类型定义
ADT  Queue 
Data
   队列中元素具有相同类型及先进先出特性,相邻元素具有前驱和后继关系
Operation
 InitQueue
     前置条件:队列不存在
     输入:无
     功能:初始化队列
     输出:无
     后置条件:创建一个空队列
  DestroyQueue
     前置条件:队列已存在
输入:无
     功能:销毁队列
     输出:无
     后置条件:释放队列所占用的存储空间
  EnQueue 
     前置条件:队列已存在
     输入:元素值x
     功能:在队尾插入一个元素
     输出:如果插入不成功,抛出异常
     后置条件:如果插入成功,队尾增加了一个元素
  DeQueue 
     前置条件:队列已存在
     输入:无
     功能:删除队头元素
     输出:如果删除成功,返回被删元素值,否则,抛出删除异常
     后置条件:如果删除成功,队头减少了一个元素
  GetQueue
     前置条件:队列已存在
     输入:无
     功能:读取队头元素
     输出:若队列不空,返回队头元素
     后置条件:队列不变
  Empty 
     前置条件:队列已存在
     输入:无
     功能:判断队列是否为空
     输出:如果队列为空,返回1,否则,返回0
     后置条件:队列不变
endADT  

3.2.2 队列的顺序存储结构及实现

  1. 队列的顺序存储结构——循环队列
    在这里插入图片描述
    放宽队列的所有元素必须存储在数组的前n个单元这一条件,入队和出队操作时间性能为O(1),但由此造成了队列的单向移动。
    在这里插入图片描述
    现实当中,你上了公交车,发现前排有两个空座位,而后排所有座位都已经坐满,你会怎么做?立马下车,并对自己说,后面没座了,我等下一辆?
    没有这么笨的人,前面有座位,当然也是可以坐的,除非坐满了,才会考虑下一辆。
    2.解决的方案(如上图b)所示)
    3.结论:队列的这种头尾相接的顺序存储结构称为循环队列
    设存储循环队列的数组长度为QueueSize,则循环队列的长度为(rear-front+QueueSize) % QueueSize。
    4.这时又会产生什么新问题?(如下图所示)队空和队满的判定问题,如何解决?
    在这里插入图片描述
  2. 循环队列的实现
const int QueueSize=100;  //定义存储队列元素的数组的最大长度
template <class T>    //定义模板类CirQueue
class CirQueue
{
public:
    CirQueue( ) {front=rear=0;}  //构造函数,置空队
    ~ CirQueue( ) { }           //析构函数
    void EnQueue(T x);        //将元素x入队
    T DeQueue( );             //将队头元素出队
    T GetQueue( );          //取队头元素(并不删除)
    bool Empty( ) {front==rear? return 1: return 0;}  //判断队列是否为空
private:
    T data[QueueSize];   //存放队列元素的数组
    int front, rear;    //队头和队尾指针,分别指向队头元素的前一个位置和队尾元素的位置
};

循环队列的入队算法如下:

template <class T>
void CirQueue::EnQueue(T x)
{
 if ((rear+1) % QueueSize ==front) throw "上溢";
 rear=(rear+1) % QueueSize;   //队尾指针在循环意义下加1
 data[rear]=x;                //在队尾处插入元素
}

循环队列的出队算法如下:

template <class T>
T CirQueue::DeQueue( )
{
  if (rear==front) throw "下溢"; 
  front=(front+1) % QueueSize;   //队头指针在循环意义下加1
  return data[front];             //读取并返回出队前的队头元素,注意队头指针
}

3.2.3 队列的链接存储结构及实现

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。

  1. 队列的链接存储结构——链队列
    队列的链接存储结构称为链队列。
    在这里插入图片描述
  2. 链队列的实现
    将队列的抽象数据类型定义在链队列存储结构下用C++中的类实现。
template <class T>
class LinkQueue
{
public:
    LinkQueue( );     //构造函数,初始化一个空的链队列
    ~LinkQueue( );    //析构函数,释放链队列中各结点的存储空间
    void EnQueue(T x);   //将元素x入队
    T DeQueue( );       //将队头元素出队
    T GetQueue( ) {if (front!=rear) return front->next->data;}  //取链队列的队头元素
    bool Empty( ) {front==rear? return 1: return 0;}  //判断链队列是否为空
private:
    Node<T> *front, *rear;  //队头和队尾指针,分别指向头结点和终端结点
};

创建链队列(即构造函数)的算法(略)
入队算法(略)
链队列的出队算法,注意在队列长度为1时的特殊处理(略)

3.2.4 循环队列和链队列的比较

循环队列和链队列的基本操作的时间性能的比较:
对于循环队列与链队列的比较,可以从两方面来考虑,从时间上,其实它们的基本操作都是常数时间,即都为0(1]的,不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。
循环队列和链队列的空间性能的比较:
对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。
总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。

参考书目:

大话数据结构
数据结构——从概念到C++实现(第2版)
数据结构(C++版)教师用书

/********************************** 
   对应教材3.5.1节,括号匹配问题 (改)
   原代码没有利用继承,直接写
   这里改用了继承
***********************************/
#include <iostream>     
#include <string>  

using namespace std;
const int StackSize = 10;           //10是示例性的数据,根据实际问题具体定义
template <typename DataType>         //定义模板类SeqStack
class SeqStack
{
public:
    SeqStack( );                 //构造函数,初始化一个空栈
    ~SeqStack( );               //析构函数
    void Push( DataType x );      //入栈操作,将元素x入栈
    DataType Pop( );            //出栈操作,将栈顶元素弹出
    DataType GetTop( );         //取栈顶元素(并不删除)
    int Empty( );                //判断栈是否为空
private:
    DataType data[StackSize];    //存放栈元素的数组
    int top;                    //游标,栈顶指针,为栈顶元素在数组中的下标
};

template <typename DataType>
SeqStack<DataType> :: SeqStack()
{
	top = -1;
}

template <typename DataType>
SeqStack<DataType> :: ~SeqStack()
{
	
}

template <typename DataType>
int SeqStack<DataType> :: Empty()
{
	if(top == -1)
		return 1;
	else 
		return 0;
}

template <typename DataType>
DataType SeqStack<DataType> :: GetTop( )
{
	if(top == -1)
		throw "下溢异常";
	else
		return data[top];
}

template <typename DataType>
void SeqStack<DataType> :: Push(DataType x)
{
	if (top == StackSize - 1) throw "上溢";
	data[++top] = x;
}

template <typename DataType>
DataType SeqStack<DataType> :: Pop( )
{
	DataType x;
	if (top == -1) throw "下溢";
	x = data[top--];
	return x;
}

//普通类继承类模板
class Matcher: public SeqStack <string>
{
public:
	Matcher(string str);
	~Matcher(){ };
	int match();
private:
	string str;
};

Matcher :: Matcher(string str){
	this->str = str;
}

int Matcher :: match()
{

	int i, top = -1;                        /* top为字符对象S的尾指针 */
	for (i = 0; str[i] != '\0'; i++)          /* 依次对str对象的每个字符, str[i]进行处理 */
	{
		if (str[i] == ')') {                 /*当前扫描的字符是右括号*/
		  if (!this->Empty()) this->Pop();
		  else return -1;
	    }
		else if (str[i] == '(')               /*当前扫描的字符是左括号*/		     
		     this->Push(&str[i]);
	}
	if (this->Empty()) return 0;                /*栈空则括号正确匹配*/
	else return 1;
}

                              
int main( )
{
	string str;                /*定义尽可能大的字符数组以接收键盘的输入*/
	int k;                                /*k接收调用函数Match的结果*/
	cout << "请输入一个算术表达式:";      
	cin >> str;                       /*将表达式以字符串方式输入*/
	Matcher m(str);
	k = m.match( );              /*函数调用,实参为字符数组的首地址*/
	if (k == 0)
		cout << "正确匹配\n";
	else if (k == 1)
		cout << "多左括号\n";
	else
		cout << "多右括号\n";
	return 0;                       
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oldmao_2000

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值