数据结构——栈
声明:本文参考清华大学计算机系列教材《数据结构(用面向对象方法与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域需要一些附加的空间,但其代价并不大。
三、总结
栈是允许在同一端进行插入和删除操作的特殊线性表,是一种重要的数据结构。栈的应用很广泛,一些关于栈的例题,比如:括号匹配问题、表达式计算问题、火车调度问题等,都是非常经典的算法题。以上就是小编的一些总结,最后,用一句话结尾,送给大家!
一个能思考的人,才真是一个力量无边的人。——巴尔扎克