第一章 栈和队列

设计一个有getMIn功能的栈

题目:实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作。

要求:

1.pop、push、getMin 操作的时间复杂度都是 O(1)。

2.设计的栈类型可以使用现成的栈结构。

方法1:不同步压入:newValue <= getMin()才入最小值栈,代价是出栈操作更麻烦

在这里插入图片描述

public class Mystack1 {
    private Stack<Integer> stackData;
    private Stack<Integer> stackMin;

    public Mystack1() {
        stackData = new Stack<>();
        stackMin = new Stack<>();
    }

    // push:<=才入栈,说明是data和min是不同步压入
    public void push(int newValue) {
        if (stackMin.isEmpty()) {
            stackMin.push(newValue);
        } else if (newValue <= getMin()) {
            stackMin.push(newValue);
        }
        stackData.push(newValue);
    }

    // pop:由于是不同步压入,data栈需要出找和getMin比较,相同两者都要出栈
    public int pop() {
        if (stackData.isEmpty()) {
            throw new RuntimeException("Your stack is empty");
        }
        int value = stackData.pop();
        // 由于入栈是<=保证不同步压入,所以这里==就需要重新判断
        if (value == getMin()) {
            stackMin.pop();
        }
        return value;
    }

    public int getMin() {
        if (stackMin.isEmpty()) {
            throw new RuntimeException("Your stack is empty");
        }
        return stackMin.peek();
    }
}

方法2:同步压入

在这里插入图片描述

public class Mystack2 {
    private Stack<Integer> stackData;
    private Stack<Integer> stackMin;

    public Mystack2() {
        stackData = new Stack<>();
        stackMin = new Stack<>();
    }
    // push:>=时候,data和min栈都同步压入,保证了其他方法的简单书写
    public void push(int newValue) {
        if (stackMin.isEmpty()) {
            stackMin.push(newValue);
        } else if (newValue < getStackMin()) {
            stackMin.push(newValue);
        } else {
            stackMin.push(getStackMin());
        }
        stackData.push(newValue);
    }

    public int pop() {
        if (stackData.isEmpty()) {
            throw new RuntimeException("Your stack is empty");
        }
        stackMin.pop();
        return stackData.pop();
    }

    public int getStackMin() {
        if (stackMin.isEmpty()) {
            throw new RuntimeException("Your stack is empty");
        }
        return stackMin.peek();
    }
}

力扣115:最小值栈

class MinStack {
    private Stack<Integer> stackData;
    private Stack<Integer> stackMin;

    public MinStack() {
        stackData = new Stack<>();
        stackMin = new Stack<>();
    }

    public void push(int x) {
        if (stackMin.isEmpty()) {
            stackMin.push(x);
        } else if (x < getMin()) {
            stackMin.push(x);
        } else {
            stackMin.push(getMin());
        }
        stackData.push(x);
    }

    public void pop() {
        if (stackData.isEmpty()) {
            throw new RuntimeException("Your stack is empty");
        }
        stackMin.pop();
        stackData.pop();
    }

    public int top() {
        if (stackData.isEmpty()) {
            throw new RuntimeException("Your stack is empty");
        }
        return stackData.peek();
    }

    public int getMin() {
        if (stackMin.isEmpty()) {
            throw new RuntimeException("Your stack is empty");
        }
        return stackMin.peek();
    }
}

由两个栈组成的队列

题目:编写一个类,用两个栈实现队列,支持队列的基本操作(add、poll、peek)。

问题:利用两个栈,定义一个pushToPop方法简化操作

1.如果 stackPush 要往 stackPop 中压入数据,那么必须一次性把 stackPush 中的数据全部压入。

  • 15 依次压入 stackPush,stackPush 的栈顶到栈底为 51,从 stackPush压入 stackPop 时,只将 5 和 4 压入了 stackPop,stackPush 还剩下 1、2、3 没有压入。此时如果用户想进行弹出操作,那么 4 将最先弹出,与预想的队列顺序就不一致。

2.如果 stackPop 不为空,stackPush 绝对不能向 stackPop 中压入数据。违反了以上两点都会发生错误。

  • 违反 2 的情况举例:15 依次压入 stackPush,stackPush 将所有的数据压入 stackPop,此时从 stackPop 的栈顶到栈底就变成了 15。此时又有 6~10 依次压入 stackPush,stackPop 不为空,stackPush 不能向其中压入数据。如果违反 2 压入了 stackPop,从 stackPop 的栈顶到栈底就变成了 6-10、1-5。那么此时如果用户想进行弹出操作,6 将最先弹出,与预想的队列顺序就不一致。
public class TwoStacksQueue {
    private Stack<Integer> stackPush;
    private Stack<Integer> stackPop;

    public TwoStacksQueue() {
        stackPush = new Stack<Integer>();
        stackPop = new Stack<Integer>();
    }

    // 定义一个压栈函数:如果pop栈为空,就一次性把push栈中元数入pop栈
    private void pushToPop() {
        if (stackPop.empty()) {
            while (!stackPush.empty()) {
                stackPop.push(stackPush.pop());
            }
        }
    }
    // 添加之后,保证一个压栈操作
    public void add(int pushInt) {
        stackPush.push(pushInt);
        pushToPop();
    }
    // 出栈之前,保证一个压栈操作
    public int poll() {
        if (stackPush.empty() && stackPop.empty()) {
            throw new RuntimeException("Queue is empty");
        }
        pushToPop();
        return stackPop.pop();
    }
    // 出栈之前,保证一个压栈操作
    public int peek() {
        if (stackPush.empty() && stackPop.empty()) {
            throw new RuntimeException("Queue is empty");
        }
        pushToPop();
        return stackPop.peek();
    }
    // 判空之前,保证一个压栈操作
    public boolean empty() {
        pushToPop();
        return stackPush.isEmpty();
    }
}

力扣232:用栈实现队列

方法:使用左神的pushToPop()方法,摊还时间复杂度

class MyQueue {
    private Stack<Integer> stackPush;
    private Stack<Integer> stackPop;

    public MyQueue() {
        stackPush = new Stack<Integer>();
        stackPop = new Stack<Integer>();
    }

    private void pushToPop() {
        while (stackPop.isEmpty()) {
            while (!stackPush.isEmpty()) {
                stackPop.push(stackPush.pop());
            }
        }
    }

    public void push(int x) {
        stackPush.push(x);
        pushToPop();
    }


    public int pop() {
        if (empty()) {
            throw new IllegalArgumentException("Stack is Empty");
        }
        pushToPop();
        return stackPop.pop();
    }


    public int peek() {
        if (empty()) {
            throw new IllegalArgumentException("Stack is Empty");
        }
        pushToPop();
        return stackPop.peek();
    }


    public boolean empty() {
        return stackPush.isEmpty() && stackPop.isEmpty();
    }
}

力扣225:用队列实现栈

方法1:使用一个队列,为了实现后进先出,先记录原数组的个数n,进栈后原个数的元素出栈再进栈

class MyStack {
    Queue<Integer> q;

    public MyStack() {
        q = new LinkedList<>();
    }
    // push:先记录之前队列个数,元素入队列后,再出之前队中元素入队
    public void push(int x) {
        int n = q.size();
        q.offer(x);
        for (int i = 0; i < n; i++) {
            q.offer(q.poll());
        }
    }

    public int pop() {
        return q.poll();
    }

    public int top() {
        return q.peek();
    }

    public boolean empty() {
        return q.isEmpty();
    }
}

方法2:使用2个队列,q1始终指向数据队列,q2是辅助队列。q2先入元素,再将q1的元素倒入q2,最后交换2个队列,

class MyStack1 {
    LinkedList<Integer> queue1;
    LinkedList<Integer> queue2;

    public MyStack1() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }

    public void push(int x) {
        // 1.q2先入元素,辅助队列中暂存队列头的元素
        queue2.offer(x);
        // 2.q1中的元素全部入q2
        while (!queue1.isEmpty()) {
            queue2.offer(queue1.poll());
        }
        // 3.交换2个栈
        LinkedList<Integer> temp;
        temp = queue1;
        queue1 = queue2;
        queue2 = temp;
    }


    public int pop() {
        return queue1.poll();
    }

    public int top() {
        return queue1.peek();
    }

    public boolean empty() {
        return queue1.isEmpty();
    }
}

递归逆序一个栈

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

方法:实现一个获取栈底元素其他元素不变的函数+主函数逆序

在这里插入图片描述

在这里插入图片描述

public class Reverse {
    public static void reverse(Stack<Integer> stack) {
        // 1.递归结束:因为get操作有弹出,肯定是栈空为结束
        if (stack.isEmpty()) {
            return;
        }
        // 2.想成普通方法,一次获得栈底元素
        int i = getAndRemoveLastElement(stack);
        // 3.递归逆序入栈
        reverse(stack);
        stack.push(i);
    }

    // 返回栈底元素
    private static int getAndRemoveLastElement(Stack<Integer> stack) {
        // 1.先弹出栈顶元素
        int result = stack.pop();
        // 2.递归结束:栈为空,返回当前弹出元素
        if (stack.empty()) {
            return result;
        } else {
            // 3.这里递归得到last = 栈底元素
            int last = getAndRemoveLastElement(stack);
            // 4.每一次都原栈的元素返回去
            stack.push(result);
            // 5.每一次递归返回都是最后一个元素
            return last;
        }
    }
}

猫狗队列

题目:猫狗队列如下,请实现一个数据结构,add、pollAll、pollDog、pollCat等方法。要求是能按照进入队列的顺序依次出队

public class DogCatQueue {

    public DogCatQueue() {

    }

    public void add(Pet pet) {

    }

    public Pet pollAll() {

    }

    public Dog pollDog() {

    }

    public Cat pollCat() {

    }

    public boolean isEmpty() {

    }

    public boolean isDogQueueEmpty() {

    }

    public boolean isCatQueueEmpty() {

    }
}

条件类:

public class Pet {
    private String type;

    public Pet(String type) {
        this.type = type;
    }

    public String getPetType() {
        return this.type;
    }
}
public class Cat  extends Pet {
    public Cat() {
        super("cat");
    }
}
public class Dog extends Pet {
    public Dog() {
        super("dog");
    }
}

方法:实现一个PetEnterQueue类,盖上时间戳。

public class PetEnterQueue {
    private Pet pet;
    private long count;

    public PetEnterQueue(Pet pet, long count) {
        this.pet = pet;
        this.count = count;
    }
    public Pet getPet(){
        return this.pet;
    }
    public long getCount() {
        return this.count;
    }
    public String getEnterPetType(){
        return this.pet.getPetType();
    }
}
public class DogCatQueue {
    private Queue<PetEnterQueue> dogQ;
    private Queue<PetEnterQueue> catQ;
    // 时间戳
    private long count;

    public DogCatQueue() {
        this.dogQ = new LinkedList<PetEnterQueue>();
        this.catQ = new LinkedList<PetEnterQueue>();
        this.count = 0;
    }

    public void add(Pet pet) {
        if (pet.getPetType().equals("dag")) {
            dogQ.add(new PetEnterQueue(pet, count++));
        } else if (pet.getPetType().equals("cat")) {
            catQ.add(new PetEnterQueue(pet, count++));
        } else {
            throw new RuntimeException("err,not dog or cat");
        }
    }

    public Pet pollAll() {
        if (!dogQ.isEmpty() && !catQ.isEmpty()) {
            if (dogQ.peek().getCount() < catQ.peek().getCount()) {
                return dogQ.poll().getPet();
            } else {
                return catQ.poll().getPet();
            }
        } else if (!dogQ.isEmpty()) {
            return dogQ.poll().getPet();
        } else if (!catQ.isEmpty()) {
            return catQ.poll().getPet();
        } else {
            throw new RuntimeException("err,queue is empty");
        }
    }

    public Dog pollDog() {
        if (!isDogQueueEmpty()) {
            return (Dog) dogQ.poll().getPet();
        } else {
            throw new RuntimeException("DogQueue is empty");
        }
    }

    public Cat pollCat() {
        if (!isCatQueueEmpty()) {
            return (Cat) catQ.poll().getPet();
        } else {
            throw new RuntimeException("CatQueue is empty");
        }
    }

    public boolean isEmpty() {
        return dogQ.isEmpty() && catQ.isEmpty();
    }

    public boolean isDogQueueEmpty() {
        return dogQ.isEmpty();
    }

    public boolean isCatQueueEmpty() {
        return catQ.isEmpty();
    }
}

用一个栈实现另一个栈的排序

题目:一个栈中元素的类型为整型,现在想将该栈从顶到底按从大到小的顺序排序,只许申请一个栈。除此之外,可以申请新的变量,但不能申请额外的数据结构。如何完成排序?

方法:使用一个辅助栈,保存从小到大的排序

public static void sortStackByStack(Stack<Integer> stack) {
    // 1.使用一个辅助栈,将stack栈中从小到大排序
    Stack<Integer> help = new Stack<>();
    while (!stack.isEmpty()) {
        int cur = stack.pop();
        // 2.反过来想,cur>辅助栈顶,辅助栈出栈进原栈
        while (!help.isEmpty() && cur > help.peek()) {
            stack.push(help.pop());
        }
        // 3.cur<=辅助栈顶,就入辅助栈
        help.push(cur);
    }
    // 4.遍历辅助栈元素,依次进原栈,实现原栈栈顶到栈底从大到小
    while (!help.isEmpty()) {
        stack.push(help.pop());
    }
}

用栈来求解汉诺塔问题

回顾原汉诺塔问题:假设有 from 柱子、mid 柱子和 to 柱子,都从 from 的圆盘 1~i 完全移动到 to,最优过程为:

步骤 1:为圆盘 1~n-1 从 from 移动到 mid。

步骤 2:为单独把圆盘 n 从 from 移动到 to。

步骤 3:为把圆盘 1~n-1 从 mid 移动到 to。如果圆盘只有 1 个,直接把这个圆盘从 from 移动到 to 即可。

public static void hanoi(int n) {
    if (n > 0) {
        hanoi(n, "left", "mid", "right");
    }
}

private static void hanoi(int n, String from, String mid, String to) {
    if (n == 1) {
        System.out.println("Move from " + from + " to " + to);
    } else {
        hanoi(n - 1, from, to, mid);
        hanoi(1, from, mid, to);
        hanoi(n - 1, mid, from, to);
    }

}

力扣面试题 08.06. 汉诺塔问题:

class Solution {
    public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
        if (A.size() > 0) {
            hanota(A.size(), A, B, C);
        }
    }

    private void hanota(int n, List<Integer> A, List<Integer> B, List<Integer> C) {
        // 递归结束条件:n=0就结束
        if (n < 1) {
            return;
        }
        hanota(n - 1, A, C, B);
        C.add(A.remove(A.size() - 1));
        hanota(n - 1, B, A, C);
    }

}

题目:汉诺塔问题比较经典,这里修改一下游戏规则:现在限制不能从最左侧的塔直接移动到最右侧,也不能从最右侧直接移动到最左侧,而是必须经过中间。求当塔有 N 层的时候,打印最优移动过程和最优移动总步数。

例如,当塔数为两层时,最上层的塔记为 1,最下层的塔记为 2,则打印:

Move 1 from left to mid

Move 1 from mid to right

Move 2 from left to mid

Move 1 from right to mid

Move 1 from mid to left

Move 2 from mid to right

Move 1 from left to mid

Move 1 from mid to right

It will move 8 steps.

递归法:

public class HanoiProblem1 {
    // 递归法
    public static int hanoiProblem1(int num, String left, String mid, String right) {
        if (num < 1) {
            return -1;
        }
        // 定义process函数,添加from、to函数
        return process(num, left, mid, right, left, right);
    }

    public static int process(int num, String left, String mid, String right, String from, String to) {
        // 1.递归结束条件
        if (num == 1) {
            if (from.equals("mid") || to.equals("mid")) {
                System.out.println("Move 1 from " + from + " to " + to);
                return 1;
            } else {
                System.out.println("Move 1 from " + from + " to " + mid);
                System.out.println("Move 1 from " + mid + " to " + to);
                return 2;
            }
        }
        // 2.左到中||中到左||右到中||中到右
        if (from.equals("mid") || to.equals("mid")) {
            String another = (from.equals("left") | to.equals("left")) ? right : left;
            int part1 = process(num - 1, left, mid, right, from, another);
            int part2 = 1;
            System.out.println("Move " + num + " from " + from + " to " + to);
            int part3 = process(num - 1, left, mid, right, another, to);
            return part1 + part2 + part3;
        } else {
            int part1 = process(num - 1, left, mid, to, from, to);
            int part2 = 1;
            System.out.println("Move " + num + " from " + from + " to " + mid);
            int part3 = process(num - 1, left, mid, right, to, from);
            int part4 = 1;
            System.out.println("Move " + num + " from " + mid + " to " + to);
            int part5 = process(num - 1, left, mid, to, from, to);
            return part1 + part2 + part3 + part4 + part5;
        }
    }
}

非递归法:

public class HanoiProblem2 {

    public static int hanoiProblem2(int num, String left, String mid, String right) {
        Stack<Integer> ls = new Stack<>();
        Stack<Integer> ms = new Stack<>();
        Stack<Integer> rs = new Stack<>();
        // 3个栈都加入最大值, 保证fStack.peek() < tStack.peek()不发生异常
        ls.push(Integer.MAX_VALUE);
        ms.push(Integer.MAX_VALUE);
        rs.push(Integer.MAX_VALUE);
        for (int i = num; i > 0; i--) {
            ls.push(i);
        }
        Action[] record = {Action.No};
        int step = 0;
        while (rs.size() <=num) {
            step += fStackToStack(record, Action.MToL, Action.LToM, ls, ms, left, mid);
            step += fStackToStack(record, Action.LToM, Action.MToL, ms, ls, mid, left);
            step += fStackToStack(record, Action.RToM, Action.MToR, ms, rs, mid, right);
            step += fStackToStack(record, Action.MToR, Action.RToM, rs, ms, right, mid);
        }
        return step;
    }

    // 定义行为函数
    public enum Action {
        No, LToM, MToL, MToR, RToM
    }

    // 出入栈函数
    public static int fStackToStack(Action[] record, Action preNoAct, Action nowAct,
                                    Stack<Integer> fStack, Stack<Integer> tStack,
                                    String from, String to) {
        // 不相邻原则 && 小压大原则
        if (record[0] != preNoAct && fStack.peek() < tStack.peek()) {
            // 满足条件就入栈,打印
            tStack.push(fStack.pop());
            System.out.println("Move " + tStack.peek() + " from " + from + " to " + to);
            // 更改行为
            record[0] = nowAct;
            return 1;
        }
        return 0;
    }

    public static void main(String[] args) {
        int step = hanoiProblem2(2, "left", "mid", "right");
        System.out.println("It will move " + step + " steps");
    }

}

滑动窗口最大值数组

题目:有一个整型数组 arr 和一个大小为 w 的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。

方法:使用双端队列qmax,标记最大值数组,长度为n-w+1

  1. arr[qMax.peekLast()] > arr[i]入队列
  2. arr[qMax.peekLast()] <= arr[i]末尾出队列
  3. qMax.peekFirst() == i - w时,滑动窗口开头索引后移一位,qMax队头出队列
  4. i >= w - 1时,就要将队头元数入结果集
public static int[] getMaxWindow(int[] arr, int w) {
    if (arr == null || w < 1 || arr.length < w) {
        return null;
    }
    // qMax保存最大值下标
    LinkedList<Integer> qMax = new LinkedList<>();
    // res长度:n-w+1个窗口最大值
    int[] res = new int[arr.length - w + 1];
    int index = 0;
    for (int i = 0; i < arr.length; i++) {
        // 队中索引的值<=数组的值,就出队尾元素
        while (!qMax.isEmpty() && arr[qMax.peekLast()] <= arr[i]) {
            qMax.pollLast();
        }
        // 队中索引的值>数组的值,就入队尾
        qMax.addLast(i);
        // 移动qMax窗口:从i-w开始,队头就得出一个
        if (qMax.peekFirst() == i - w) {
            qMax.pollFirst();
        }
        // 从w-1索引开始,最大值需要加入结果集res中
        if (i >= w - 1) {
            res[index++] = arr[qMax.peekFirst()];
        }
    }
    return res;
}

力扣239:同一题,改下参数名即可

class Solution {
    // 剑指 Offer 59 - I. 滑动窗口的最大值
    // 239. 滑动窗口最大值
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length < k || nums.length < 1 || k < 1) {
            return new int[]{};
        }
        LinkedList<Integer> q = new LinkedList<Integer>();
        int[] res = new int[nums.length - k + 1];
        int index = 0;
        for (int i = 0; i < nums.length; i++) {
            while (!q.isEmpty() && nums[q.peekLast()] <= nums[i]) {
                q.pollLast();
            }
            q.addLast(i);
            if (q.peekFirst() == i - k) {
                q.pollFirst();
            }
            if (i >= k - 1) {
                res[index++] = nums[q.peekFirst()];
            }
        }
        return res;
    }
}

单调栈

原题目:给定一个不含有重复值的数组 arr,找到每一个 i 位置左边和右边离 i 位置最近且值比 arr[i]小的位置。返回所有位置相应的信息。

arr = {3,4,1,5,6,2,7}
返回如下二维数组作为结果:
{ 
 {-1, 2},
 { 0, 2},
 {-1,-1},
 { 2, 5},
 { 3, 5},
 { 2,-1},
 { 5,-1}
}

方法:原数组无重复元素,使用一个栈,存放数组索引,栈顶元素为j,带入栈的元素是i(=i是遍历数组的指针)

  1. arr[stack[j]]<arr[i]就入栈
  2. arr[stack[j]]>=arr[i]就出栈顶元素,直到满足1的条件停止再入栈
  3. 怎么记?保证栈中元素从栈顶到栈底的指针在原数组中的值是严格递减的,自己想象一个栈画面即可
// 给定一个不重复的数组,找到左右最靠近小于它的下标
public static int[][] getNearLessNoRepeat(int[] arr) {
    Stack<Integer> stack = new Stack<>();
    int[][] res = new int[arr.length][2];
    for (int i = 0; i < arr.length; i++) {
        // 遍历辅助栈,peek()>arr[i]就要出栈
        while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
            int popIndex = stack.pop();
            int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
            res[popIndex][0] = leftLessIndex;
            res[popIndex][1] = i;
        }
        stack.push(i);
    }
    while (!stack.isEmpty()) {
        int popIndex = stack.pop();
        int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
        res[popIndex][0] = leftLessIndex;
        // 遍历辅助栈时,因为无数再入,所以值为-1
        res[popIndex][1] = -1;
    }
    return res;
}

进阶问题:如果原数组含有重复数组,怎么办?

方法:整体方法与上面一致,只是栈中存储链表,相同元素的就用链表依次链接下去,出栈的时候出链表末尾

// 给定一个重复的数组,找到左右最靠近小于它的下标
public static int[][] getNearLessNoRepeat(int[] arr) {
    int[][] res = new int[arr.length][2];
    Stack<List<Integer>> stack = new Stack<>();
    for (int i = 0; i < arr.length; i++) {
        // 大于
        while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
            List<Integer> popList = stack.pop();
            int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
            for (Integer popValue : popList) {
                res[popValue][0] = leftLessIndex;
                res[popValue][1] = i;
            }
        }
        // 等于
        if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
            stack.peek().add(i);
        } else {// 小于
            LinkedList<Integer> list = new LinkedList<>();
            list.add(i);
            stack.push(list);
        }
    }
    // 栈不空,就遍历出栈
    while (!stack.isEmpty()) {
        List<Integer> popList = stack.pop();
        int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.size() - 1);
        for (Integer popValue : popList) {
            res[popValue][0] = leftLessIndex;
            res[popValue][1] = -1;
        }
    }
    return res;
}

求最大子矩阵的面积

题目:给定一个整型矩阵 map,其中的值只有 0 和 1 两种,求其中全是 1 的所有矩形区域中,最大的矩形区域为 1 的数量。

1 0 1 1 

1 1 1 1 

1 1 1 0

其中,最大的矩形区域有 6 个 1,所以返回 6。

方法:使用一个height数组保存数组每一行的1个数;使用一个单调栈保存索引,遍历指针i,栈顶元素j,弹出后的栈顶元素k

  1. height[j]<height[i]入栈
  2. height[j]>=height[i]就出栈顶元素,直到满足1的条件停止再入栈
  3. 由于是栈顶到栈底单调栈性质,所以j左边能构成最大面积起始位置是k+1,右边能构成最大面试终点位置是i-1,整体面积大小是[i-1-(k+1)]*height[j]
    在这里插入图片描述
public static int maxRecSize(int[][] map) {
    if (map == null || map.length == 0 || map[0].length == 0) {
        return 0;
    }
    int maxArea = 0;
    int[] height = new int[map[0].length];
    for (int i = 0; i < map.length; i++) {
        for (int j = 0; j < map[0].length; j++) {
            height[j] = map[i][j] == 0 ? 0 : height[j] + 1;
        }
        maxArea = Math.max(maxRecFromBottom(height), maxArea);
    }
    return maxArea;
}

public static int maxRecFromBottom(int[] height) {
    if (height == null || height.length == 0) {
        return 0;
    }
    int maxArea = 0;
    Stack<Integer> stack = new Stack<>();
    // 遍历h数组,使用单调栈判断业务逻辑
    for (int i = 0; i < height.length; i++) {
        // 1.单调栈,先写出栈的逻辑:h[i]<=h[j]就不断出栈
        while (!stack.isEmpty() && height[i] <= height[stack.peek()]) {
            int j = stack.pop();
            int k = stack.isEmpty() ? -1 : stack.peek();
            int curArea = (i - k - 1) * height[j];
            maxArea = Math.max(curArea, maxArea);
        }
        stack.push(i);
    }
    // 遍历完h数组后,如果栈中还有元素,就出栈
    while (!stack.isEmpty()) {
        int j = stack.pop();
        int k = stack.isEmpty() ? -1 : stack.peek();
        int curArea = (height.length - k - 1) * height[j];
        maxArea = Math.max(curArea, maxArea);
    }
    return maxArea;
}

力扣85:最大数组。参数是字符数组,转化成整型数组就行,其余问题一样

public class Solution {
    public int maximalRectangle(char[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return 0;
        }
        int[][] map = new int[matrix.length][matrix[0].length];
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[0].length; j++) {
                map[i][j] = Integer.parseInt(String.valueOf(matrix[i][j]));
            }
        }
        int maxArea = 0;
        int[] height = new int[map[0].length];
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[0].length; j++) {
                height[j] = map[i][j] == 0 ? 0 : height[j] + 1;
            }
            maxArea = Math.max(maxRecFromBottom(height), maxArea);
        }
        return maxArea;
    }

    public int maxRecFromBottom(int[] height) {
        if (height == null || height.length == 0) {
            return 0;
        }
        int maxArea = 0;
        Stack<Integer> stack = new Stack<>();
        // 遍历h数组,使用单调栈判断业务逻辑
        for (int i = 0; i < height.length; i++) {
            // 1.单调栈,先写出栈的逻辑:h[i]<=h[j]就不断出栈
            while (!stack.isEmpty() && height[i] <= height[stack.peek()]) {
                int j = stack.pop();
                int k = stack.isEmpty() ? -1 : stack.peek();
                int curArea = (i - k - 1) * height[j];
                maxArea = Math.max(curArea, maxArea);
            }
            stack.push(i);
        }
        // 遍历完h数组后,如果栈中还有元素,就出栈
        while (!stack.isEmpty()) {
            int j = stack.pop();
            int k = stack.isEmpty() ? -1 : stack.peek();
            int curArea = (height.length - k - 1) * height[j];
            maxArea = Math.max(curArea, maxArea);
        }
        return maxArea;
    }
}

最大值减去最小值<=num的子数组数量

题目:给定数组 arr 和整数 num,共返回有多少个子数组满足如下情况:

max(arr[i..j]) - min(arr[i..j]) <= num

max(arr[i..j])表示子数组 arr[i..j]中的最大值,min(arr[i..j])表示子数组 arr[i..j]中的最小值。

方法:

  1. 生成两个双端队列 qmax 和 qmin,含义如上文所说。生成两个整型变量 ij,表示子数组的范围,即 arr[i..j]。生成整型变量 res,表示所有满足条件的子数组数量。

  2. j 不断向右移动(j++),表示 arr[i..j]一直向右扩大,并不断更新 qmax 和 qmin 结构,保证 qmax 和 qmin 始终维持动态窗口最大值和最小值的更新结构。一旦出现 arr[i..j]不满足条件的情况,j 向右扩的过程停止,此时 arr[i..j-1]、arr[i..j-2]、arr[i..j-3]...arr[i..i]一定都是满足条件的。也就是说,所有必须以 arr[i]作为第一个元素的子数组,满足条件的数量为 j-i 个。于是令 res+=j-i。

  3. 当进行完步骤 2,令 i 向右移动一个位置,并对 qmax 和 qmin 做出相应的更新,qmax和 qmin 从原来的 arr[i..j]窗口变成 arr[i+1..j]窗口的最大值和最小值的更新结构。然后重复步骤 2,也就是求所有必须以 arr[i+1]作为第一个元素的子数组中,满足条件的数量有多少个。

  4. 根据步骤 2 和步骤 3,依次求出:必须以 arr[0]开头的子数组,满足条件的数量有多少个;必须以 arr[1]开头的子数组,满足条件的数量有多少个;必须以 arr[2]开头的子数组,满足条件的数量有多少个,全部累加起来就是答案。上述过程中,所有的下标值最多进 qmax 和 qmin 一次,出 qmax 和 qmin 一次。i 和 j 的值也不断增加,并且从来不减小。所以整个过程的时间复杂度为 O(N)。

public int getNum(int[] arr, int num) {
    if (arr == null || arr.length == 0 || num < 0) {
        return 0;
    }
    LinkedList<Integer> qMax = new LinkedList<>();
    LinkedList<Integer> qMin = new LinkedList<>();
    int i = 0, j = 0;
    int res = 0;
    while (i < arr.length) {
        while (j < arr.length) {
            if (qMin.isEmpty() || qMin.peekLast() != j) {
                while (!qMin.isEmpty() && arr[qMin.peekLast()] >= arr[j]) {
                    qMin.pollLast();
                }
                qMin.add(j);
                while (!qMax.isEmpty() && arr[qMax.peekLast()] <= arr[j]) {
                    qMax.pollLast();
                }
                qMax.add(j);
            }
            if (qMax.peekFirst() - qMin.peekFirst() > num) {
                break;
            }
            j++;
        }
        // j++以后跳出循环,所以不用+1,直接j-i就行
        res += j - i;
        if (qMin.peekFirst() == i) {
            qMin.pollFirst();
        }
        if (qMax.peekFirst() == i) {
            qMax.pollFirst();
        }
        i++;
    }
    return res;
}

可见的山峰数量

原题目:给定一个不含有负数且没有重复值的数组 arr,请返回有多少对山峰能够相互看见。

答案:数学推导,假设环形结构中有i座山峰(i>2),可见山峰数为2*i-3

在这里插入图片描述
在这里插入图片描述

进阶问题:给定一个不含有负数但可能含有重复值的数组 arr,返回有多少对山峰能够相互看见。

方法:

  1. 定义一个Reord类,保存数组值和该元素出现的次数/山峰数
  2. 遍历arr,得出最大值索引,Record(arr[maxIndex],1)放进一个栈里
  3. 定义nextIndex方法获取maxIndex的逆时针索引。逆时针遍历arr
    1. stack.peek().value < arr[index],弹出栈,计算山峰数:栈顶*2+C(2,次数)
    2. stack.peek().value == arr[index],栈顶元素次数加1
    3. stack.peek().value > arr[index],正常入栈
  4. 栈非空,就清算栈里元素,k为栈顶元素次数
    1. stack.size() > 2 ,循环操作。res+=2*k+C(2,k)
    2. stack.size() == 2,只有一次操作。k=1,res+=k;k>1,res+=k*2+C(2,k)
    3. stack.size() > 2,必然存在的操作,因为栈底是最大值。k=1,res+=0;k>1,res+=C(2,k)
public static int getVisibleNum(int[] arr) {
    if (arr == null || arr.length < 2) {
        return 0;
    }
    int size = arr.length;
    int maxIndex = 0;
    // 现在环中找到一个最大值,哪个索引都行
    for (int i = 0; i < size; i++) {
        maxIndex = arr[maxIndex] < arr[i] ? i : maxIndex;
    }
    // 单调栈:要求从栈底到栈顶严格递减
    Stack<Record> stack = new Stack<>();
    // 将(最大值,1)放入栈底
    stack.push(new Record(arr[maxIndex]));
    // 从最大值的逆时针开始遍历
    int index = nextIndex(maxIndex, size);
    int res = 0;
    while (index != maxIndex) {
        // >就出栈
        while (stack.peek().value < arr[index]) {
            int k = stack.pop().times;
            res += 2 * k + getInternalSum(k);
        }
        // =就次数加1
        if (stack.peek().value == arr[index]) {
            stack.peek().times++;
        } else {
            // >就入栈
            stack.push(new Record(arr[index]));
        }
        index = nextIndex(index, size);
    }
    // 清算第1阶段:弹出的记录不是栈中倒数2个位置
    while (stack.size() > 2) {
        int k = stack.pop().times;
        res += 2 * k + getInternalSum(k);
    }
    // 清算第2阶段:弹出的记录是倒数1个位置
    while (stack.size() == 2) {
        int k = stack.pop().times;
        // 以下的stack.peek().times就是栈底元素了
        res += (stack.peek().times == 1 ? k : k * 2) + getInternalSum(k);
    }
    // 清算第3阶段:弹出的记录是栈底
    res += getInternalSum(stack.peek().times);
    return res;
}

public static int getInternalSum(int k) {
    // k>1:C(2,k)= (k * (k - 1) / 2)
    return k == 1 ? 0 : (k * (k - 1) / 2);
}

// 环形数组中当前位置为i,返回i的下一个索引
private static int nextIndex(int i, int size) {
    return i < (size - 1) ? i + 1 : 0;
}

补充题

力扣20:有效的括号。

题目:给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

输入: "(]"
输出: false

方法:

  1. 使用一个栈,遍历字符串,遇到左边括号就入栈
  2. 否则栈非空,就出栈一个元素,如果不匹配当前遍历字符,返回false;栈空,也返回false
public class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '(' || c == '{' || c == '[') {
                stack.push(c);
            } else {
                if (stack.isEmpty()) {
                    return false;
                }
                Character pop = stack.pop();
                if (c == ')' && pop != '(') {
                    return false;
                }
                if (c == '}' && pop != '{') {
                    return false;
                }
                if (c == ']' && pop != '[') {
                    return false;
                }
            }
        }
        return stack.isEmpty();
    }
}

力扣71:简化路径

题目:以 Unix 风格给出一个文件的绝对路径,你需要简化它。或者换句话说,将其转换为规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。

输入:"/home/"
输出:"/home"
解释:注意,最后一个目录名后面没有斜杠。

输入:"/../"
输出:"/"
解释:从根目录向上一级是不可行的,因为根是你可以到达的最高级。 

输入:"/home//foo/"
输出:"/home/foo"
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。

输入:"/a/../../b/../c//.//"
输出:"/c"

输入:"/a//b////c/d//././/.."
输出:"/a/b/c"
public class Solution {
    public String simplifyPath(String path) {
        // 1.spilt分字符,然后遍历字符数组
        String[] splits = path.split("/");
        Stack<String> stack = new Stack<>();
        for (String s : splits) {
            // 2.trim分割当前元素的前后空格
            String cur = s.trim();
            // 3.如果当前字符为空||为.,跳出当前循环
            if (cur == null || cur.length() == 0 || cur.equals(".")) {
                continue;
            }
            // 4.当前元素是..,非空栈就得出栈元素
            if (cur.equals("..")) {
                if (!stack.isEmpty()) {
                    stack.pop();
                }
            } else { // 5.遍历到其他元素就入栈
                stack.push(cur);
            }
        }
        // 6.出栈,构成结果
        StringBuilder res = new StringBuilder();
        while (!stack.isEmpty()) {
            res.insert(0, "/" + stack.pop());
        }
        // 7 res为空,返回”/“,否则返回res.toString
        return res.length() == 0 ? "/" : res.toString();
    }
}