[程序员代码面试指南-笔记] 第1章-栈与队列

这里整理一些阅读此书的笔记。可能是题解浓缩,可能是重点提炼,也可能是书中以外的或是自己实现的题解代码,甚至个人认为书中有些题解其实讲的并不好,这里也会以自己的理解重新给出解答。另外,部分题目给出对应leetcode训练题号与链接。

设计一个有getMin功能的栈

使用两个栈,另一个栈用来存每一步的最小值

LeetCode.155 最小栈

public class MinStack {

    Stack<Integer> elements;
    Stack<Integer> min;

    MinStack() {
        elements = new Stack<Integer>();
        min = new Stack<Integer>();
    }

    public int getMin() {
        return min.peek();
    }

    public void push(int x) {
        elements.push(x);
        if (min.isEmpty() || getMin() > x) {
            min.push(x);
        } else {
            min.push(getMin());
        }
    }

    public int pop() {
        min.pop();
        return elements.pop();
    }

    int top() {
        return elements.peek();
    }
}

由两个栈组成的队列

pop时将栈倒腾到另外一个栈里,然后再另外一个栈pop就可以了。

LeetCode.232 用栈实现队列

public class MyQueue {

    Stack<Integer> push;
    Stack<Integer> pop;

    /**
     * Initialize your data structure here.
     */
    public MyQueue() {
        push = new Stack<Integer>();
        pop = new Stack<Integer>();
    }

    /**
     * Push element x to the back of queue.
     */
    public void push(int x) {
        push.push(x);
    }

    /**
     * Removes the element from in front of queue and returns that element.
     */
    public int pop() {
        if (pop.isEmpty()) {
            while (!push.isEmpty()) {
                pop.push(push.pop());
            }
        }

        return pop.pop();
    }

    /**
     * Get the front element.
     */
    public int peek() {
        if (pop.isEmpty()) {
            while (!push.isEmpty()) {
                pop.push(push.pop());
            }
        }

        return pop.peek();
    }

    /**
     * Returns whether the queue is empty.
     */
    public boolean empty() {
        return pop.isEmpty() && push.isEmpty();
    }
}

同样,有个类似的LeetCode.225 用队列实现栈

相反的,栈pop的时候将当前队列的处最后一个元素之外的所有元素出队到另外一个元素里,然后出队最后一个元素即可

public class MyStack {

    List<Integer> first;
    List<Integer> second;

    /**
     * Initialize your data structure here.
     */
    public MyStack() {
        first = new ArrayList<Integer>();
        second = new ArrayList<Integer>();
    }

    /**
     * Push element x onto stack.
     */
    public void push(int x) {
        first.add(x);
    }

    /**
     * Removes the element on top of the stack and returns that element.
     */
    public int pop() {
        while (!first.isEmpty()) {
            second.add(first.get(0));
            first.remove(0);
        }

        Integer integer = second.get(second.size() - 1);
        second.remove(second.size() - 1);

        return integer;
    }

    /**
     * Get the top element.
     */
    public int top() {
        while (!first.isEmpty()) {
            second.add(first.get(0));
            first.remove(0);
        }

        Integer integer = second.get(second.size() - 1);

        return integer;
    }

    /**
     * Returns whether the stack is empty.
     */
    public boolean empty() {
        return first.isEmpty() && second.isEmpty();
    }
}

用栈解决汉诺塔问题

实际上理解该题的要点就是要理解"递归过程"这一概念。举个例子,三个柱子分别记作A、B、C,如果要把A中的n个积木移到B,实际上只需要3步:

  1. 将A中的n-1块积木通过递归过程移动到C。move(n-1,from A,to C)
  2. 将A的第n块积木移动到B,print("number n from A to B")
  3. 将C中的n-1块积木通过递归过程移动到B。move(n-1,from C,to B)

理解了这个事情,接下来只需处理两个事情即可了:

  1. 函数怎么声明?(递归状态是什么?)
  2. 递归终止条件是什么?

函数声明:move(int num,String A,String B,String C,String from,String to),6个参数表示一次移动的状态,其中num表示剩余待处理块的数目。

递归终止条件,也就是只剩一层积木时的打印过程:

  1. A to B
  2. A to C
  3. B to A
  4. B to C
  5. C to A
  6. C to B

当然,再加上一个当num==1的时候。

生成窗口最大值数组

滑动窗口最值问题。用一个双端队列来解决。其核心思想就是

  1. 队列中记录的是数组的下标
  2. 队列的下标所代表的数组值始终保持单调递减
  3. 队列中的下标始终是处于窗口中的下标
  4. 队列右侧用来刷新新增的下标。如果新增的值比当前队列最后一位所代表的数组值大,从右侧pop直到小于新增值,同时将新增值入队
  5. 队列左侧用来刷新下标过期后的值。如果队列左侧的值下标不再在窗口内,从左侧弹出该下标

Todo 找几个leetcode相关题刷刷

求最大子矩阵的大小

实际上该题为求直方图中的最大矩阵面积的变形。

直方图中的最大矩阵面积

求直方图矩阵最值问题,其核心解决思路实际上就是,如何快速的定位左右边界。

为方便描述,假设存在一个height={3,4,5,4,3,6}的直方图,
其中一个简单可行的暴力思路为,从左到右枚举x,使x为左边界,且当前x为矩阵高。那么求改矩阵的面积实际上就是往右边依次查找不低于x的最右边一条边在哪。对于5来说,其右边界仍然为5,对于height[1]的4,其右边界为height[3]。该方法为O(n^2)

显而易见,往右查找最远的右边界时,包含了重复计算。

另外一个暴力思路就是,枚举每条柱子,作为高,然后向左向右查找最远的左右边界。该方法为O(n^3),显然包含了更多的重复计算。

对暴力思路2做个优化,同样是每条柱子作为高,但同时把该条柱子作为右边界,然后只向左查找最远左边界。你可能会顾虑如果右侧还可以扩展,此时计算的矩阵面积偏小怎么办。实际上当枚举到真实最右边界时,会覆盖掉这条计算。

如果我们通过增加空间,实现O(1)效率快速定位到左边界,那么就可以做到O(n)。不难发现,找最远左边界,实际上就是当前右边界往左开始找,比当前右边界小的第一个值的下标k,那么k+1既为最远左边界。如果当前右边界的下标为j,那么此时矩阵长度则为(j - (k+1) + 1).

那么,我们维护一个单调递增的数组即可。借助栈使用O(n)算法。

  1. 栈中存储下标
  2. 栈中存储下标对应的值从左到右(从底到顶)始终保持单调递增
  3. 从左到右枚举。
    1. 如果height[i]比height[stack.peek()]大,i入栈。
    2. 否则,依次弹出栈顶元素,记作j。此时形成矩阵的高即为height[j],那么左边界在哪?最远左边界肯定是左边第一个小于height[j]的值,也就是当前栈顶元素,那么左边界下标就是stack.peek()+1,(如果栈空,左边界下标为0)。则此时矩阵长为(i-(stack.peek()+1)),代表的值。
    3. 直到栈顶元素大于i,此时将i入栈。
  4. 遍历完成后,栈不为空,同样,当然栈顶元素记为j,j为右边界;依次弹出栈顶元素,找到左边界,计算矩阵值。直到栈为空。
public int maxInZhifangtu(int[] heights, int len) {
        Stack<Integer> stack = new Stack<>();
        int ans = 0;
        for (int i = 0; i < len; i++) {
            while (i < len && (stack.isEmpty() || heights[stack.peek()] < heights[i])) {
                stack.push(i);
                i++;
            }
            if (i == len) break;
            while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
                int j = stack.pop();// i 为右界下标
                int height = heights[j];
                int k = stack.isEmpty() ? -1 : stack.peek();//k 为左界下标,实际上所求矩阵为(k,i),也就是[k+1,i-1];实际上,j永远会==i-1
                ans = Math.max(ans, height * (i - k - 1));
            }
            stack.push(i);
        }

        while (!stack.isEmpty()) {
            int i = len;
            int j = stack.pop();
            int height = heights[j];
            int k = stack.isEmpty() ? -1 : stack.peek();
            ans = Math.max(ans, height * (i - k - 1));
        }

        return ans;
    }
}

对于求最大子矩阵的大小,假设矩阵的行数为N,枚举每一行作为一个直方图,然后套用直方图求矩阵的算法。另外下一行直方图的height数组可根据上一行的height数组累加得来,则算法整体的时间复杂度为O(n^2)

posted @ 2019-01-18 00:18 ACBingo 阅读(...) 评论(...) 编辑 收藏