zoukankan      html  css  js  c++  java
  • 由“栈的逆序”谈谈递归算法

    要求将一个栈逆序,使用递归。

     

    我们先看看最常规的解法应该是怎样的,显然对于“逆序”这种问题描述,栈这种数据结构就会蹦入我们的脑海。

     

    实现代码如下:

    1. public static LinkedStack<Integer> reverseStackDirectly(LinkedStack<Integer> stack) {  
    2.       
    3.     if(null != stack && !stack.isEmpty()) {  
    4.         LinkedStack<Integer> auxiliary = new LinkedStack<Integer>();  
    5.         while(!stack.isEmpty()) {  
    6.             auxiliary.push(stack.pop());  
    7.         }  
    8.         return auxiliary;  
    9.     }  
    10.       
    11.     return stack;  
    12. }  

    代码的思路很明确,首先开辟了一个新的栈作为辅助栈,将原栈中的元素依次弹出并压入到辅助栈中,最后返回辅助栈。

     

    当然我们的这种做法是不符合题意的,题目规定需要使用递归来解决。其实大家都明白,递归究其本质,也使用到了栈这种数据结构,只不过在递归中使用的是程序运行期间的方法调用栈,它并不由我们显式创建和管理。


    下面我们考虑递归的解法,在考虑递归解法的时候,需要铭记的一点是:发现原问题中的子问题。

    就本问题而言,子问题就是可以考虑成以下两种形式:

    1. 取出栈顶元素后,将栈进行逆序,最后将取出的栈顶元素插入到栈底
    2. 取出栈底元素后,将栈进行逆序,最后将取出的栈底元素压入到栈顶

    我们可以发现,不管使用哪一种子问题的描述,都出现了将栈进行逆序这个步骤,而这个步骤,在不考虑具体的待操作对象的时候,和我们要解决的大问题是一样的。

     

    不妨将第一种子问题的描述形式转换成代码:

    1. public static void reverseStack(LinkedStack<Integer> stack) {  
    2.       
    3.     if(null != stack && !stack.isEmpty()) {  
    4.         // 将栈顶元素取出  
    5.         Integer top = stack.pop();  
    6.         // 递归地将该栈逆序  
    7.         reverseStack(stack);  
    8.         // 将之前取出的元素插入栈底  
    9.         insertToStackBottom(stack, top);  
    10.     }  
    11. }  

    我们先不考虑如何实现最后一个步骤:将取出的元素插入栈底。

    仅仅对比一下使用递归以及非递归的方法时,程序结构上的不同点:

    • 在递归实现中,我们首先声明了一个变量来保存栈顶元素,然后递归调用本方法。
    • 在非递归实现中,我们直接拿到了栈顶元素并压入到辅助栈中。

    在声明一个方法内变量时,实际上是向方法调用栈的栈顶栈帧上添加了一个变量。这一点从代码上不明显,就Java程序而言,javap这个工具能够让你看到具体发生了什么,就上面的程序而言:

    1. public static void reverseStack(LinkedStack stack);     0  aload_0 [stack]  
    2.      1  ifnull 28  
    3.      4  aload_0 [stack]  
    4.      5  invokevirtual LinkedStack.isEmpty() : boolean [18]  
    5.      8  ifne 28  
    6.     11  aload_0 [stack]  
    7.     12  invokevirtual LinkedStack.pop() : java.lang.Object [24]  
    8.     15  checkcast java.lang.Integer [28]  
    9.     18  astore_1 [top]  
    10.     19  aload_0 [stack]  
    11.     20  invokestatic misc.ReverseStack.reverseStack(LinkedStack) : void [30]  
    12.     23  aload_0 [stack]  
    13.     24  aload_1 [top]  
    14.     25  invokestatic misc.ReverseStack.insertToStackBottom(LinkedStack, java.lang.Integer) : void [32]  
    15.     28  return  
    16.   
    17.       Local variable table:  
    18.         [pc: 0, pc: 29] local: stack index: 0 type: LinkedStack  
    19.         [pc: 19, pc: 28] local: top index: 1 type: java.lang.Integer  

    几个重要的地方:

    • aload_x这个指令的意思是本地变量x中的值压入当前栈帧中
    • astore_x是将当前栈帧中栈顶的元素存储到本地变量x

     

    PC(程序计数器)的值为18时,发生的astore_1[top] 的意义就是讲top的值存储到索引为1的本地变量中,本地变量参考最下面的Local variable table

     

    紧接着,在PC等于20时,进行了方法的递归调用,这里发生的操作是,创建了新的栈帧,新的栈帧中含有传入的参数(同样以本地变量的形式存在),并将该新创建的栈帧压入方法调用栈。然后接着执行同样的操作,最后递归一层层返回,也就是方法调用栈一个栈帧接一个的弹出。

     

    重述一下,本地变量表是属于一个栈帧的,而一个栈帧则是方法调用栈的一个元素。所以,将栈顶元素保存到一个本地变量中,本质还是将这个本地变量压入了栈中(表现形式为栈帧被压入了方法调用栈),只不过这个过程不那么明显罢了。

    1. // 非递归实现  
    2. auxiliary.push(stack.pop());  
    3. // 递归实现  
    4. Integer top = stack.pop();  
    5. reverseStack(stack);  

    至此,前两个步骤已经完成。就剩下最后一个步骤:将取出的栈顶元素插入到栈底。

     

    首先还是从最直观的想法开始,栈的特性决定了直接将元素插入到栈底是不可能的。所以我们可以将当前栈中的所有元素弹出,然后将待插入元素压入栈,此时压入的位置当然就是栈底了,最后将之前弹出的元素再压入到栈中。很明显的,这里又涉及到了元素的弹出和压入,不难得出这个顺序也是满足栈的操作特点的,更具体的,元素的弹出和再压入是满足“后出先进”(Last-out-First-In)规律的。


    所以最直观的实现如下所示:

    1. private static void insertToStackBottom(LinkedStack<Integer> stack, Integer bottom) {  
    2.     assert (null != stack);  
    3.     LinkedStack<Integer> auxiliary = new LinkedStack<Integer>();  
    4.     while(!stack.isEmpty()) {  
    5.         auxiliary.push(stack.pop());  
    6.     }  
    7.     stack.push(bottom);  
    8.     while(!auxiliary.isEmpty()) {  
    9.         stack.push(auxiliary.pop());  
    10.     }  
    11. }  

    上述代码使用了一个辅助栈作为原栈中元素的临时存储空间。待传入的元素被压入到栈底之后,再将临时栈中的元素压入原栈中。但是,很明显的,这里又使用了显式声明的栈。

     

    如果想将以上的方法使用递归实现,那么就必须找出可以利用的子问题。听起来好像是在使用动态规划求解问题。实际上,递归算法和动态规划算法之间也有很微妙的关系,一般的动态规划方法有自顶向下以及自底向上的方法。而自顶向下的方法往往会采用递归加上备忘录的方式实现。


    将以上问题使用递归的方式进行描述如下:

    • 将栈顶元素取出,将待插入元素插入到栈的栈底,将取出的栈顶元素压入

    以上的子问题描述感觉不是很自然,但是为了不显式的使用栈结构,也只能如此了。

    很明显的,当栈中不含有任何元素的时候,就可以将待插入元素压入栈了,因此可以写出以下的递归实现:

    1. private static void insertToStackBottom(LinkedStack<Integer> stack,  
    2.         Integer bottom) {  
    3.     // 当栈为空的时候,将待插入的元素放到栈底  
    4.     if(stack.isEmpty()) {  
    5.         stack.push(bottom);  
    6.         return;  
    7.     }  
    8.     // 取出栈顶的元素  
    9.     Integer top = stack.pop();  
    10.     // 将传入的栈底元素放到栈底  
    11.     insertToStackBottom(stack, bottom);  
    12.     // 还原  
    13.     stack.push(top);  
    14. }  

    还是来比较一下此方法的非递归版本和递归版本:

    • 递归实现中,存在声明本地变量然后利用方法调用栈来保存本地变量的情况
    • 非递归实现中,显式的创建了一个栈,来保存相关变量

     

    仔细比较一下,可以发现以下两种实现本质上也是相同的:

    1. // 非递归实现  
    2. while(!stack.isEmpty()) {  
    3.     auxiliary.push(stack.pop());  
    4. }  
    5. stack.push(bottom);  
    6. while(!auxiliary.isEmpty()) {  
    7.     stack.push(auxiliary.pop());  
    8. }  
    9. // 递归实现  
    10. Integer top = stack.pop();  
    11. insertToStackBottom(stack, bottom);  
    12. stack.push(top);  

    具体而言,非递归实现中将栈中的元素都显式地保存在了另外创建的一个栈中,而递归实现则隐式地将栈中的元素都保存到了方法调用栈中(通过栈帧中的本地变量表)

     

    前文中还提到了另外一种子问题的描述,即:

    • 取出栈底元素后,将栈进行逆序,最后将取出的栈底元素压入到栈顶

    本质上是一样的,这里就不提供实现了。

     

    我想,之所以递归算法有时候难以理解,可能是因为对方法调用栈的运行规律还不够了解所致。一般而言,同一个问题的递归实现总是会比非递归实现来的更简洁一些,这里是指代码量上的简洁,而代码量上的简洁来源于对方法调用栈的隐式使用 —— 不需要显式声明、操作栈这类数据结构当然会减少代码量。但是毫无疑问,递归实现对思维的要求会更高一些,首先你需要对待解决问题有一个全局的认识,知道如何将问题分解成子问题;其次,还需要有较好的编程能力,知道如何处理递归过程中的各种边界判断和终止条件。

  • 相关阅读:
    在C语言中,double、long、unsigned、int、char类型数据所占字节数
    C++基础之头文件和源文件的关系
    Activity与Fragment数据传递之Activity从Fragment获取数据 分类: Android 2015-07-02 09:56 12人阅读 评论(0) 收藏
    Activity与Fragment数据传递之Fragment从Activity获取数据 分类: Android 2015-07-01 14:12 17人阅读 评论(0) 收藏
    Java反射机制和对象序列化 分类: Java 2015-06-26 12:08 21人阅读 评论(0) 收藏
    Android通过播放多张图片形成一个动画 分类: Android 2015-04-24 14:05 16人阅读 评论(0) 收藏
    jvm参数的配置、垃圾回收器的配置
    selenium2工作原理
    LeetCode#1 Two Sum
    LeetCode#27 Remove Element
  • 原文地址:https://www.cnblogs.com/interdrp/p/7493773.html
Copyright © 2011-2022 走看看