【算法】Dinner Plate Stacks 餐盘栈
文章目录
- Dinner Plate Stacks 餐盘栈
- 问题描述:
- 分析
- 代码
- Tag
Dinner Plate Stacks 餐盘栈
问题描述:
有一个无限长度的行,从左到右每隔一段就有一个栈,容量是capacity ,编号从0开始。
要求实现一个餐盘的类。
初始化 DinnerPlates(int capacity) - 给出栈的最大容量 capacity
void push(int val) - 将给出的正整数 val 推入 从左往右第一个 没有满的栈。
int pop() - 返回 从右往左第一个 非空栈顶部的值,并将其从栈中删除;如果所有的栈都是空的,请返回 -1。
int popAtStack(int index) - 返回编号 index 的栈顶部的值,并将其从栈中删除;如果编号 index 的栈是空的,请返回 -1。
capacity的范围[1,20000],val的范围[1,20000]
index 的范围[0,100000],最多会对push,pop,popAtStack进行200000次的调用。
分析
首先是暴力的思路,可以初始化一个长度是100000+的数组,数组的每个元素是一个栈。
如果不考虑popAtStack,那么每次push和pop都应该很接近,即push的位置左侧应该都是满的,pop的右侧应该都是空的。
但是因为popAtStack的存在,会使得整个数组可能呈现锯齿状。
为了寻找左起第一个不满的pushind,就需要从0开始遍历,时间复杂度ON。同样的为了找到右起第一个不空的popind,就需要从末尾开始向左遍历,时间复杂度ON.
每次pop,push的时间复杂度ON,而且调用次数是ON,整体时间复杂度ON*N。
而且为了放下所有的栈,开一个100000大小的数组。无论从空间还是时间的角度,在算法界也是相当炸裂的。
对于push来说,它需要关心的是不满的位置。而pop,关心的是不空的。
也就是说push 关心 下标最小的&& 0<=size<cap,pop ,关心 下标最大的&& 0<size<=cap.
如果可以找到快速维护这样特性的数据结构,就可以解决问题。
使用小顶堆存放 每个不满的栈的ind,同时可以使用list存放栈,list的长度就表示了下标最大的栈位置。
push操作时,首先要判断小顶堆是否empty,或者当前的下标,是否超过了目前list的长度。如果超过了,就要把小顶堆清空,这里涉及到一个lazy del。
再判断如果小顶堆空了,说明此时没有不满的栈,就必须新建一个栈插入list尾部,同时将数据入栈,并且判断栈是否不满,条件满足的情况下入小顶堆。
如果小顶堆不空,那么就使用大顶堆当前的栈,将元素入栈,同时判断栈是否满了,如果满了,就出堆。
pop的操作,是弹出最大下标的栈顶。而popAtstatck是弹出指定下标的栈顶,所以2个可以合并。
pop== popAtStack(last),只要构建一个popAtStack(idx)就可以。
因为关心的是都是非空栈,所以需要进行维护list,是list的末尾始终是非空的。
先判断idx的范围,idx的范围必须满足[0,list.size()-1].
idx合法,再判断栈是否空。
此时idx对应的栈一定是非空的,将元素弹出,同时要将该栈入小顶堆。
最后对list维护,如果list的尾部栈空了,就从list尾部删除。
整体的时间复杂度ONLogN,空间ON
设计的问题,就是非常的烧。
时间复杂度 O(N(logN))
空间复杂度: O(N)
代码
时间复杂度 O(N(logN))
空间复杂度: O(N)
class DinnerPlates {
// 栈的容量
private int capacity;
// 所有栈
private List<Deque<Integer>> stacks = new ArrayList<>();
// 最小堆,保存未满栈的下标
private PriorityQueue<Integer> idx = new PriorityQueue<>();
public DinnerPlates(int capacity) {
this.capacity = capacity;
}
public void push(int val) {
if (!idx.isEmpty() && idx.peek() >= stacks.size())
idx.clear(); // 堆中都是越界下标,直接清空
if (idx.isEmpty()) { // 所有栈都是满的
var st = new ArrayDeque<Integer>();
st.push(val);
stacks.add(st); // 添加一个新的栈
if (capacity > 1) // 新的栈没有满
idx.add(stacks.size() - 1); // 入堆
} else { // 还有未满栈
var st = stacks.get(idx.peek());
st.push(val); // 入栈
if (st.size() == capacity) // 栈满了
idx.poll(); // 从堆中去掉
}
}
public int pop() {
// 等价为 popAtStack 最后一个非空栈
return popAtStack(stacks.size() - 1);
}
public int popAtStack(int index) {
if (index < 0 || index >= stacks.size() || stacks.get(index).isEmpty())
return -1; // 非法操作
var st = stacks.get(index);
if (st.size() == capacity) // 满栈
idx.add(index); // 元素出栈后,栈就不满了,把下标入堆
int val = st.pop();
// 去掉末尾的空栈(懒删除,堆中下标在 push 时处理)
while (!stacks.isEmpty() && stacks.get(stacks.size() - 1).isEmpty())
stacks.remove(stacks.size() - 1);
return val;
}
}
代码来源于 灵神大佬,非常优雅,值得学习
Tag
栈
哈希
设计