一 设计一个有getMin功能的栈
实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作。
【要求】1.pop、push、getMin操作的时间复杂度都是O(1)。
2.设计的栈类型可以使用现成的栈结构。
public class SpecialStack {
private Stack<Integer> dataStack;
private Stack<Integer> minStack;
public SpecialStack() {
dataStack = new Stack<>();
minStack = new Stack<>();
}
public void push(int value) {
dataStack.push(value); // 将元素压入数据栈
if (minStack.isEmpty() || value <= minStack.peek()) {
minStack.push(value); // 如果当前元素小于等于辅助栈栈顶元素,则将当前元素压入辅助栈
} else {
minStack.push(minStack.peek()); // 如果当前元素大于辅助栈栈顶元素,则将辅助栈的栈顶元素再次压入辅助栈
}
}
public int pop() {
if (dataStack.isEmpty()) {
throw new RuntimeException("Stack is empty");
}
minStack.pop(); // 辅助栈同步pop
return dataStack.pop(); // 数据栈pop
}
public int getMin() {
if (minStack.isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return minStack.peek(); // 返回辅助栈的栈顶元素
}
}
要实现一个特殊的栈,其中pop、push和getMin操作的时间复杂度都是O(1),我们可以使用两个栈来实现。
具体设计如下:
-
使用一个数据栈来存储栈中的元素。
-
使用一个辅助栈来存储当前数据栈中的最小元素。
在push操作中,将元素压入数据栈时,比较当前元素与辅助栈的栈顶元素,如果小于等于辅助栈的栈顶元素,则将当前元素也压入辅助栈;如果大于辅助栈的栈顶元素,则将辅助栈的栈顶元素再次压入辅助栈。
在pop操作中,同时对数据栈和辅助栈进行pop操作,以确保两个栈中的元素同步。
getMin操作直接返回辅助栈的栈顶元素即可。
二 由两个栈组成的队列
【题目】编写一个类,用两个栈
/*
* 必须满足两个条件
* stackPush要向stackPop中压入数据,必须一次性把stackPush中的数据全部压入。
* 如果stackPop不为空,stackPush绝不能向StackPop中压入数据。
*/
class MyQueue232 {
Stack<Integer> stack1 = new Stack<>(); // 用于输入流的栈
Stack<Integer> stack2 = new Stack<>(); // 用于输出流的栈
/** Initialize your data structure here. */
public MyQueue232() {
// 构造函数,初始化Queue
}
/** Push element x to the back of queue. */
public void push(int x) {
stack1.push(x); // 直接将元素压入输入流栈
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
if (stack2.isEmpty()) { // 如果输出流栈为空
while (!stack1.isEmpty()) {
stack2.push(stack1.pop()); // 将输入流栈的元素依次弹出并压入输出流栈,实现翻转顺序
}
}
return stack2.pop(); // 弹出输出流栈的栈顶元素,即队列的头部元素
}
/** Get the front element. */
public int peek() {
if (stack2.isEmpty()) { // 如果输出流栈为空
while (!stack1.isEmpty()) {
stack2.push(stack1.pop()); // 将输入流栈的元素依次弹出并压入输出流栈,实现翻转顺序
}
}
return stack2.peek(); // 返回输出流栈的栈顶元素,即队列的头部元素,但不弹出
}
/** Returns whether the queue is empty. */
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty(); // 如果输入流栈和输出流栈都为空,队列为空
}
}
实现队列,支持队列的基本操作(add、poll、peek)。
三 如何仅用递归函数和栈操作逆序一个栈
【题目】一个栈依次压入1、2、3、4、5,那么从栈顶到栈底分别为5、4、3、2、1。将这个栈转置后,从栈顶到栈底为 1、2、3、4、5,也就是实现栈中元素的逆序,但是只能用递归函数来实现,不能用其他数据结构。
/**
* 递归函数,用于获取并移除栈中的最后一个元素
* @param stack 要操作的栈
* @return 移除的最后一个元素
*/
private static int getAndRemoveLastElement(Stack<Integer> stack) {
// 弹出栈顶元素
int result = stack.pop();
// 如果栈为空,说明已经处理完所有元素,直接返回结果
if (stack.isEmpty()) {
return result;
} else {
// 递归调用,获取并移除剩余元素的最后一个元素
int last = getAndRemoveLastElement(stack);
// 将之前弹出的栈顶元素重新压入栈
stack.push(result);
// 返回最后一个元素
return last;
}
}
/**
* 递归函数,用于将栈逆序
* @param stack 要操作的栈
*/
public static void reverse(Stack<Integer> stack) {
// 如果栈为空,说明已经处理完所有元素,直接返回
if (stack.isEmpty()) {
return;
}
// 获取并移除栈中的最后一个元素
int i = getAndRemoveLastElement(stack);
// 递归调用,将剩余元素逆序
reverse(stack);
// 将最后一个元素压入栈,实现逆序
stack.push(i);
}
以上代码使用递归函数实现了将栈逆序的操作。其中,getAndRemoveLastElement
函数用于获取并移除栈中的最后一个元素,通过递归调用实现。而 reverse
函数则用于将栈逆序,首先获取并移除栈中的最后一个元素,然后递归调用 reverse
函数对剩余元素进行逆序操作,最后将最后一个元素重新压入栈,实现逆序。
四 猫狗队列
【题目】宠物、狗和猫的类如下:
实现一种狗猫队列的结构,要求如下:
● 用户可以调用add方法将cat类或dog类的实例放入队列中;
● 用户可以调用pollAll方法,将队列中所有的实例按照进队列的先后顺序依次弹出;
● 用户可以调用pollDog方法,将队列中dog类的实例按照进队列的先后顺序依次弹出;
● 用户可以调用pollCat方法,将队列中cat类的实例按照进队列的先后顺序依次弹出;
● 用户可以调用isEmpty方法,检查队列中是否还有dog或cat的实例;
● 用户可以调用isDogEmpty方法,检查队列中是否有dog类的实例;
● 用户可以调用isCatEmpty方法,检查队列中是否有cat类的实例。
public class Pet {
private String type; // 定义宠物类型
public Pet(String type) {
this.type = type;
}
public String getPetType() { // 获取宠物类型的方法
return type;
}
}
public class Dog extends Pet { // Dog 类继承自 Pet 类
public Dog() {
super("dog"); // 调用父类的构造方法,设置宠物类型为 "dog"
}
}
public class Cat extends Pet { // Cat 类继承自 Pet 类
public Cat() {
super("cat"); // 调用父类的构造方法,设置宠物类型为 "cat"
}
}
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; // 计数器初始化为 0
}
public void add(Pet pet) {
if (pet.getPetType().equals("dog")) { // 如果是狗
this.dogQ.add(new PetEnterQueue(pet, this.count++)); // 将狗加入狗队列中,同时更新计数器
} else if (pet.getPetType().equals("cat")) { // 如果是猫
this.catQ.add(new PetEnterQueue(pet, this.count++)); // 将猫加入猫队列中,同时更新计数器
} else {
throw new RuntimeException("error,not dog or cat"); // 如果不是狗也不是猫,抛出异常
}
}
public Pet pollAll() {
if (!this.dogQ.isEmpty() && !this.catQ.isEmpty()) { // 如果狗队列和猫队列都不为空
if (this.dogQ.peek().getCount() < this.catQ.peek().getCount()) { // 如果狗队列中的第一个元素进入队列的顺序在前
return this.dogQ.poll().getPet(); // 弹出狗队列中的第一个元素(最早进入队列的狗)
} else {
return this.catQ.poll().getPet(); // 弹出猫队列中的第一个元素(最早进入队列的猫)
}
} else if (!this.dogQ.isEmpty()) { // 如果只有狗队列不为空
return this.dogQ.poll().getPet(); // 弹出狗队列中的第一个元素
} else if (!this.catQ.isEmpty()) { // 如果只有猫队列不为空
return this.catQ.poll().getPet(); // 弹出猫队列中的第一个元素
} else {
throw new RuntimeException("err,queue is empty"); // 如果狗队列和猫队列都为空,抛出异常
}
}
public Dog polldog() {
if (!this.dogQ.isEmpty()) { // 如果狗队列不为空
return (Dog) this.dogQ.poll().getPet(); // 弹出狗队列中的第一个元素,并将其转换为 Dog 对象
} else {
throw new RuntimeException("Dog Queue is empty!"); // 如果狗队列为空,抛出异常
}
}
public Cat pollcat() {
if (!this.catQ.isEmpty()) { // 如果猫队列不为空
return (Cat) this.catQ.poll().getPet(); // 弹出猫队列中的第一个元素,并将其转换
}else {
throw new RuntimeException("Cat Queue is empty!");
}
}
public boolean isEmpty() {
return this.dogQ.isEmpty() && this.catQ.isEmpty();
}
public boolean isDogQueueEmpty() {
return this.dogQ.isEmpty();
}
public boolean isCatQueueEmpty() {
return this.catQ.isEmpty();
}
五 用一个栈实现另一个栈的排序
【题目】一个栈中元素的类型为整型,现在想将该栈从顶到底按从大到小的顺序排序,只许申请一个栈。除此之外,可以申请新的变量,但不能申请额外的数据结构。如何完成排序?
public static void sortStackByStack(Stack<Integer> stack) {
// 创建辅助栈
Stack<Integer> help = new Stack<Integer>();
// 遍历原栈内元素,挨个弹出
while (!stack.isEmpty()) {
int cur = stack.pop(); // 弹出当前元素
// 帮助排序的循环:
// 1.在辅助栈不为空的前提下
// 2.如果当前弹出的元素比辅助栈顶元素大,那么将辅助栈顶元素弹出,并放回原栈 (放回到原栈的原因是因为该元素还没有排序)
// 重复这两步,直到辅助栈为空或者辅助栈顶元素不再比当前元素小为止
while (!help.isEmpty() && help.peek() < cur) {
stack.push(help.pop());
}
// 将当前元素压入辅助栈
help.push(cur);
}
// 帮助排序完成后,将辅助栈内的元素全部弹出,并依次压入原栈中即可
while (!help.isEmpty()) {
stack.push(help.pop());
}
}
这种方法的时间复杂度为 O(n^2),空间复杂度也为 O(n),其中 n 表示原栈中的元素个数。虽然效率不是很高,但确实只使用一个辅助栈就完成了排序,最节省空间的解法之一。
六 用栈来求解汉诺塔问题
【题目】
汉诺塔问题比较经典,这里修改一下游戏规则:现在限制不能从最左侧的塔直接移动到最右侧,也不能从最右侧直接移动到最左侧,而是必须经过中间。求当塔有N层的时候,打印最优移动过程和最优移动总步数。例如,当塔数为两层时,最上层的塔记为1,最下层的塔记为2,则打印:
递归解法:
public int moveHanoi1(int n, String left, String mid, String right) {
// 如果n小于1,没有盘子需要移动,直接返回0
if (n < 1) {
return 0;
}
// 调用辅助方法moveHanoi1进行递归求解
return moveHanoi1(n, left, mid, right, left, right);
}
// 递归实现
public int moveHanoi1(int n, String left, String mid, String right, String from, String to) {
// 如果只有一个盘子
if (n == 1) {
// 如果起始柱或目标柱为中间柱
if (MID.equalsIgnoreCase(from) || MID.equalsIgnoreCase(to)) {
// 直接从起始柱移动到目标柱
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 (MID.equalsIgnoreCase(from) || MID.equalsIgnoreCase(to)) {
// 另一个柱子为左柱或右柱
String another = LEFT.equalsIgnoreCase(to) || LEFT.equalsIgnoreCase(from) ? RIGHT : LEFT;
// 递归将n-1个盘子从起始柱移动到另一个柱子
int step1 = moveHanoi1(n - 1, left, mid, right, from, another);
// 将最大的盘子从起始柱移动到目标柱
int step2 = 1;
System.out.println("move " + n + " from " + from + " to " + to);
// 递归将n-1个盘子从另一个柱子移动到目标柱
int step3 = moveHanoi1(n - 1, left, mid, right, another, to);
// 返回总共移动的步数
return step1 + step2 + step3;
} else { // 起始柱和目标柱都不包括中间柱
// 递归将n-1个盘子从起始柱移动到目标柱
int step1 = moveHanoi1(n - 1, left, mid, right, from, to);
// 将最大的盘子从起始柱移动到中间柱
int step2 = 1;
System.out.println("move " + n + " from " + from + " to mid");
// 递归将n-1个盘子从目标柱移动到起始柱
int step3 = moveHanoi1(n - 1, left, mid, right, to, from);
// 将最大的盘子从中间柱移动到目标柱
int step4 = 1;
System.out.println("move " + n + " from mid to " + to);
// 递归将n-1个盘子从起始柱移动到目标柱
int step5 = moveHanoi1(n - 1, left, mid, right, from, to);
// 返回总共移动的步数
return step1 + step2 + step3 + step4 + step5;
}
}
方法二:非递归的方法--用栈来模拟整个过程。
enum Action {
NO, LM, ML, MR, RM;
}
public int moveHanoi(int n, String left, String mid, String right) {
// 创建左、中、右三个柱子的栈
Stack<Integer> leftStack = new Stack<Integer>();
Stack<Integer> midStack = new Stack<Integer>();
Stack<Integer> rightStack = new Stack<Integer>();
// 在栈底添加一个最大值,作为哨兵
leftStack.push(Integer.MAX_VALUE);
midStack.push(Integer.MAX_VALUE);
rightStack.push(Integer.MAX_VALUE);
// 将n个盘子从大到小依次压入左柱的栈中
for (int i = n; i > 0; i--) {
leftStack.push(i);
}
int step = 0; // 记录移动步数
Action[] action = { Action.NO }; // 保存上一次移动的动作
// 循环直到右柱的栈中盘子数量为n
while (rightStack.size() != n + 1) {
// 将左柱的盘子移动到中柱
step += moveHanoi(action, Action.ML, Action.LM, leftStack, midStack, LEFT, MID);
// 将中柱的盘子移动到左柱
step += moveHanoi(action, Action.LM, Action.ML, midStack, leftStack, MID, LEFT);
// 将中柱的盘子移动到右柱
step += moveHanoi(action, Action.RM, Action.MR, midStack, rightStack, MID, RIGHT);
// 将右柱的盘子移动到中柱
step += moveHanoi(action, Action.MR, Action.RM, rightStack, midStack, RIGHT, MID);
}
return step; // 返回移动总步数
}
/**
* 根据移动条件执行移动,并返回移动步数
*/
public int moveHanoi(Action[] action, Action preAction, Action curAction, Stack<Integer> fromStack,
Stack<Integer> toStack, String from, String to) {
// 判断上一次移动与当前移动是否允许,以及移动条件是否满足
if (action[0] != preAction && fromStack.peek() < toStack.peek()) {
toStack.push(fromStack.pop()); // 将盘子从起始栈移动到目标栈
System.out.println("move " + toStack.peek() + " from " + from + " to " + to); // 输出移动过程
action[0] = curAction; // 更新上一次移动的动作
return 1; // 返回移动步数1
}
return 0; // 没有移动则返回0步
}
七 生成窗口最大值数组
【题目】
有一个整型数组arr和一个大小为w的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。例如,数组为[4,3,5,4,3,3,6,7],窗口大小为3时:
如果数组长度为n,窗口大小为w,则一共产生n-w+1个窗口的最大值。请实现一个函数。
● 输入:整型数组arr,窗口大小为w。● 输出:一个长度为n-w+1的数组res,res[i]表示每一种窗口状态下的最大值。
以本题为例,结果应该返回{5,5,5,4,6,7}。
import java.util.LinkedList;
import java.util.Arrays;
public class MaxWindow {
/**
* 获取窗口最大值数组
* @param arr 整型数组
* @param w 窗口大小
* @return 一个长度为n-w+1的数组,表示每一种窗口状态下的最大值
*/
private int[] getMaxWindow(int[] arr, int w) {
// 判断参数是否合法
if (arr == null || w < 1 || arr.length < w) {
return null; // 参数错误,返回null
}
LinkedList<Integer> qmax = new LinkedList<>(); // 用于存储窗口中的最大值
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); // 将当前元素的索引添加到队列中
// 如果窗口中的最大值已经过期,则删除队列中的最大值
if (qmax.peekFirst() == i - w) {
qmax.pollFirst(); // 删除队列中已经不在窗口范围内的索引
}
// 如果窗口已经形成,在每次滑动窗口时将窗口中的最大值保存到结果数组中
if (i >= w - 1) {
res[index++] = arr[qmax.peekFirst()]; // 将队列的第一个元素(窗口中的最大值)保存到结果数组中
}
}
return res; // 返回结果数组
}
public static void main(String[] args) {
MaxWindow mw = new MaxWindow();
int[] arr = {4, 3, 5, 4, 3, 3, 6, 7};
int w = 3;
int[] res = mw.getMaxWindow(arr, w);
System.out.println(Arrays.toString(res)); // 输出结果数组
}
}
八 单调栈结构
【题目】给定一个不含有重复值的数组arr,找到每一个i位置左边和右边离i位置最近且值比arr[i]小的位置。返回所有位置相应的信息。
【举例】
-1表示不存在。
所以上面的结果表示在arr中,0位置左边和右边离0位置最近且值比arr[0]小的位置是-1和2;
1位置左边和右边离1位置最近且值比arr[1]小的位置是0和2;
2位置左边和右边离2位置最近且值比arr[2]小的位置是-1和-1
……进阶问题:给定一个可能含有重复值的数组arr,找到每一个i位置左边和右边离i位置最近且值比arr[i]小的位置。返回所有位置相应的信息。
【要求】如果arr长度为N,实现原问题和进阶问题的解法,时间复杂度都达到O(N)。
public static void main(String[] args) {
// 给定数组
int[] arr = { 3, 4, 1, 5, 6, 2, 7 };
// 获取不含重复值的左右最近且比当前值小的位置
int[][] nearLessNoRepeat = getNearLessNoRepeat(arr);
System.out.println(Arrays.deepToString(nearLessNoRepeat));
// 获取包含重复值的左右最近且比当前值小的位置
int[][] nearLessRepeat = getNearLessRepeat(arr);
System.out.println(Arrays.deepToString(nearLessRepeat));
}
public static int[][] getNearLessNoRepeat(int[] arr) {
int[][] res = new int[arr.length][2]; // 存储结果的二维数组
Stack<Integer> stack = new Stack<>(); // 使用栈来记录位置
for (int i = 0; i < arr.length; 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;
res[popIndex][1] = -1; // 右侧最近小于当前值的位置设为-1,表示不存在
}
return res;
}
段代码通过使用栈来解决问题,遍历数组并维护一个递增栈。当遇到一个比栈顶元素小的新元素时,持续弹出栈顶元素,直到栈为空或者栈顶元素不再大于新元素。在这个过程中,每次弹出栈顶元素时,记录该位置的左侧最近小于当前值的位置和右侧最近小于当前值的位置。最后,处理栈中剩余的元素,将其右侧最近小于当前值的位置设为-1,表示不存在。
进阶问题
public static int[][] getNearLessRepeat(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> popIs = stack.pop(); // 弹出栈顶的位置和重复值列表
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1); // 获取左侧最近小于当前值的位置
for (Integer popi : popIs) {
res[popi][0] = leftLessIndex; // 记录左侧最近小于当前值的位置
res[popi][1] = i; // 记录右侧最近小于当前值的位置
}
}
if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
stack.peek().add(i); // 如果当前元素和栈顶元素相等,则将当前元素添加到重复值列表中
} else {
ArrayList<Integer> list = new ArrayList<>(); // 创建新的重复值列表
list.add(i); // 将当前元素添加到新的重复值列表中
stack.push(list); // 将新的重复值列表入栈
}
}
// 处理栈中剩余的元素
while (!stack.isEmpty()) {
List<Integer> popIs = stack.pop();
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
for (Integer popi : popIs) {
res[popi][0] = leftLessIndex;
res[popi][1] = -1; // 右侧最近小于当前值的位置设为-1,表示不存在
}
}
return res;
}
这段代码与之前的解决方案类似,但需要处理数组中的重复值。通过使用栈,在每次找到离当前位置最近且比当前值小的位置时,将其记录在结果数组中。对于重复值,通过使用列表来记录每个重复值的位置。
九 求最大子矩阵的大小
【题目】给定一个整型矩阵map,其中的值只有0和1两种,求其中全是1的所有矩形区域中,最大的矩形区域为1的数量。
下面是给定代码的详细注释:
public static void main(String[] args) {
Chapter1_9 chapter = new Chapter1_9();
int[][] arr = {{1, 0, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 0}};
int res = chapter.maxRecSize(arr);
System.out.print(res + " ");
}
private int maxRecSize(int[][] map) {
// 如果矩阵为空或者行数为0或者列数为0,则返回0
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++) {
// 如果当前位置是0,将对应柱子的高度置为0
// 如果当前位置是1,将对应柱子的高度加1
height[j] = map[i][j] == 0 ? 0 : height[j] + 1;
}
// 使用从底部到顶部的柱状图中最大矩形的算法计算最大矩形面积
maxArea = Math.max(maxRecFromBottom(height), maxArea);
}
return maxArea;
}
private int maxRecFromBottom(int[] height) {
// 如果高度数组为空或者长度为0,则返回0
if (height == null || height.length == 0) {
return 0;
}
int maxArea = 0; // 最大矩形面积
Stack<Integer> stack = new Stack<Integer>(); // 存储柱子索引的栈
for (int i = 0; i < height.length; i++) {
// 如果当前柱子的高度小于等于栈顶柱子的高度,则存在一个以栈顶柱子为高度的矩形
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(maxArea, curArea); // 更新最大矩形面积
}
stack.push(i); // 当前柱子的索引入栈
}
// 处理栈中剩余的柱子
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,共返回有多少个子数组满足如下情况:
【要求】如果数组长度为N,请实现时间复杂度为O(N)的解法。
public static void main(String[] args) {
Chapter1_10 chapter = new Chapter1_10();
int[] arr = {4, 3, 5, 4, 3, 3, 6, 7};
int res = chapter.getNum(arr, 3);
System.out.print(res + " ");
}
public static int getNum(int[] arr,int num) {
// 如果数组为空或者数组长度为0,或者num小于0,直接返回0,没有符合条件的子数组
if (arr == null || arr.length == 0 || num < 0) {
return 0;
}
LinkedList<Integer> qmin = new LinkedList<Integer>(); // 维护一个存储最小值位置的队列
LinkedList<Integer> qmax = new LinkedList<Integer>(); // 维护一个存储最大值位置的队列
int i = 0; // 左指针
int 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.addLast(j); // 将当前位置加入最小值队列
// 更新最大值队列
while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[j]) {
qmax.pollLast();
}
qmax.addLast(j); // 将当前位置加入最大值队列
}
// 判断当前的窗口内最大值和最小值的差是否大于num
if (arr[qmax.getFirst()] - arr[qmin.getFirst()] > num) {
break; // 如果大于num,则退出内循环,右指针不再向右移动
}
j++;
}
res += j - i; // 更新子数组数量
if (qmin.peekFirst() == i) {
qmin.pollFirst(); // 如果左指针对应的位置在最小值队列中的最前端,则将其出队
}
if (qmax.peekFirst() == i) {
qmax.pollFirst(); // 如果左指针对应的位置在最大值队列中的最前端,则将其出队
}
i++; // 左指针右移
}
return res; // 返回符合条件的子数组数量
}
上述代码使用了两个队列qmin
和qmax
来分别维护滑动窗口内的最小值和最大值的位置。
- 外循环中的
i
代表滑动窗口的左指针,初始位置为0,内循环中的j
代表滑动窗口的右指针。 - 在每一轮内循环中,首先判断最小值队列是否为空,如果不为空且队列的最后一个位置不等于
j
,则将最后一个位置移除,直到满足条件。然后将j
加入最小值队列,维护了一个递增的最小值队列。 - 同样,更新最大值队列时,需要先检查最大值队列是否为空以及最后一个位置是否等于
j
,然后将j
加入最大值队列,维护了一个递减的最大值队列。 - 判断当前窗口内的最大值和最小值的差是否大于
num
,如果大于则退出内循环,不再右移右指针j
。 - 更新子数组数量,将当前窗口内的子数组数量
j - i
加入结果res
中。 - 检查左指针
i
对应的位置是否在最小值队列和最大值队列的最前端,如果是,则将其出队。 - 左指针右移,进入下一轮循环。
通过这种滑动窗口的方法,可以计算满足最大值减去最小值小于等于num
的子数组数量。
十一 可见的山峰对数量
【题目】一个不含有负数的数组可以代表一圈环形山,每个位置的值代表山的高度。比如,{3,1,2,4,5}、{4,5,3,1,2}或{1,2,4,5,3}都代表同样结构的环形山。3->1->2->4->5->3方向叫作next方向(逆时针),3->5->4->2->1->3方向叫作last方向(顺时针),如图1-8所示。
图1-8山峰A和山峰B能够相互看见的条件为:1.如果A和B是同一座山,认为不能相互看见。
2.如果A和B是不同的山,并且在环中相邻,认为可以相互看见。比如图1-8中,相邻的山峰对有(1,2)(2,4)(4,5)(3,5)(1,3)。
3.如果A和B是不同的山,并且在环中不相邻,假设两座山高度的最小值为min。如果A通过next方向到B的途中没有高度比min大的山峰,或者A通过last方向到B的途中没有高度比min大的山峰,认为A和B可以相互看见。比如图1-8中,高度为3的山和高度为4的山,两座山的高度最小值为3。3从last方向走向4,中途会遇见5,所以last方向走不通;3从next方向走向4,中途会遇见1和2,但是都不大于两座山高度的最小值3,所以next方向可以走通。有一个能走通就认为可以相互看见。再如,高度为2的山和高度为5的山,两个方向上都走不通,所以不能相互看见。图1-8中所有在环中不相邻,并且能看见的山峰对有(2,3)(3,4)。
给定一个不含有负数但可能含有重复值的数组arr,返回有多少对山峰能够相互看见
public class Record {
public int value; // 记录元素的值
public int times; // 记录元素出现的次数
public Record(int value) {
this.value = value;
this.times = 1;
}
}
public class Chapter1_11 {
public static void main(String[] args) {
Chapter1_11 chapter = new Chapter1_11();
int[] arr = {3, 1, 2, 4, 5, 5, 6, 6, 6};
int res = chapter.getVisibleNum(arr);
System.out.print(res + " ");
}
public 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<Record>();
stack.push(new Record(arr[maxIndex])); // 将最大值的Record对象压入栈中
int index = nextIndex(maxIndex, size);
int res = 0;
// 遍历环中的每个元素
while (index != maxIndex) {
// 如果当前元素大于栈顶元素,则弹出栈顶元素,并计算栈顶元素之前的所有相邻元素能够相互看见的对数
while (!stack.empty() && stack.peek().value < arr[index]) {
int k = stack.pop().times;
res += getInternalSum(k) + 2 * k;
}
// 如果当前元素等于栈顶元素,则将栈顶元素的times加1
if (!stack.empty() && stack.peek().value == arr[index]) {
stack.peek().times++;
} else {
stack.push(new Record(arr[index])); // 否则,将当前元素压入栈中
}
index = nextIndex(index, size); // 更新index
}
// 处理栈中剩余元素
while (stack.size() > 2) {
int times = stack.pop().times;
res += getInternalSum(times) + 2 * times;
}
if (stack.size() == 2) {
int times = stack.pop().times;
res += getInternalSum(times) + (stack.peek().times == 1 ? times : 2 * times);
}
res += getInternalSum(stack.pop().times); // 处理最后一个元素
return res;
}
// 计算给定次数k的山峰对的数量
private int getInternalSum(int k) {
return k == 1 ? 0 : (k * (k - 1) / 2);
}
// 根据当前位置和数组大小获取下一个位置
private int nextIndex(int maxIndex, int size) {
return maxIndex < (size - 1) ? (maxIndex + 1) : 0;
}
}
解题思路:
- 首先,找到数组中的一个最大值,记录其位置,并将其作为起始位置。
- 使用栈来存储不同的山峰。遍历数组,将元素与栈顶元素进行比较。
- 如果当前元素大于栈顶元素,说明栈顶元素被当前元素看见,从栈顶弹出并计算栈顶元素之前的所有相邻元素能够相互看见的对数。
- 如果当前元素等于栈顶元素,说明当前元素和栈顶元素相邻,将栈顶元素的times加1。
- 如果当前元素小于栈顶元素,将当前元素压入栈中。
- 继续遍历数组,直到回到起始位置。
- 最后,处理栈中剩余的元素,计算它们之间能够相互看见的对数。