一、题目和要求
【题目】
一个栈依次压入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]
五、总结
这道题的实现其实是很简单的,但是思路却非常值得借鉴,首先是我们要深入理解递归,不仅要明白递归的调用,还需要明白递归中的两类变量,懂得了两类变量的不同,就可以实现像题目中既递归获取栈底元素,又不会改变其他元素顺序的逻辑,其次是我们需要学习分治思想,学习这种整体的完成可以分为小部分的完成,最后加一片瓦就能实现整体的功能的思想。
后记
个人总结,欢迎转载、评论、批评指正