基本介绍
堆栈数据结构是通过对线性表的插入和删除操作进行限制得到的,因此也是一种特殊的线性表,也就可以通过对线性表类的派生来产生堆栈类,虽然这种方法比较简便但是还是会引起一定的效率损失,同时堆栈类本身就是一种基本的数据结构,所以一般的程序设计也会单独给出基于公式描述和基于链表结构的堆栈类(而非从其他类派生过来),效率上也会有明显的提升。
定义
堆栈(stack)是一种线性表,其插入和删除操作都是在表的同一端进行。其中一端称为栈顶(top),另一端称为栈底(bottom),堆栈是一个后进先出表。
抽象数据类型 Stack{
实例 元素线性表,栈底,栈顶
操作
Create() :创建一个空的堆栈
Isempty():如果堆栈为空,则返回true ,否则返回false
Isfull(): 如果堆栈满,则返回true,否则返回false
Top():返回栈顶元素
Add(x):向堆栈中添加元素x
Delete(x):删除栈顶元素,并将它传递给x
}
访问权限 | public | protected | private |
---|---|---|---|
对本类 | 可见 | 可见 | 可见 |
对子(派生)类 | 可见 | 可见 | 不可见 |
对外部 | 可见 | 不可见 | 不可见 |
公式化描述
LinearList类派生
template<class T>
class Stack :: private LinearList <T>{ // LIFO 对象
public:
Stack(int MaxStackSize = 10):LinearList<T>(MaxStackSize){ }
bool IsEmpty() const
{return LinearList<T>::IsEmpty();}
bool IsFull() const
{return (Length() == GetMaxSize());}
T Top() const
{if (IsEmpty()) throw OutOfBounds();
T x; Find(Length(), x); return x;}
Stack<T>& Add(const T& x)
{Insert(Length(), x); return *this;}
Stack<T>& Delete(T& x)};
{LinearList<T>::Delete(L length(), x); return *this;}
stack 效率
通过从 LinearList 派生 Stack,一方面可以大大减少代码量,另一方面也使程序的可靠性得到了保证。但是同时也带来运行效率的损失,例如向堆栈添加一个元素,首先要确定堆栈的长度 Length(),然后调用 insert()。insert() 函数首先会判断插入操作会不会引起越界,然后需要使用一个 for 循环的开销来执行 0 个元素的移动。因此我们可以将 Stack 定义为基类,而不是派生类。
自定义Stack
template<class T> class Stack{
// LIFO 对象
public:
Stack(int MaxStackSize = 10);
~Stack () {delete [] stack;}
bool IsEmpty() const
{return top == -1;}
bool IsFull() const
{return top == MaxTop;}
T Top() const;
Stack<T>& Add(const T& x);
Stack<T>& Delete(T& x);
private:
int top; // 栈顶
int MaxTop; // 最大的栈顶值
T *stack; // 堆栈元素数组 };
template<class T> Stack<T>::Stack(int MaxStackSize) {// Stack 类构造函数
MaxTop = MaxStackSize - 1 ;
stack = new T[MaxStackSize];
top = -1;
}
template<class T>T Stack<T>::Top() const {// 返回栈顶元素
if (IsEmpty())
throw OutOfBounds();
else
return stack[top];
}
template<class T> Stack<T>& Stack<T>::Add(const T& x) { / / 添加元素 x
if (IsFull()) throw NoMem();
stack[++top] = x;
return *this;
}
template<class T> Stack<T>& Stack<T>::Delete(T& x) {// 删除栈顶元素,并将其送入x
if (IsEmpty()) throw OutOfBounds(); x = stack[top--];
return *this;
}
堆的应用:
- 平衡符号
做一个空栈。读入自负直到文件尾部。如果字符是一个开放符号,则将其压入栈中。如果字符是一个封闭符号,那么若栈为空则报错;若栈不为空,则将栈顶的元素弹出。如果弹出的符号不是对应的开放符号,则报错。在文件尾,如果栈非空则报错。 - 后缀表达式
后缀记法又叫逆波兰记法,每当遇到一个操作符,就作用于从该栈弹出的两个数(符号)上,再将结果压入栈中。计算一个后缀表达式花费的时间为O(N)。 - 中缀到后缀的转换
当读到一个操作数时,立即将其放到输出中。操作符不立即输出,从而必须先存在某个地方。正确的做法是将操作符放到栈中而不是直接输出,当遇到”(“我们也要将其入栈。计算是从一个初始化的空的栈开始的。如果遇见一个”)”,那么我们就将栈元素弹出,将符号输出直到遇到一个对应的”(“,但是这个”(“只被弹出并不输出。如果见到其他任何符号 “+”,”*”,”(“,”/”,那么从栈中弹出栈元素直到发现优先级更低的元素为止。对”)”处理除外。
当遇到一个操作符时,把它放入栈中。栈代表挂起的操作符。然而,当栈中那些具有高优先级的操作符完成使用时,就不需要在被挂起而是应该被弹起。 - 函数调用
当调用一个新函数时,主调例程的所有局部变量需要由系统存储起来,否则被调用的函数将会重写由主调例程的变量所使用的内存。不仅如此,该主调例程的当前的位置也要被存储,以便在新函数运行完后直到往哪里转移。这些变量一般由编译器指派给机器的寄存器,但该问题类似于平衡符号问题的原因在于,函数调用和函数的返回基本上类似于开括号和闭括号。
在存储函数调用的时候,需要存储所有重要的信息,诸如寄存器的值和返回地址等,都要一抽象的方式存在“一张纸上“并被置于一个堆的顶部。然后控制转移到新函数,该函数自由地用它的值替代这些寄存器。如果它又进行其他的函数调用,那么也遵循相同的过程。当该函数要返回时,它查看堆顶部的那张“纸“并复原所有的寄存器。然后它进行返回转移。
但是由于有一些语言和系统没有栈溢出的检测,因此程序可能因为栈空间用尽而崩溃,例如失控递归(缺少递归终止条件)。