栈
书本知识整理。
把线性表的插入和删除操作放在同一端进行就能得到栈数据结构。我们可以从相应线性表类派生出栈类,也可以直接创建基于数组和链表的栈类。直接派生的优点是减少编码量,提高程序可靠性,缺点是运行效率低。
8.1 定义和应用
栈:是特殊线性表,插入和删除只能在表的同一端进行,允许插入和删除的这一端栈顶,另一端叫栈底,是一个后进先出的数据结构。
8.2 抽象数据类型
栈的抽象数据类型给出了栈的最基础常用操作.
C++ 抽象类栈
template < class T>
class stack
{
public:
virtual ~stack () {}
virtual bool empty () const = 0;
//返回true,当且仅当栈为空
virtual int size () const = 0;
//返回栈中元素个数
virtual T & top() = 0;
//返回栈顶元素的引用
virtual void pop() = 0;
//删除栈顶元素
virtual void push( const T & theElement) = 0;
//将元素theElement压入栈顶
};
8.3 数组描述
把数组线性表的右端定义为栈顶,那么入栈和出栈就是线性表在最好情况下的插入和删除,两个操作的时间都为O(1)。
8.3.1 作为一个派生类实现
从类arrayList和stack派生类derivedArrayStack,其成员函数通过调用基类成员函数实现。
一个从类arrayList 派生的数组栈类
template <class T>
class derivedArrayStack: private arrayList<T>,public stack <T>
{
public:
derivedArrayStack(int initialCapacity = 10)
:arrayList< T > (initialCapacity) {}
bool empty () const
{ return arrayList <T>::empty(); }
int size () const
{ return arrayList <T> ::size(); }
T & top()
{//返回栈顶元素
if (arrayList <T>::empty())
throw stackEmpty();
return get(arrayList <T>::size()-1 );
}
void & pop()
{//删除栈顶元素
if (allayList <T>::empty())
throw stackEmpty();
erase(arrayList <T>:: size()-1);
}
void push(const T & theElement)
{//插入
insert(allayList <T>::size(),theElement)
}
};
时间复杂度:构造函数是O(initialCapacity),插入操作在数组长度不增加时为Θ (1),在增加时为O(stack size)。其他复杂度都是为Θ(1)。
因为get和erase遇到空栈都会抛出异常,所以在top和pop中可以删除对空栈的检查。可以用try—catch结构代替对空栈的检查。
8.3.2 类arrayStack
为了减少不必要的下标检查和往回复制,我们可以定制数组栈。每种操作的时间复杂度和derivedArrayStack的相应操作一样。
类arrayStack:
template<class T>
class arrayStack :public stack<T>
{
public:
arrayStack(int initialCapacity = 10);
~ arrayStack() { delete [] stack; }
bool empty() const { return stackTop == -1; }
int size() const { return stackTop+1; }
T & top();
void pop();
void push(const T& theElement);
private:
int stackTop; //当前栈顶的索引值
int arrayLength; //栈容量
T * stack; //元素数组
};
构造函数:
template < class T >
arrayStack<T> :: arrayStack(int initialCapacity = 10)
{ // 构造函数
if (initialCapacity < 1) //输出错误信息,抛出异常
{
ostringstream s;
s<<"initialCapacity ="<< initialCapacity <<"Must be >0";
throw illegalParameterValue(s.str());
}
arrayLength = initialCapacity;//初始化
stack = new T[arrayLength];
stackTop = -1;//!不是从0开始
}
得到栈顶元素:
template < class T >
T & arrayStack<T> :: top ()
{
if (stackTop == -1)//当栈为空时
throw StackEmpty();
return stack[ stackTop ];
}
删除栈顶元素:
template<class T>
void arrayStack<T> :: pop()
{
if (stackTop == -1)
throw StackEmpty();
stack [stackTop--].~T();
// !调用T的析构函数,而不是栈的析构函数,然后stackTop-1
}
在栈顶插入:
template<class T>
void arrayStack<T>:: push(const T & theElement)
{
//如果栈已满,则容量加倍.
if (stackTop == arrayLength-1)
{
changeLength1D(stack, arrayLength, 2*arrayLength);
arrayLength*=2;
}
//在栈顶插入元素
stack[++stackTop] = theElement;
}
8.4 链表描述
用链表描述,如果链表右端作为栈顶,每次top、pop、push操作都需要从左往右找到最后一个节点,时间复杂度为O(size()),如果把链表左端作为栈顶,则每次top、pop、push操作的时间复杂度为Θ(1),所以我们选择链表左端作为栈顶。
8.4.1 类derivedLinkyStack
①将类derivedArrayStack中的private arrayList替换为private chain,②用名称derivedLinkyStack替代derivedArrayStack,③insert和erase的调用中索引实参改为0 。
8.4.2 类linkStack
为提高性能,我们可以定制链表栈。
定制链表栈:
template<class T>
class LinkedStack :public stack<T>
{
public:
LinkedStack(int initialCapacity = 10)
{stackTop=NULL; stackSize=0;}
~ LinkedStack();
bool empty() const { return stackSize == 0; }
int size() const { return stackSize; }
T & top();
void pop();
void push(const T & theElement);
private:
chainNode<T>* stackTop; //栈顶指针
int stackSize; //栈中元素个数
};
析构函数:
template <class T>
LinkedStack<T>::~LinkedStack()
{//删除栈中所有节点
while(stackTop!=NULL)
{//删除栈顶节点
chainNode <T> * nextNode = stackTop->next;
delete stackNode;
stackNode = nextNode;
}
}
时间复杂度:O(sackSize)
得到栈顶元素:
template <class T>
T & LinkedStack<T> ::top()
{
if(stackSize == 0)
throw stackEmpty();
return stackTop->Element;
}
时间复杂度:Θ(1)
删除栈顶节点:
template <class T>
void LinkedStack<T> ::pop()
{
if(stackSize == 0)
throw stackEmpty();
chainNode <T> * nextNode = stackTop->next;
delete stackTop;
stackTop = nextNode;
stackSize--;
}
时间复杂度:Θ(1)
在栈顶插入:
template <class T>
void LinkedStack<T> ::push(const T & theElement)
{
stackTop = new chainNode (theElement,stackTop);
stackSize++;
}
时间复杂度:Θ(1)
8.5应用
某些应用满足LIFO方式,可以用栈数据结构解决。
8.5.1 括号匹配
目的:输入一个字符串,输出匹配的括号和不匹配的括号
思路:从左到右扫描字符串,扫描到左括号保存到栈中,每当扫描一个右括号,就将它与栈顶的左括号(如果存在)相匹配,并将匹配到的左括号从栈顶删除。
void PrintMatchedPairs (string expr)
{// 括号匹配
arrayStack<int> s;
int length = (int) expr.size(); //也可以用strlen(expr)
// 扫描表达式expr ,寻找( 、)
for ( int i = 0; i<length; i++)
{
if ( expr.at(i) == ' ( ' ) //at是STL中的函数
s.push(i); //保存的是索引值
else if (expr.at(i) ==' ) ' )
try
{ //从栈中删除匹配的左括号,输出匹配括号的索引值
cout << s.top() <<' ' << i << endl;
s.pop();
}
catch (stackEmpty)
{ //栈空,没有匹配的左括号
cout<<"No match for right parenthesis"<<" at "<<i<<endl;
}
}
//右括号匹配结束后,栈不为空,栈中所剩下的左括号都是未匹配的
while( !s.empty() )
{
cout << "No match for left parenthesis at " << s.top() < endl;
s.pop();
}
}
时间复杂度:O(n),n为字符串长度
8.5.2 汉诺塔
目的:假设有n个碟子和三座塔。初始时所有碟子从小到大堆在塔1上,我们要把碟子都移动到塔2,每次移动一个,而且任何时候都不能把大碟子压在小碟子上。
思路:利用递归,先把塔1上面n-1个碟子移到塔3,再把最大的碟子从塔1移到塔2,最后把n-1个碟子从塔3移到塔2。
第一种实现方法:输出碟子从塔1到塔2的移动次序
void towersOfHanoi ( int n, int x, int y, int z )
{//把n 个碟子从塔x 移动到塔y,可借助于塔z
if (n > 0)
{
towersOfHanoi(n-1, x,z,y); //把塔x上面n-1个碟子移到塔z
cout << "Move top disk from tower " << x
<<" to top of tower " << y << endl;
//把最大的碟子从塔x移到塔y
towersOfHanoi(n-l, z, y, x);
//把n-1个碟子从塔z移到塔y
}
}
时间复杂度:Θ(2n)
第二中实现方法:要显示每次移动后三座塔的布局,所以要在内存中保存这些布局。因为碟子的移动是按照LIFO方式进行的,所以可以将每座塔表示为一个栈,因为数组栈运行速度比链表栈快,所以我们采用数组栈。
//全局变量,tower[1:3]表示三个塔
arrayStack<int> tower[4];
void moveAndShow ( int n, int x, int y, int z);//函数声明
void towersOfHanoi(int n)
{ // 函数moveAndShow的预处理程序
for (int d = n; d > 0; d--) // 初始化
tower[1].push(d); // 把碟子d从大到小放到塔1上
//把塔1上的n个碟子移动到塔2上,借助于塔3的帮助
moveAndShow(n, 1, 2, 3);
}
void moveAndShow(int n, int x, int y, int z)
{ // 把n 个碟子从塔x 移动到塔y,可借助于塔z
if (n > 0)
{
moveAndShow(n-l, x, z, y);//把塔x上面n-1个碟子移到塔z
int d= tower[x].top(); // 碟子编号
tower[x].pop(); //从x中移走一个碟子
tower[y].push(d); //把这个碟子放到y 上
showState(); //显示塔的布局
moveAndShow(n-l, z, y, x);
}//把n-1个碟子从塔z移到塔y
}
时间复杂度:Θ(2n)
8.5.3 列车车厢重排
目的:借助缓冲轨道,将入轨道上非有序车厢,按照车厢号递增方式移到出轨道。
思路:从前至后检查入轨道上的车厢,如果正在检查的车厢是满足排列要求的下一节车厢,直接把它移到出轨道(此时缓冲轨道上可能也有满足条件的车厢,将其移到出轨道),否则将它移到缓冲轨道。
对缓冲轨道选择的基本要求是:编号为u的车厢应该直接进入的缓冲轨道其顶部车厢是大于u的最小者,即最小上限。因为缓冲轨道符合LIFO方式,所以可以数组栈表示缓冲轨道。
函数railroad:
//全局变量
arrayStack<int> * track; //缓冲轨道数组,数组类型是数组栈
int numberOfCars; //车箱数
int numberOfTracks; //缓冲轨道数
int smallestCar; //在缓冲轨道中编号最小的车厢
int itsTrack; //停靠最小编号车厢的缓冲轨道
bool railRoad ( int inputOrder[], int theNumberOfCars, int theNumberOfTracks )
{//从初始顺序开始重排车厢
numberOfCars = theNumberOfCars;
numberOfTracks = theNumberOfTracks;
//创建用于缓冲轨道的栈
track = new arrayStack <int> [numberOfTrack+1];
int nextCarToOutput=1; //当前能进入出轨道的车厢
smallestCar = numberOfCars +1; //初始化,取一个比最大车厢号大的值
//重排车厢
for( int i =1; i<=numberOfCars; i++)
{
if( inputOrder[i] == nextCarToOutput)
{//满足排列要求的下一节车厢,直接移到出轨道
cout<<"Move car"<<inputOrder[i]
<< "from input track to output track"<<endl;
nextCarToOutput++;
while(smallestCar == nextCarToOutput)
{//缓冲轨道上满足出轨条件的车厢,将其移到出轨道
outputFromHoldingTrack();
nextCarToOutput++;
}
}
else
//将车厢inputOrder[i]移到一个缓冲轨道
if(!putInHoldingTrack(inputOrder[i]))
return false;
}
return true;
}
时间复杂度:O(numberOfTracks*numberOfCars)
函数outputFromHoldingTrack:
void outputFromHoldingTrack()
{//把编号最小的车厢从缓冲轨道移到出轨道
track[itsTrack].pop(); //删除编号最小车厢
cout <<"Move car"<<smallestCar<<"from holding track"
<<itsTrack <<"to output track"<<endl;
//检查所有栈的栈顶,寻找编号最小的车厢和它所属的栈
smallestCar = numberOfCar + 2;
//因为不能确定编号最小的车厢编号,所以取一个较大值,方便寻找
for( int i=1; i< = numberOfTracks; i++)
if( !track[i].empty() && ( track[i].top() < smallestCar) )
{
smallestCar = track[i].top();
itsTrack = i;
}
}
时间复杂度:O(numberOfTracks)
函数putInHoldingTrack:
bool putInHoldingTrack(int c)
{//将车厢c移到一个缓冲轨道。返回false,当且仅当没有可用的缓冲轨道
//为车厢c寻找最合适的缓冲轨道
//初始化
int bestTrack = 0; //最适合的轨道
bestTop = numberOfCars + 1;
//因为不能确定编号最适合的轨道,所以最适合轨道的顶部车厢号取一个较大值,方便寻找
//扫描缓冲轨道
for (int i =1; i <=numberOfTracks; i++)
if(!track[i].empty())
{//缓冲轨道不为空
int topCar = track[i].top();
if( c<topCar && topCar < bestCar)
{//缓冲轨道i具有编号更小的车厢,同时满足比c编号大,且又是比c编号大的车厢号中最小的
bestCar = topCar;
bestTrack = i;
}
}
//缓冲轨道为空,必须先考虑有车厢的轨道,因为空轨道要留给比所有轨道顶部车厢号都大的车厢
else if(bestTrack = 0) bestTrack = i;
//没有满足条件的缓冲轨道
if(bestTrack == 0) return false;
//把车厢c移到轨道bestTrack
track[bestTrack].push(c);
cout << "Move car " <<c<< "from input track "
<< "to holding track "<< bestTrack <<endl;
//如果需要更新smallestCar 和itsTrack
if( c < smallestCar )
{
smallestCar = c;
itsTrack = bestTrack;
}
return true;
}
时间复杂度:O(numberOfTracks)
8.5.6 迷宫老鼠
目的:寻找一条从入口到出口的路径。
思路:a.用 n×m 的矩阵来描述迷宫,在位置(i,j)处有一个障碍物时其值为1,否则其值为0。
b.为了避免处理内部位置和边界位置时存在差别,在迷宫的周围增加一圈障碍物。
c.如果当前位置不是迷宫出口,则在当前位置上放置障碍物,以便阻止搜索过程又绕
回到这个位置。
d.用系统的方式来确定从当前位置要向哪一个相邻位置移动。
e.检查相邻的位置中是否有空闲的,如果有就移动到这个新的相邻位置上,然后从这
个位置开始搜索通往出口的路径。如果不成功,选择另一个相邻的空闲位置,并从
它开始搜索通往出口的路径。如果相邻的位置中没有空闲的,则回退到上一位置。
f.为了方便移动,在进入新的相邻位置之前,把当前位置保存在一个堆栈中。在堆栈
中始终包含从入口到当前位置的路径。
g.如果所有相邻的空闲位置都已经被探索过,并且未能找到路径,则表明在迷宫中不
存在从入口到出口的路径。
bool findPath()
{//寻找一条从入口(1,1)到出口(n,n)的路径
//如果找到,返回true,否则返回false
//动态建立栈用来保存路径,position类中有数据成员col,row
path = new arrayStack <position>;
//初始化偏移量
position offset[4];
offset[0].row = 0; offset[0].col = 1; //向右
offset[1].row = 1; offset[1].col = 0; //向下
offset[2].row = 0; offset[1].col = -1; //向左
offset[1].row = -1; offset[1].col = 0; //向上
//初始化迷宫外围的障碍墙
for( int i = 0; i<= size +1; i++)
{
maze[0][[i]] = maze[size+1][i] = 1; //底部和顶部
maze[i][0] = maze[i][sie+1] = 1; //左和右
}
position here; //当前位置
here.row = 1; //在起点
here.col = 1;
maze [1][1] = 1; //在起点放上障碍物,防止再次回到起点
int option = 0; //下一步要往哪个方向走,0~3分别对应右下左上
int lastOption = 3;
//寻找一条路径
while( here.row != size || here.col != size)
{//还没有到达出口
int r,c; //临时row,col
while(option <= lastOption)
{//查看往哪个方向走没有障碍物
r = here.row + offset[option].row;
c = here.col + offset[option].col;
if( maze[r][c] == 0 ) break;
//有相邻位置没有障碍物,就到这个位置,跳出循环
option++;
}
if( option <= lastOption)
{//有相邻位置可走
path->push(here);//把当前位置放入栈
here.row = r; //移到可行的相邻位置
here.col = c;
maze[r][c] = 1; //放上障碍物,防止重复访问
option = 0; //!为寻找下一个位置准备
}
else
{//如果没有相邻的一步可走,返回上一步
if(path->empty())
return false;
//链表为空,无路可返,即不存在这样一条路径,结束循环
position next = path->top();
path->pop();
//返回到上一步,要把上一步从栈中删了,直到确定它有可行的下一步再放入栈
if(next.row == here.row)
option = 2 + next.col - here.col;
//上一步和当前位置行数相同,返回到上一步后可能要向下走或者向上走
else option = 3 + next.row - here.row;
//上一步和当前位置列数相同,返回到上一步后只能要向左走,
//否则只能返回到上上步,如果存在上上步的话
//因为按照移动规则,第一次进入下一步,一定是先考虑向右走。
here = next;
}
}
return true;
}
时间复杂度:O(size2) = O(m2)