数据结构——栈

声明:本文参考清华大学计算机系列教材《数据结构(用面向对象方法与C++语言描述)》(第2版)

一、栈的定义

1.什么是栈

栈是一种非常重要的数据结构,栈可以定义为只允许在表的末端进行插入和删除的线性表。允许插入和删除的一端叫做栈顶(top),而不允许插入和删除的一端叫做栈底(bottom)。当栈中没有任何元素时称为空栈。

2.举例演示

如下图,设定栈S=(a1,a1,…,an),则称最后加入栈的元素an为栈顶。栈中按a1,a2,…,an的顺序进栈。而退栈的顺序反过来,an先退出,然后an-1才能退出,最后退出a1。换句话说,后进者先出。因此,栈又叫做后进先出(LIFO,Last In First Out)的线性表。
栈

3.常用函数

栈在c++ STL中通过引入头文件#include <stack>来使用栈,下面我们自定义一个栈来看看栈中的常用的方法。

class Stack{		//自定义栈 
	Stack(){};		//构造函数
	virtual void Push(const T& x)=0;		//进栈 
	virtual  bool Pop(T &x)=0;		//出栈 
	virtual bool getTop(T &x)const=0;		//得到栈顶元素 
	virtual bool IsEmpty()const=0;			//判断栈是否为空 
	virtual bool IsFull()const=0;			// 判断栈是否为满 
	virtual int getSize()const=0;			//计算栈中元素个数 
};

二、栈的分类

1.顺序栈

1.1什么是顺序栈

顺序栈可以采用顺序表作为其存储方式,为此,可以在顺序栈的声明中用顺序表定义它的存储空间。本文章使用一维数组作为栈的存储空间。
存放数组元素的数组的头指针为*element,该数组的最大允许存放元素个数maxSize,当前栈顶位置由数组下标指针top指示,如果栈不空时element[0]是栈中第一个元素。

1.2顺序栈代码展示
const int stackadd=20;
template<class T>
class Seqtack{
	public:
		friend ostream& operator<<(ostream& os,SeqStack<T>& s){ //友元函数,输出 
			for(int i=s.top;i>=0;i--){
				os<<s.element[i];
			}
			return os;
		}
		SeqStack(int sz=100);				//构造函数 
		~SeqStack(){delete[] element;}		//析构函数 
		void Push(const T& x);			//在栈顶放元素 
		bool Pop(T &x);					//在栈顶删除元素 
		bool getTop(T &x);				//得到栈顶元素(不删除) 
		bool isEmpty()const{return (top==-1)?true:false;};		//判断栈是否为空 
		bool isFull()const{return (top==maxSize-1)?true:false;}; 		//判断栈是否为满 
		int getSize(){return top+1;};			//得到栈元素个数 
		void makeEmpty(){top=-1;};			//置空 
	private:
		T* element;						//定义指针动态开辟空间(用数组存储) 
		int top;						//	栈顶下标 
		int maxSize;					//最大容量 
		void overflowPocess(); 			//溢出扩容函数 
};

其中,栈的构造函数用于在建立栈的对象时为栈的数据成员赋初值。函数中动态建立栈数组的最大尺寸为maxSize,由函数参数sz给出,并令top=-1,置栈为空。
在这个函数实现中,使用了一种断言(Assert)机制,这是c++提供的一种功能,若断言语句assert参数表中给定的条件满足,则继续执行后面的语句;否则出错处理,终止程序的执行。这种断言句格式简洁,逻辑清晰,不但降低了程序的复杂度,而且提高了程序的可读性。

template<class T>
Stack<T>::Stack(int sz):top(-1),maxSize(sz){
	element=new T[maxSize];
	assert(element!=NULL);
}

对于溢出处理函数,我们先创建一个新的数组,数组空间为maxSize+stackadd,即扩容后的容量,再通过循环遍历,将原数组中的值依次赋值到新数组,释放老数组,将新数组赋值给element。

template<class T>
void Stack<T>::overflowPocess(){
	T* newArr=new T[maxSize+stackadd];
	if(newArr==NULL){
		cerr<<"内存分配失败"<<endl; 
	}
	for(int i=0;i<top;i++){
		newArr[i]=element[i];
	}
	delete[] element;
	element=newArr;
}

在入栈函数中,应先判断栈是否已满,栈的最后允许存放位置为maxSize-1,如果栈顶指针top==maxSize-1,则说明栈中所有位置均已使用,栈已满。这是若再有新元素进栈,将发生栈溢出,程序转入溢出处理。如果top<maxSize-1,则先让栈顶指针进1,指到当前可加入新元素的位置,再按栈顶指针所指的位置将新元素加入。这个新插入的元素将成为新的栈顶元素。
进栈

template<class T>
void Stack<T>::Push(const T &x){
	if(isFull()==true) overflowPocess();
	element[++top]=x;
}

在出栈函数中,如果退栈时发现是空栈,即top==-1,则退栈操作失败。若当前top>=0,则可以先将此时top指针指向的元素存储到x中,再将栈顶指针减一,等于栈顶退回到次栈顶的位置。
退栈

template<class T>
bool Stack<T>::Pop(T &x){
	if(isEmpty()==true) return false;
	x=element[top--];
	return true;
}

在得到栈顶元素函数中,如果栈是空栈,即top==-1,则操作失败。若当前top>=0,则得到此时top指针指向的元素存储到x中。

template<class T>
bool Stack<T>::getTop(T &x){
	if(isEmpty()==true) return false;
	x=element[top];
	return true;
}
1.3栈空间共享

当栈满时要发生溢出,为了避免这种情况,需要为栈设立一个足够大的空间。但如果空间设置得过大,而栈中实际只有几个元素,也是一种空间浪费。此外,空间中往往同时存在几个栈,因为各个栈中所需要得空间在运行中是动态变化着的。如果给几个栈分配同样大小的空间,可能存在实际运行时,有的栈膨胀得快,很快就产生了溢出,而其他栈可能此时还有许多空闲的空间。此时就必须调整栈的空间,防止栈的溢出。
例如,程序同样需要两个栈时,我们可以定义一个足够的栈空间。该空间的两端分别设为两个栈底,用b[0](=-1)和b[1](=maxSize)指示。让两个栈的栈顶t[0]和t[1]都向中间伸展,直到两个栈的栈顶相遇,才认为发生了溢出。
栈空间共享
注意,每次进栈时t[0]加一,t[1]减一;而退栈时,t[0]减一,t[1]加一
两栈的大小是固定不变的。在实际运算的过程中,一个栈有可能进栈元素多而体现大些,另一个则可能小些。两个栈公用一个栈空间,相互调剂,灵活性强。
在双栈的情况下,各栈的初始化语句为t[0]=b[0]=-1,t[1]=b[1]=maxSize。栈满的条件t[0]+1=t[1],即当两个栈的栈顶指针相遇才算栈满。而栈空的条件为t[0]=b[0]或t[1]=b[1],此时栈顶指针退到栈底

//在双栈中插入元素,d=0表示插入0号栈,d=1表示插入1号栈 
bool push(DualStack& DS,T x,int d){
	if(DS.t[0]+1==DS.t[1]) return false;		//栈满 
	if(d==0) DS.t[0]++;
	else DS.t[1]--;
	DS.Vector[DS.t[d]]=x;
	return true;
}

//在双栈中删除元素,d=0表示删除0号栈栈顶元素,d=1表示删除1号栈栈顶元素 
bool Pop(DualStack& DS,T x,int d){
	if(DS.t[d]==DS.b[d]) return false;		//栈空 
	x=DS.Vector[DS.t[d]];
	if(d==0) DS.t[0]--;
	else DS.t[1]++;
	return true;
}

在n(n>2)个栈的情形有所不同,采用多个栈共享栈空间的顺序存储表示方式,处理十分复杂,在插入时元素的移动量很大,因而时间代价较高。特别是当整个存储空间即将充满时,这个问题更加严重。解决的办法就是采用链接方式作为栈得存储表示。

2.链式栈

2.1什么是链式栈

链式栈是线性表的链接存储方式。采用链式栈来表示一个栈,便于结点的插入和删除。链式栈的栈顶在链表的表头。因此,新结点的插入和栈顶结点的删除都在链表的表头,即栈顶进行
链式栈

2.2链式栈代码展示
#include <bits/stdc++.h>
using namespace std;
template<class T>
struct LinkNode{			 //定义链表结点
	T data;						//结点数据域
	LinkNode<T> *link;			//结点指针域
	LinkNode(LinkNode<T> *ptr){link=ptr;}			//构造函数(参数为指针域)
	LinkNode(const T& item,LinkNode<T> *ptr=NULL){data=item;link=ptr;}	//构造函数(参数为数据与和指针域)
};
template<class T>
class LinkedStack{				//链式栈定义
	public:
		LinkedStack():top(NULL){}		//构造函数:默认链表为空(top为空)
		~LinkedStack(){makeEmpty();}		//析构函数:置空
		void Push(const T& x);				//入栈函数
		bool Pop(T &x);						//出栈函数
		bool getTop(T &x)const;				//得到栈顶元素
		bool IsEmpty()const{return(top==NULL)?true:false;}		//判断栈是否为空
		int getSize()const;					//得到栈中元素个数
		void makeEmpty();					//置空函数
		friend ostream& operator<<(ostream& os,LinkedStack<T>& s){		//友元函数:输出栈
			os<<"栈中的元素个数="<<s.getSize()<<endl;
			LinkNode<T> *p=s.top;int i=0;
			while(p!=NULL){
				os<<(++i)<<":"<<p->data<<endl;
				p=p->link;
			}
			return os;
		}
	private:
		LinkNode<T> *top;					//成员属性:top指针,指向栈顶
}; 
//入栈函数实现:创建数据域为x,指针域为当前头结点的链表结点,将top设置成新结点
template<class T>
void LinkedStack<T>::Push(const T&x){
	top=new LinkNode<T>(x,top);
	assert(top!=NULL);
}

//置空函数:循环链表中每一个结点,释放结点
template<class T>
void LinkedStack<T>::makeEmpty(){
	LinkNode<T> *p;
	while(top!=NULL){
		p=top;
		top=top->link;
		delete p;
	}
}
//出栈函数:先判断栈是否为空,如果为空,返回false,否则创建一个新结点p,指向当前top结点,将top设置成top的下一个结点,释放p结点
template<class T>
bool LinkedStack<T>::Pop(T &x){
	if(IsEmpty()==true) return false;
	LinkNode<T> *p=top;
	top=top->link;
	x=p->data;
	delete p;
	return true;
}
//得到栈顶元素:先判断栈是否为空,如果为空,返回false,否则将top的数据域赋值给x
template<class T>
bool LinkedStack<T>::getTop(T &x)const{
	if(IsEmpty()==true) return false;
	x=top->data;
	return true;
}
//得到栈中元素个数:不断遍历链表,记录结点个数
template<class T>
int LinkedStack<T>::getSize()const{
	LinkNode<T> *p=top;
	int k=0;
	while(p!=NULL){
		p=p->link;
		k++;
	}
	return k;
}
//主函数:测验链式栈类
int main(){
	LinkedStack<int> ls;
	int x;
	cout<<ls.IsEmpty()<<endl;
	for(int i=0;i<10;i++){
		ls.Push(i);
	}
	cout<<ls.getSize()<<endl;
	ls.Pop(x);
	cout<<x<<endl;
	ls.getTop(x);
	cout<<x<<endl;
	cout<<ls<<endl;
}

运行结果展示
运行结果展示
如果同时使用n个链式栈,其头指针数组可以用以下方式定义:

LinkNode<T> *s=new LinkNode<T>[n];

在多个链式栈的情形中,link域需要一些附加的空间,但其代价并不大。

三、总结

栈是允许在同一端进行插入和删除操作的特殊线性表,是一种重要的数据结构。栈的应用很广泛,一些关于栈的例题,比如:括号匹配问题、表达式计算问题、火车调度问题等,都是非常经典的算法题。以上就是小编的一些总结,最后,用一句话结尾,送给大家!

一个能思考的人,才真是一个力量无边的人。——巴尔扎克

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值