【算法刷题】如何仅用递归函数和栈操作逆序一个栈

一、题目和要求

【题目】
  一个栈依次压入1、2、3、4、5,那么从栈顶到栈底分别为5、4、3、2、1。将这个栈转置后,从栈顶到栈底为1、2、3、4、5,也就是实现栈中元素的逆序,但是只能用递归函数来实现,不能用其他数据结构。
【要求】
  只能用递归函数来实现,不能用其他的数据结构

二、思路分析

  要实现这个题目的要求,我们需要深入理解一下递归和分治两个常用的算法思路:

递归:

  递归调用的本质就是一个方法中再次去调用方法本身,直到达到递归结束的条件,跳出递归,这样做的好处其实就是将一个复杂的问题,通过一步一步的简化,直到问题的解决,也就是我们常说的分而治之的分治思想。对于递归,有以下几个点需要特别的注意:

  • 递归的调用位置:递归的调用位置就是调用本身方法的那个地方,因为递归是在一个线程中运行的,所以执行其实是线性的,调用了递归方法,递归方法后面的代码就不会再去执行了,需要等到下一级的递归返回,才会去执行后面的代码,这一点大家都明白,但是在解题的时候,又尝尝蒙圈。
  • 递归中的变量:递归其实是有两类变量的,一类是调用递归方法返回的这个变量,这种变量的结果其实是下一级递归的返回值,也就是我们分而治之的细分逻辑的结果,这种变量是在运行完下一级递归才能确定的;还有一类变量是本次递归方法中和下级递归没有关系的变量,这种变量不会因为调用下一级递归而变化,在调用下一级递归时,这类变量是存储在调用方法栈中的。清楚的区分了这两类的变量,我们就能利用他们中的区别去完成很多的逻辑处理。

分治思想

  有时候我们在解决问题的时候,问题本身就可以分成一个一个的小问题,最后将这些小问题的答案经过特定的逻辑组合在一起,就能够解决整体的问题。分治思想是一种解题的思路,在遇到一个问题比较复杂的时候,我们就可以思考能不能把它分成一个一个的小问题,然后再进行整合。

三、实现分析

  回归到题目本身上来,要将一个栈进行逆序,就是将栈中最底的元素放到最上面来,这让我们第一个想到的就是汉诺塔,不同的是,汉诺塔是用另外两个栈进行辅助的,我们这里要求用的是递归方法。
  结合递归方法和栈的数据结构,我们要逆序,首先要拿到的就是栈底的元素,但是栈只有pop()出上面的元素,才能取出下面的元素,那原来取到的元素就需要按照取出顺序有一个保存的地方,因为有顺序和保存这两个维度,在没有其他保存的数据结构辅助的时候,我们是没法使用一个递归方法完成栈逆序的操作的。这个时候我们就可以用分治的思想,比如从栈顶到栈底有3个元素,如果我们已经实现了前两个的逆序,我们只需要把第三个数放到最上面就可以,依次类推下去,如果栈底元素之上的所有元素都已经实现了逆序,我们只需要将栈底元素取出来,然后放到栈的最上面就实现了整体的功能。
  要用递归来实现上面的分治逻辑,要怎么实现呢?我们需要有两个递归方法,一个递归方法是获取到栈底的元素,其他元素的顺序并不改变,另一个递归方法来完成逆序的逻辑,就是将上个方法中除了栈底元素的新栈,进行递归,直到将除了栈底元素的新栈进行逆序完成,只需要将获取到的栈底元素push到栈顶,就完成了整个逻辑。具体两个方法的实现和分析如下:

1、获取并移除栈底元素的方法

    /**
     * 从栈中获取到最后一个(栈底)元素,其他的元素顺序不变
     *
     * @param stack :
     * @return :
     */
    private int getAndRemoveLastElement(Stack<Integer> stack) {
        int currentTopElement = stack.pop();
        if (stack.isEmpty()) {
            // 如果为空,说明当前就是最后一个元素,直接返回
            return currentTopElement;
        } else {
            // 递归获取最后一个元素
            int lastElement = getAndRemoveLastElement(stack);
            // 将当前的元素放到栈中
            stack.push(currentTopElement);
            return lastElement;
        }
    }

逻辑分析:假设一个栈中从栈顶到栈底一次是[1,2,3] ,其调用逻辑和变量值如下

  • 第一次递归,currentTopElement = 1,,栈中元素为:[2,3],stack.isEmpty() 为假,进入下次递归
  • 第二次递归,currentTopElement = 2,,栈中元素为:[3] stack.isEmpty()为假,进入下次调用
  • 第三次递归,currentTomElement=3,stack.isEmpty()为真,返回 3
  • 回到第二次递归,递归获取的lastElement = 3,方法栈中保存的currentTopElement = 2,stack.push(currentTopEmelet)为2,栈中元素为:[2]
  • 回到第一次递归,递归获取的lastElement = 3,方法栈中保存的currentTopElement = 1,stack.push(currentTopEmelet)为1,栈中元素为:[1,2]
  • 最后返回结果为3,栈中元素为[1,2],方法除了获取到栈底元素3,其他元素的位置没有发生改变

2、递归实现逆序的方法

    /**
     * 逆序栈中的元素
     *
     * @param stack :
     */
    private void reverse(Stack<Integer> stack) {
        if (stack.isEmpty()) {
            return;
        }
        // 获取到最后一个元素,然后把剩下的栈中的元素递归进行逆序(分治思想)
        int lastElement = getAndRemoveLastElement(stack);
        reverse(stack);
        // 到这个位置,说明已经用递归将除了lastElement的元素的栈已经逆序,只需要把最后元素压入堆栈中即可
        stack.push(lastElement);
    }

逻辑分析:假设一个栈中从栈顶到栈底一次是[1,2,3] ,其调用逻辑和变量值如下

  • 第一次递归,lastElement = 3,栈中元素为[1,2](具体原因请参照上面getAndRemoveLastElement方法的分析)
  • 第二次递归,lastElement = 2,栈中元素为[1]
  • 第三次递归,lastElement = 1,栈中已经没有元素
  • 第四次递归,栈已空,直接返回
  • 回到第三次递归,lastElement = 1,调用stack.push(lastElement),栈中元素为[1]
  • 回到第二次递归,lastElement = 2,调用stack.push(lastElement),栈中元素为[2,1]
  • 回到第一次递归,lastElement = 3,调用stack.push(lastElement),栈中元素为[3,2,1]
  • 最终调用结束,栈中元素为[3,2,1],实现了栈的反转

四、实现方案

完整代码和测试方法如下:

/**
 * 用递归函数逆序一个栈
 *
 * @author :
 */
public class RecursiveFunctionReverseStack {

    @Test
    public void test() {
        Stack<Integer> stack = new Stack<>();
        Arrays.asList(5, 4, 3, 2, 1).forEach(stack::push);
        System.out.println("--------------反转前元素-------------------");
        System.out.println(stack);
        System.out.println("--------------反转后元素-------------------");
        reverse(stack);
        System.out.println(stack);
    }

    /**
     * 从栈中获取到最后一个(栈底)元素,其他的元素顺序不变
     *
     * @param stack :
     * @return :
     */
    private int getAndRemoveLastElement(Stack<Integer> stack) {
        int currentTopElement = stack.pop();
        if (stack.isEmpty()) {
            // 如果为空,说明当前就是最后一个元素,直接返回
            return currentTopElement;
        } else {
            // 递归获取最后一个元素
            int lastElement = getAndRemoveLastElement(stack);
            // 将当前的元素放到栈中
            stack.push(currentTopElement);
            return lastElement;
        }
    }

    /**
     * 逆序栈中的元素
     *
     * @param stack :
     */
    private void reverse(Stack<Integer> stack) {
        if (stack.isEmpty()) {
            return;
        }
        // 获取到最后一个元素,然后把剩下的栈中的元素递归进行逆序(分治思想)
        int lastElement = getAndRemoveLastElement(stack);
        reverse(stack);
        // 到这个位置,说明已经用递归将除了lastElement的元素的栈已经逆序,只需要把最后元素压入堆栈中即可
        stack.push(lastElement);
    }
}

测试结果:

--------------反转前元素-------------------
[5, 4, 3, 2, 1]
--------------反转后元素-------------------
[1, 2, 3, 4, 5]

五、总结

  这道题的实现其实是很简单的,但是思路却非常值得借鉴,首先是我们要深入理解递归,不仅要明白递归的调用,还需要明白递归中的两类变量,懂得了两类变量的不同,就可以实现像题目中既递归获取栈底元素,又不会改变其他元素顺序的逻辑,其次是我们需要学习分治思想,学习这种整体的完成可以分为小部分的完成,最后加一片瓦就能实现整体的功能的思想。


后记
  个人总结,欢迎转载、评论、批评指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值