7.1 栈ADT
栈是数据项序列,只能在序列的一端访问这些数据项。push()操作在栈顶增加一项。pop操作删除最后添加到栈上的项,我们称栈有LIFO(后进先出)顺序。
push和pop操作改变了栈。有时,需要在不删除最上面的元素的情况下访问最上面的元素。为了达到这个目的,ADT给了操作top();只有这个操作可以对栈中的元素进行访问,因为只有最上面的元素是可见的。
栈的抽象定义允许任意大的序列。因此,push操作没有前提条件。但是pop和top不同,这两个操作有前提条件,因为除非栈至少有一个元素(非空),才能成功的访问和删除其最上面的元素。操作empty()指示栈是否有元素,操作size返回栈中的元素个数。
为了在应用程序中使用栈ADT,STL提供了stack类。以下是这个类的API。其中包含一个声明空栈的构造函数。
stack类 构造函数
stack() ;生成一个空栈
stack类 操作
bool empty()const;
void pop()
void push(const T&item)
int size() const ;
T& top();
const T& top() const ;
利用top()可以返回栈最上面项的引用,使程序员能够更改最上面项的值。
有限的栈::在这种情形下,push操作有一个前提条件:当栈满了时,阻止新项入栈。对于这样的应用,我们需要一个新的ADT,在其中加入一个maxStackSize属性。栈的最大尺寸可通过构造函数或setMaxSize()设定。getMaxSize()操作允许应用程序确定当前对栈大小的限制。布尔函数full()指明栈是否为满。
7.1.1 多进制输出
string multibaseOutput(int num , int base)
{
string digitChar = "0123456789ABCDEF", numStr = "";
stack stk;
do
{
stk.push(digitChar[num % base]);
num /=base ;
} while (num != 0);
while(!stk.empty())
{
numStr += stk.top();
stk.pop();
}
return numStr ;
}
7.1.2 分解栈元素
开发一种算法:uncouple(),她使用辅助栈查找和从当前栈中删除第一次出现的元素target。重复的删除栈顶的元素,并将其推入到辅助栈中,直到找到target。使target从原来的栈中出栈,然后按次序将辅助栈中的每个元素推入到原来的栈。如果发生了分解操作,则返回TRUE,否则,返回 FALSE。
template
bool uncouple(stack
& s , const T& target)
{
stack
tmpStk;
bool foundTarget = true;
while(! s.empty() && s.top() != target)
{
tmpStk.push(s.top());
s.pop();
}
if (! s.empty())
{
s.pop(); //delete the target
}
else
{
foundTarget = false ;
}
while(! tmpStk.empty())
{
s.push(tmpStk.top());
tmpStk.pop();
}
return foundTarget ;
}
注意::pop返回类型:为什么pop()返回void,而不是类型T呢?如果pop()返回栈顶部的元素,则必须按值返回,而不是引用返回。按引用返回是不可行的,因为元素在栈中已经不存在了,必须在按引用返回她之前先将其存储到某个地方。如果选用动态内存,除非动态内存最终被删除,否则将导致内存泄露。按照数值返回效率很差,因为他包含对类型T的复制构造函数的调用。让pop()返回数值将会导致潜在的内存问题或效率低下,因此,最好让她什么数值也不返回,而是通过使用top()来得到栈顶的数值。
7.2 递归代码和运行栈
在函数调用时,运行时系统将活动记录推入到系统提供的栈中,称为运行栈。然后将控制权传递给函数中的语句,在这里,记录中的数据可以被函数体使用。在推出函数时,运行时系统从活动记录中提取返回地址,然后把记录出栈。
7.3 栈的实现
我们选择向量作为基本的存储结构,把向量尾(back)的概念与栈顶(top)的概念联系起来。我们使用对象组合,将向量对象声明为私有数据成员。用向量操作实现stack类。栈没有迭代器,因为程序员使用迭代器的目的是在容器中的任意位置上进行一般性的访问和更新操作。
7.3.2 STL stack类的实现
STL 允许程序员通过声明带两个模板参数的stack类来选择底层存储容器。第1个参数T表示栈中元素类型。第2个参数的名字为容器,表示由程序员指定的存储结构类型。STL通过使用在向量,表和双端队列类中均可使用的size(),push_back()等操作来实现stack类。默认情况下,选择的是双端队列容器,这就是在声明STL栈对象时只需要指明对象类型的原因。
template
>
//template
>
class stack {
public:
typedef Cont::allocator_type allocator_type;
typedef Cont::value_type value_type;
typedef Cont::size_type size_type;
explicit stack(const allocator_type& al = allocator_type()) const;
bool empty() const;
size_type size() const;
allocator_type get_allocator() const;
value_type& top();
const value_type& top() const;
void push(const value_type& x);
void pop();
protected:
Cont c;
};
适配器类是容器,她使用某类提供的接口来实现另一个类所需的接口。STL栈容器是一个适配器类,因为他用向量,双端队列或表实现栈的行为。
选择栈容器::栈的每个底层存储结构都有不同的设计,会影响到其性能。因此,程序员应该选择最适合应用程序的容器。一般情况下,对于相对小的栈来说,向量是最有效的。当需要经常改变大小来调节栈的大小时,双端队列是较好的选择。表容器的特定的设计允许在任意位置进行一般性的插入和删除,因为栈不需要这个特性,所以,几乎没有理由使用表存储栈的元素。
7.4 后缀表达式
运算符出现在其运算数之后。这个格式也称为RPN(逆波兰表示法)。我们可以用两条规则总结从中缀到后缀的转换:
规则1:从左到右扫描中缀表达式。在表达式中找到一个运算数的同时记录下来。
规则2: 找到运算符的运算数的同时记录下运算符。
7.4.1 后缀计算
计算后缀表达式的算法从左到右扫描表达式的每一项,用一个单个的栈装载运算数。如果一项是运算数,将其推入栈。如果此项是二元运算符,则可以计算其结果,因为他的两个运算数已经在栈的最上面的两个位置了。为了实现运算符的计算,我们出栈两次得到运算数,计算表达式,然后把结果推入栈中。表达式中的所有项被执行一遍后,只有一个数值位于栈顶,这个数值就是结果。
7.5 中缀表达式计算
表达式的等级:我们把中缀表达式算法限制在仅包含二元运算符的表达式中。 此算法使用等级的概念计算中缀表达式,等级的概念是给表达式中每一项赋一个值(-1 0 或 1 ):运算数为1,二元运算符等级为 -1 ,右左圆括号为0 ;累计等级必须在0 – 1 之间,正确的表达式累计值为 1 。
读取运算数,立即把她写到后缀表达式中,当扫描运算符时,先删除栈中与当前运算符优先级相同或较高的所有运算符,并把他们写到或追字符串中。
当运算符是右结合时,给他赋两个不同的优先级数值,输入优先级和栈优先级。当运算符位于栈中时,应用栈优先级。当我们读取一个运算符时,比较此运算符的输入优先级和栈顶部运算符的栈优先级。如果输入优先级低于或等于栈顶运算符的栈优先级,将运算符出栈并写入后缀字符串中。
当读取圆括号时,就进入了子表达式。我们把左园括号存储在栈中,直到遇到子表达式的结尾,把左括号和她上面的全部运算符输出到表达式中。
输入和栈优先级等级
符号 输入优先级 栈优先级 Rank
+ - 1 1 -1
* / % 2 2 -1
^ 4 3 -1
( 5 -1 0
) 0 0 0
中缀表达式计算的规则::
规则1:在每个符号后面检查累计等级,其值必须在范围 0~1 。如果值为负,说明表达式运算符太多。如果值为2 , 则说明表达式有太多的运算数。累计等级的最终值必须是 1 。
规则2 : 如果输入是运算数,则立即把他写入后缀字符串中。
规则3:遇到运算符和左圆括号的输入,比较其输入优先级和栈中最上面的栈优先级。如果栈优先级高于或等于输入优先级,运算符出栈,并写入后缀字符串中。继续这个过程,直到栈优先级不再高于或等于输入优先级,然后把输入符号入栈。
规则4:如果输入是右圆括号,则说明扫描到了子表达式的结束。把栈中相应的左圆括号后面的所有运算符出栈,写入到后缀字符串中。然后出栈左圆括号,继续扫描过程。
规则5:到达中缀表达式的结尾后,出栈所有剩余的运算符,写入后缀字符串中。