1 栈的定义
栈(stack,zhan):把线性表的插入和删除操作限制在同一端进行,就得到栈数据结构。
把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈是一个线性表,栈元素具有线性关系,即前驱后继关系。表尾即栈顶。特殊之处限制了线性表的插入和删除位置,所以栈底是固定的,最先进栈的只能在栈底。
栈的插入(push)操作,叫作进栈,也称压栈、入栈;栈的删除(pop)操作,叫作出栈。
应用:递归函数力就用到了栈。
2 栈的实现
栈的抽象数据类型如下。
ADT stack
{
实例
线性表;一端称为底,另一端称为顶
操作
empty(); //栈为空时返回true,否则返回false
size(); //返回栈中元素个数
top(); //返回栈顶元素
pop(); //删除栈顶元素
push(x) //将元素x压入栈顶
}
C++抽象类栈如下。
template<class T>
class stack
{
public:
virtual ~stack {}
virtual bool empty() const = 0;
virtual int size() const = 0;
virtual T& top() = 0;
virtual void pop = 0;
virtual void push(const T& theElement) = 0;
};
栈是一种特殊的线性表,所以其实现可以基于数组或者链表形式。
2.1 基于数组形式
如果把数组线性表的右端定义为栈顶,那么入栈和出栈操作对应的就是线性表在最好情况下的插入和删除操作。结果两个操作的时间都为O(1)。以下arrayList类见文章头部的链接1。
template<class T>
class derivedArrayStack : private arrayList<T>,public stack<T>
{
public:
deriverdArrayStack(int initialCapacity = 10) : arrayList<T> (initialCapacity) {}
bool empty() const
{ return arrayList<T>::empty(); }
int size() cosnt
{ return arrayList<T>::size(); }
T& top()
{
if (arrayList<T>::empty())
//抛出异常
return get(arrayList<T>::size() - 1);
}
void pop()
{
if (arrayList<T>::empty())
//抛出异常
erase(arrayList<T>::size() - 1);
}
void push(const T& theElement)
{ insert (arrayList<T>::size(), theElement); }
};
分析:
(1)stack私有继承arrayList。因此,arrayList的共有和保护性方法以及数据成员都是类derivedArrayStack可以访问的。尤其是,栈stack的用户不能访问arrayList的方法get、insert、erase,因此,LIFO原则得以在类derivedArrayStack的实例上贯彻执行。
(2)构造函数的复杂度在T是基本类型时为O(1),在T时用户定义的类型时为O(initialCapacity)。插入操作的复杂度在数组长度不增加时为O(1),在增加时为O(stack size)。其他操作的复杂度都为O(1)。
(3)这种通过线性表类派生得到一个栈类,看起来代码实现很简单,但是付出的代价时性能的损失。例如,每当我们往栈中插入一个元素时,push方法都会调用arrayList的insert方法arrayList<T>::insert。这个函数在实际插入一个新元素之前都会小标检查,可能要将数组加长,而且还要往回复制。下表检查和往回复制是不需要i的,因为我们总时把元素插入到线性表的右端。
要得到一个特性能更好的是数组栈的方法,一种途径是开发一个类,它利用数组stack来包含所有的元素。栈底元素是stack[0],栈顶元素是stack[stackTop]。
template<class T>
class arrayStack : public stack<T>
{
public:
arrayList(int initialCapacity = 10);
~arrayList() { delete[] stack; }
bool empty() const { return stackTop == -1; }
int size() const
{ return stackTop + 1; }
T& top()
{
if (stackTop == -1)
//抛出异常
return stack[stackTop];
}
void pop()
{
if (stackTop == -1)
//抛出异常
stack[topStack--].~T();
}
void push(const T& theElement);
private:
int stackTop; //当前栈顶
T* stack; //元素数组
int arrayLength;//栈容量
};
template<class T>
arrayStack<T>::arrayStack(int initialCapacity)
{//构造函数
if (initalCapacity < 1)
{
ostringstream s;
s << "Initial capacity = " << initialCapacity << " Must be > 0";
throw illegalParameterValue(s.str());
}
arrayLength = initialCapacity;
stack = new T[arrayLength];
stackTop = -1;
}
template<class T>
void arrayStack::push(const T& theElement)
{//将元素theElment压入栈
if(stackTop == arrayLength - 1)
{//空间已满,容量加倍
changeLength1D(stack, arrayLength, 2 * arrayLength);
arrayLength *= 2;
}
stack[++stackTop] = theElement;
}
性能比较: 尽管数组栈类arrayStack(AS)、derivedArrayStack(DAS)和C++STL容器类stack(STL)都实现了栈的抽象数据类型的所有方法,而且渐近时间复杂度相同,但是,每一种类的方法实际性能都不尽相同。
(1)在缺省值情况下,性能比较,AS>STL>DAS
(2)指定初始容量,AS>DAS。由于STL的栈派生于STL的类deque,因为它的构造函数不运行指定初始容量。
(3)如果一个数组的缺省长度为10,那么AS在改变数组容量时所用的时间占总时间的44%。
(4)在容量为缺省的情况下,STL>DAS,是因为deque没有索引检查,而DAS有。
2.2 基于链表形式
当用链表描述栈时,我们必须确定用链表的哪一端表示栈顶。若用链表的右端作为栈顶,则栈操作top、push和pop的实现需要调用链表方法get(size()-1)、insert(size(),theElement)和erase(size()-1)。每个链表方法需要用时O(size())。而用链表的左端作为栈顶,需要调用的链表方法是get(0)、insert(0,theElement)和erase(0),其中每一个链表方法需要用时O(1)。所以选择链表的左端作为栈顶。
下面代码为,类derivedLinkedStack从chain派生,实现抽象类stack。
template<class T>
class derivedLinkedStack : private chain<T>,public stack<T>
{
public:
derivedLinkedStack(int initialCapacity = 10) : chain<T> (initialCapacity) {}
bool empty() const
{ return chain<T>::empty(); }
int size() cosnt
{ return chain<T>::size(); }
T& top()
{
if (chain<T>::empty())
//抛出异常
return get(0);
}
void pop()
{
if (chain<T>::empty())
//抛出异常
erase(0);
}
void push(const T& theElement)
{ insert(0, theElement); }
};
其实主要就是修改了get、insert、erase方法中的索引。
下面实现类linkedStack,直接派生stack,而不是从类chain派生。
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()
{
if (stackSize == 0)
//抛出异常
return stackTop->element;
}
void pop();
void push(const T& theElement)
{
stackTop = new chainNode<T>(theElement, stackTop);
stackSize++;
}
private:
chainNode<T>* stackTop; //栈顶指针
int stackSize; //栈中元素个数
};
template<class T>
linkedStack<T>::~linkedStack()
{//析构函数
while (stackTop != NULL)
{//删除栈顶结点
chainNode<T>* nextNode = stackTop->next;
delete stackTop;
stackTop = nextNode;
}
}
template<class T>
void linkedStack<T>::pop()
{//删除栈顶结点
if (stackSize == 0)
//抛出异常
chainNode<T>* nextNode = stackTop->next;
delete stackTop;
stackTop = nextNode;
stackSize--;
}
性能分析:derivedLinkedStack和linkedStack执行时间差不多,但是比arrayStack慢的多。
3 参考文献
1 《大话数据结构》 程杰著.
2 《数据结构、算法与应用(C++语言秒杀你)》 Sartaj Sahni著.