使用栈将递归函数转化为非递归函数_《程序员代码面试指南-左程云》数据结构与算法笔记 第一章 栈和队列...

《程序员代码面试指南-左程云》笔记
第一章 栈和队列

左神(左程云)《程序员面试指南》金三银四阿里面试必备的两道算法面试题讲解

马士兵老师2020最新数据结构与算法全套合集


设计一个有getMin功能的栈
实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作。
要求:pop、push、getMin操作的时间复杂度都是O(1)。
解答:增加一个栈(minStack),用来维护每个元素进栈时栈的最小值。每个元素进栈时,minStack的更新规则:若当前元素比minStack栈顶元素小,则直接将当前元素进栈;否则,将之前的栈顶元素复制再次进栈。 由两个栈组成的队列 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek)。
解答:两个栈,一主一辅。在poll或peek的时候,将主栈中的元素依次出栈然后进栈到辅栈,这样队列头就是辅栈的栈顶元素。
这里朴素的想法是在每次poll或peek的时候将主栈的元素全部dump到辅栈,得到队列头元素,然后再全部dump回主栈。但其实没有必要。add永远是在主栈add,但poll或peek的时候,先看辅栈是否为空,若辅栈非空,说明队列头部已经dump过来了,直接取栈顶;若辅栈为空,再进行dump操作。重点是,dump完后,不必再dump回去。

2aa1450a92c217823ae0d62e5d5159f8.png

如何仅用递归函数和栈操作逆序一个栈

要求:只有一个待逆转的栈,不能有其他数据结构。
这道题的确没有想到。解法用了两个递归函数,其中一个非常的“tricky”:
public int getAndRemoveLastElement(Stack<Integer> stack) { int top = stack.pop(); if (stack.isEmpty()) return top; int last = getAndRemoveLastElement(stack); stack.push(top); return last; }
getAndRemoveLast,得到并移除栈底元素,递归可以做到吗?怎么做?

这个递归和我以前理解的递归是有些不一样的。回想一下,不论是汉诺塔问题或是中序遍历二叉树,父问题向子问题的转化是十分清晰的、自然而然的;但这里,getAndRemoveLast(n)与getAndRemove(n-1)一眼是看不出转化过程的。所以,有点tricky.
可以从两个角度来再现这个递归过程:1,自底向上;2,递归栈。
这道题另一个“tricky”的地方在于,没有见识过两个递归的情况,而习惯性的想怎么用一个递归解决,陷入死胡同。
用一个栈实现另一个栈的排序“维持一个有序的栈”系列开始。
解答:将要排序的栈记为stack,辅助栈为helpStack。在stack上执行pop操作,弹出的元素记为cur。

  • 如果cur小于等于helpStack的栈顶元素,则直接将cur压入helpStack;
  • 如果cur大于helpStack的栈顶元素,则将helpStack的元素依次出栈,压入stack,直到cur小于等于helpStack栈顶元素,然后再将cur压入helpStack。

用栈解决汉诺塔问题
条件:不能直接从A到C或C到A,必须经过B。
这道题我的第一反应是用栈模拟递归。然而,好像并没有“用栈模拟递归”这回事(也不知道这个印象怎么来的)。以树的遍历为例,要说也是递归模拟栈吧?
睡觉前躺床上又想了想,补上。
用栈实现和递归实现没有半毛钱关系。用栈和用队列本质上是一样的,都只是借助栈或队列这种结构的特点。
解答:用3个栈来模拟3座塔。那么,每一步该选哪个栈呢?出栈后又该往哪个栈进呢?
这里的关键是,有两个原则:

  1. 小压大原则:小的只能在大的上面(汉诺塔的要求);
  2. 相邻不可逆原则:X->Y和Y->X是互斥的;
 根据着两条原则,假如上一步是A->B,那么当前就不能是B->A,而B-C和C->B中只有一个可以选(根据B、C栈顶元素大小)。所以每一步的走法都是由上一步确定的。
 public void hanoi(int n) {
    Stack<Integer> stackA = new Stack<>();
    Stack<Integer> stackB = new Stack<>();
    Stack<Integer> stackC = new Stack<>();
    int lastMove = 3; // 1, 2, 3, 4, 分别代表左至中、中至左、中至右、右至中;1/2互斥、3/4互斥。初始化为3或4都行,trick。
    int thisMove = 0;
    int moveCnt = 0;
    for (int i = n; i > 0; i--) {
        stackA.push(i);
    }
    while (stackC.size() != n) {
        moveCnt++;
        if (lastMove <= 2) { // 上一步是1或2,下一步只能是3或4;
            if (stackB.isEmpty())
                thisMove = 4;
            else if (stackC.isEmpty())
                thisMove = 3;
            else if (stackB.peek() < stackC.peek())
                thisMove = 3;
            else
                thisMove = 4;
            if (thisMove == 3) {
                System.out.println(String.format("Move %d B ==> C", stackB.peek()));
                stackC.push(stackB.pop());
            } else {
                System.out.println(String.format("Move %d C ==> B", stackC.peek()));
                stackB.push(stackC.pop());
            }
        } else {
            if (stackA.isEmpty())
                thisMove = 2;
            else if (stackB.isEmpty())
                thisMove = 1;
            else if (stackA.peek() < stackB.peek())
                thisMove = 1;
            else
                thisMove = 2;
            if (thisMove == 1) {
                System.out.println(String.format("Move %d A ==> B", stackA.peek()));
                stackB.push(stackA.pop());
            } else {
                System.out.println(String.format("Move %d B ==> A", stackB.peek()));
                stackA.push(stackB.pop());
            }
        }
        lastMove = thisMove;
    }
    System.out.println("Total move count: " + moveCnt);
}

生成窗口最大值数组 有一个整型数组arr和一个大小为w的窗口从数组的最左边滑动到最右边,一共可产生n-w+1个窗口的最大值,请返回这个最大值数组。
要求:O(N)实现。
维护最大值可以用堆,但堆怎么在窗口移动过程中移除掉被窗口划过的值?
这里介绍了一种简单但性质强大的结构:有序栈(自己起的名字)。即将一系列值依次入栈,但在入栈过程中要始终保持栈的有序性,不合格的元素要弹出来(过程很像插入排序)。

9fcc45ba73b3f0060572e981747f9075.png

有序栈的性质有(假设从栈顶到栈底递增顺序):

  • 当前元素 i 入栈后,栈底元素即为数组中 i 左边(包括 i)的最大值;
  • i 下面的一个元素即为数组中 i 左边第一个比它大的值;
  • 若被弹出的元素为 j,则 i 是 j 右边第一个比它大的元素(这样对每个弹出的元素来说,就获取了其左右两边第一个比它大的元素);
 简单说就是:左边最大值,左、右两边第一个比它大的值;而且,栈中元素不仅大小有序,序号也是有序的!
 而且而且,对一个大小为n的数组来说,数组元素依次进栈,维护这个有序栈的时间复杂度是O(N)!(考虑一个元素不好想,但全局来看,基本操作只有进栈和出栈两种,而每个元素进栈出栈最多一次,所以时间最多是2n-1)
 回到这道题,因为窗口是移动的,不断有元素要移出窗口,所以每次移动都要看栈底元素是否已经在窗口之外了(看上面,栈底元素就是栈中所有元素里序号最小的),若是,则从栈底移除——这样就是队列了(大部分时候它都是个栈)。另,因为要根据序号来移除元素,所以栈中的元素是数组元素的下标,而不是元素值。
 int[] getMaxWindow(int[] arr, int w) {
    if (arr == null || w < 1 || w > arr.length)
        return null;
    int[] result = new int[arr.length - w + 1];
    LinkedList<Integer> queue = new LinkedList<>();
    for (int i = 0; i < arr.length; i++) {
        while (!queue.isEmpty() && arr[queue.peekLast()] < arr[i]) {
            queue.pollLast();
        }
        queue.addLast(i);
        if (queue.peekFirst() <= i - w) {
            queue.pollFirst();
        }
        if (i >= w - 1) {
            result[i - w + 1] = arr[queue.peekFirst()];
        }
    }
    return result;
}

求最大子矩阵的大小
给定一个整型矩阵map,其中的值只有0和1两种,求其中全是1的矩形区域中,最大的一个有多少个1。
例如:
1 0 1 1
1 1 1 1
1 1 1 0
返回6.
矩阵大小为MN,要求时间复杂度为O(MN)。
解答:矩阵的行数为M,以每一行做切割,统计以当前行作为底的情况下,每个位置上的1的“高度”,并计算以当前行为底的最大矩阵的大小。
例如,遍历到第三行时,每个位置1的高度为{3,2,3,0}:

64b0cb9fd65e4eeff9e295fb043fbd06.png

接下来就要求以每根柱子向两边扩展出去的最大矩形,即要找到左右两边第一根比它矮的柱子——左右两边第一个比它小的值——有序栈。

 public int maxRecFromBottom(int[] height) {
    if (height == null || height.length == 0) {
        return 0;
    }
    int maxArea = 0;
    Stack<Integer> stack = new Stack<>();
    for (int i = 0; i < height.length; i++) {
        while (!stack.isEmpty() && height[stack.peek()] > height[i]) {
            // 对于被弹出去的元素来说,就找到了它左右两边第一个比它小的元素;
            int mid = stack.pop();
            int left = stack.isEmpty() ? -1 : stack.peek();
            int area = height[mid] * (i - left - 1);
            maxArea = Math.max(maxArea, area);
        }
        stack.push(i);
    }
    while (!stack.isEmpty()) { // 全部元素遍历完后,栈可能非空,需继续处理;
        // 此时的栈是什么样子呢?数组最后一个元素一定在栈顶,栈中元素的右边没有比它小的元素(如果有它就被弹出了);
        int mid = stack.pop();
        int left = stack.isEmpty() ? -1 : stack.peek();
        int area = height[mid] * (height.length - left - 1);
        maxArea = Math.max(maxArea, area);
    }
    return maxArea;
}

"此时的栈是什么样子呢?数组最后一个元素一定在栈顶,栈中元素的右边没有比它小的元素(如果有它就被弹出了)"

最大值减去最小值小与或等于num的子数组的数量

给定数组arr和整数num,返回共有多少个子数组满足如下情况:
 max(arr[i..j]) - min(arr[i..j]) <= num
 要求:O(N)实现。
 关键字:最大值、最小值、O(N)
 解答:使用两个有序队列(相对于有序栈来命名)qmax和qmin,分别维护arr[i..j]的最大值和最小值更新结构。当子数组a[i..j]向右滑动一个位置变成arr[i..j+1]时,qmax和qmin可以在O(1)时间更新(上面已经分析,平均来看的确是O(1)),并且可以在O(1)时间得到arr[i..j+1]的最大值和最小值。
 步骤是这样的:i,j 从0开始,首先 j 向右滑动,这个过程中维护arr[i..j]的最大值和最小值更新结构,同时观察(max-min)的值,当其大于num时,停止 j,此时,arr[i..j] 内以 i 为起始满足条件的子数组有 j - i 个;然后 i 向右滑动一位,继续上诉过程,直到 i 到达数组末尾。
 public int getNum(int[] arr, int num) {
    if (arr == null || arr.length == 0) {
        return 0;
    }
    LinkedList<Integer> qmax = new LinkedList<>();
    LinkedList<Integer> qmin = new LinkedList<>();
    int i = 0, j = 0;
    int cnt = 0;
    while (i < arr.length) {
        while (j < arr.length) {
            while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[j])
                qmax.pollLast();
            qmax.addLast(j);
            while (!qmin.isEmpty() && arr[qmin.peekLast()] >= arr[j])
                qmin.pollLast();
            qmin.addLast(j);
            if (arr[qmax.peekFirst()] - arr[qmin.peekFirst()] > num) {
                break;
            }
            j++;
        }
        cnt += j - i;
        i++;
        if (qmax.peekFirst() < i)
            qmax.pollFirst();
        if (qmin.peekFirst() < i)
            qmin.peekFirst();
    }
    return cnt;
}

注意上面的<= 和 >=,是为了在 i 向前移动一步的时候保持 j 不变(先进栈再出栈)。

左神(左程云)《程序员面试指南》金三银四阿里面试必备的两道算法面试题讲解

马士兵老师2020最新数据结构与算法全套合集

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值