您的位置:首页 > 其它

栈的抽象数据类型(abstract data type,ADT)

2016-09-03 12:26 519 查看

栈(stack)

  栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶(top)。对栈的基本操作有push(进栈)和pop(出栈),前者相当于插入,后者则是删除最后插入的元素。最后插入的元素可以通过使用top例程在执行pop之前进行考察。对空栈进行的pop或top操作一般被认为是栈ADT中的一个错误。另一方面,当运行push时空间用尽是一个实现限制,但不是ADT错误。

  栈有时又叫做LIFO(后进先出)表。一般的模型是,存在某个元素位于栈顶,而该元素是唯一的可见元素。





栈模型:只有栈顶元素是可访问的。

栈的实现

  由于栈是一个表,因此任何实现表的方法都可以实现栈。栈操作是常数时间操作。

栈的链表实现

  栈的第一种实现方法是使用单链表。通过在表的顶端插入来实现push,通过删除表顶端元素来实现pop。top操作只是考察表顶端元素并返回它的值。有时pop操作和top操作合二为一。

栈的数组实现

  模仿ArrayList的add操作。与每个栈相关联的操作是theArray和topOfStack,对于空栈它是-1(这就是空栈初始化的做法)。为将某个元素x推入栈中,我们使topOfStack增1然后置theArray[topOfStack]=x。为了弹出栈元素,我们置返回值为theArray[topOfStack]然后使topOfStack减-1。

  注意,这些操作不仅以常数时间运行,而且是以非常快的常数时间运行。

应用

平衡符号

  例如:{}、[]符号等,有一个“{”符号就需要一个“}”符号与之对应。这个简单的算法用到一个栈,叙述如下:

  做一个空栈。读入字符直到文件结尾。如果字符是一个开放符号,则将其推入栈中。如果字符是一个封闭符号,则当栈空时报错。否则,将栈元素弹出。如果弹出的符号不是对应的开放符号,则报错。在文件结尾,如果栈非空则报错。很清楚,它是线性的,事实上它只需对输入进行一趟检验。因此,它是联机(on-line)的,是相当快的。当报错时决定如何处理需要做一些附加的工作--例如判断可能的原因。

后缀表达式

  后缀(postfix)记法或逆波兰(reverse Polish)记法。计算一个后缀表达式花费的时间是o(N)。注意,当一个表达式以后缀记号给出时,没有必要知道任何优先的规则,这是一个明显的优点。

中缀到后缀的转换

  标准形式的表达式(或叫做中缀表达式(infix))转换成后缀表达式。

  当读到一个操作数的时候,立即把它放到输出中。操作符不立即输出,从而必须先存放某个地方。正确的做法是将已经见到过但尚未放到输出中的操作符推入栈中。当遇到左圆括号时我们也要将其推入栈中。计算从一个空栈开始。

  如果见到一个一个右括号,那么就将栈元素弹出,将弹出的符号写出直到遇到一个(对应的)左括号,但是这个左括号只被弹出并不输出。

  如果我们见到任何其它的符号(+,*,(),那么我们从栈中弹出栈元素直到发现优先级更低的元素为止。有一个例外:除非是在处理一个)的时候,否则我们绝不从栈中移走(。对于这种操作,+的优先级最低,而(的优先级最高。当从栈弹出元素的工作完成后,我们再将操作符压人栈中。

  最后,如果读到输入的末尾,我们将栈元素弹出直到该栈变成空栈,将符号写到输出中。

  这个算法的想法是,当看到一个操作符的时候,把它放到栈中。栈代表挂起的操作符。然而,栈中有些具有高优先级的操作符现在知道当它们不再被挂起时要完成使用,应该被弹出。这样,在把当前操作符放入栈中之前,那些在栈中并在当前操作符之前要完成使用的操作符被弹出。

  当左括号是一个输入符号时我们可以把它看成是一个高优先级的操作符(使得挂起的操作符仍是挂起的),而当它在栈中时把它看成是低优先级的操作符(从而不会被操作符意外地删除)。右括号被处理成特殊的情况。

  

例子:中缀表达式:a + b * c + ( d * e + f ) * g 转成后缀表达式:abc * + de * f + g * +



方法调用

  当调用一个新方法时,主调例程的所有局部变量需要由系统存储起来,否则被调用的新方法将会重写由主调例程的变量所使用的内存。不仅如此,该主调例程的当前位置也必须要存储,以便在新方法运行完后知道向哪里转移。这些变量一般由编译器指派给机器的寄存器,但存在某些冲突(通常所有的方法都是获取指定给1号寄存器的某些变量),特别是涉及到递归的时候。该问题类似于平衡符号的原因在于,方法调用和方法返回基本上类似于开括号和闭括号。

 当存在方法调用的时候,需要存储的所有重要信息,诸如寄存器的值(对应变量的名字)和返回地址 (它可从程序计数器得到,一般情况是在一个寄存器中)等, 都要以抽象的方法存在“一张纸上”并被置于一个堆(pile)的顶部。然后控制转移到新方法,该方法自由地用它的一些值替代这些寄存器。如果它又进行其它的方法调用,那么它也遵循相同的过 程。当该方法要返回时,它查看堆顶部的那张“纸”并复原所有的寄存器,然后进行返回转移。

  显然,所有全部工作均可由一个栈来完成,而这正是在实现递归的每一种程序设计语言中实际发生的事实。所储存的信息或称为活动记录(activation record),或叫做帧栈(stack frame)。

  在实际计算机中的栈常常是从内存分区的高端向下增长,而在许多非Java系统中是不检测溢出的。失控递归可能导致栈溢出。

  尾递归(tail recursion):涉及在最后一行的递归调用。尾递归可以通过将代码放到一个while循环中并用每个方法参数的一次赋值代替递归调用而被手工清除。

  递归总是能够被彻底去除(编译器是在转变成汇编语言时完成递归去除的),但是这么做是相当乏味冗长的。一般方法是要求使用一个栈,而且仅当你能够把最低限度的最小值放到栈上时这个方法才值得一用。

  

 

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: