《程序员代码面试指南》第一章 栈和队列
第一章 栈和队列
设计一个有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 中的数据全部压入。
- 1
5 依次压入 stackPush,stackPush 的栈顶到栈底为 51,从 stackPush压入 stackPop 时,只将 5 和 4 压入了 stackPop,stackPush 还剩下 1、2、3 没有压入。此时如果用户想进行弹出操作,那么 4 将最先弹出,与预想的队列顺序就不一致。
2.如果 stackPop 不为空,stackPush 绝对不能向 stackPop 中压入数据。违反了以上两点都会发生错误。
- 违反 2 的情况举例:1
5 依次压入 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
- arr[qMax.peekLast()] > arr[i]入队列
- arr[qMax.peekLast()] <= arr[i]末尾出队列
- qMax.peekFirst() == i - w时,滑动窗口开头索引后移一位,qMax队头出队列
- 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是遍历数组的指针)
- arr[stack[j]]<arr[i]就入栈
- arr[stack[j]]>=arr[i]就出栈顶元素,直到满足1的条件停止再入栈
- 怎么记?保证栈中元素从栈顶到栈底的指针在原数组中的值是严格递减的,自己想象一个栈画面即可
// 给定一个不重复的数组,找到左右最靠近小于它的下标 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
- height[j]<height[i]入栈
- height[j]>=height[i]就出栈顶元素,直到满足1的条件停止再入栈
- 由于是栈顶到栈底单调栈性质,所以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]中的最小值。
方法:
生成两个双端队列 qmax 和 qmin,含义如上文所说。生成两个整型变量 i 和 j,表示子数组的范围,即 arr[i..j]。生成整型变量 res,表示所有满足条件的子数组数量。
令 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。
当进行完步骤 2,令 i 向右移动一个位置,并对 qmax 和 qmin 做出相应的更新,qmax和 qmin 从原来的 arr[i..j]窗口变成 arr[i+1..j]窗口的最大值和最小值的更新结构。然后重复步骤 2,也就是求所有必须以 arr[i+1]作为第一个元素的子数组中,满足条件的数量有多少个。
根据步骤 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,返回有多少对山峰能够相互看见。
方法:
- 定义一个Reord类,保存数组值和该元素出现的次数/山峰数
- 遍历arr,得出最大值索引,Record(arr[maxIndex],1)放进一个栈里
- 定义nextIndex方法获取maxIndex的逆时针索引。逆时针遍历arr
- stack.peek().value < arr[index],弹出栈,计算山峰数:栈顶*2+C(2,次数)
- stack.peek().value == arr[index],栈顶元素次数加1
- stack.peek().value > arr[index],正常入栈
- 栈非空,就清算栈里元素,k为栈顶元素次数
- stack.size() > 2 ,循环操作。res+=2*k+C(2,k)
- stack.size() == 2,只有一次操作。k=1,res+=k;k>1,res+=k*2+C(2,k)
- 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
方法:
- 使用一个栈,遍历字符串,遇到左边括号就入栈
- 否则栈非空,就出栈一个元素,如果不匹配当前遍历字符,返回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(); } }