1172. 餐盘栈
难度困难48
我们把无限数量 ∞ 的栈排成一行,按从左到右的次序从 0 开始编号。每个栈的的最大容量 capacity
都相同。
实现一个叫「餐盘」的类 DinnerPlates
:
DinnerPlates(int capacity)
- 给出栈的最大容量capacity
。void push(int val)
- 将给出的正整数val
推入 从左往右第一个 没有满的栈。int pop()
- 返回 从右往左第一个 非空栈顶部的值,并将其从栈中删除;如果所有的栈都是空的,请返回-1
。int popAtStack(int index)
- 返回编号index
的栈顶部的值,并将其从栈中删除;如果编号index
的栈是空的,请返回-1
。
示例:
输入:
["DinnerPlates","push","push","push","push","push","popAtStack","push","push","popAtStack","popAtStack","pop","pop","pop","pop","pop"]
[[2],[1],[2],[3],[4],[5],[0],[20],[21],[0],[2],[],[],[],[],[]]
输出:
[null,null,null,null,null,null,2,null,null,20,21,5,4,3,1,-1]
解释:
DinnerPlates D = DinnerPlates(2); // 初始化,栈最大容量 capacity = 2
D.push(1);
D.push(2);
D.push(3);
D.push(4);
D.push(5); // 栈的现状为: 2 4
1 3 5
﹈ ﹈ ﹈
D.popAtStack(0); // 返回 2。栈的现状为: 4
1 3 5
﹈ ﹈ ﹈
D.push(20); // 栈的现状为: 20 4
1 3 5
﹈ ﹈ ﹈
D.push(21); // 栈的现状为: 20 4 21
1 3 5
﹈ ﹈ ﹈
D.popAtStack(0); // 返回 20。栈的现状为: 4 21
1 3 5
﹈ ﹈ ﹈
D.popAtStack(2); // 返回 21。栈的现状为: 4
1 3 5
﹈ ﹈ ﹈
D.pop() // 返回 5。栈的现状为: 4
1 3
﹈ ﹈
D.pop() // 返回 4。栈的现状为: 1 3
﹈ ﹈
D.pop() // 返回 3。栈的现状为: 1
﹈
D.pop() // 返回 1。现在没有栈。
D.pop() // 返回 -1。仍然没有栈。
提示:
1 <= capacity <= 20000
1 <= val <= 20000
0 <= index <= 100000
- 最多会对
push
,pop
,和popAtStack
进行200000
次调用。
超时,暴力做法
暴力做法的缺点,每次push和pop时都从端点开始找栈,有没有数据结构能帮我维护未满的栈
class DinnerPlates {
List<Deque<Integer>> list;
int capacity;
public DinnerPlates(int capacity) {
list = new ArrayList<>();
this.capacity = capacity;
}
public void push(int val) {
int idx = 0;
while(idx < list.size()){
if(list.get(idx).size() < capacity) break;
idx++;
}
if(idx == list.size()) list.add(new ArrayDeque<>());
Deque<Integer> cur = list.get(idx);
cur.push(val);
list.set(idx, cur);
}
public int pop() {
int idx = list.size() - 1;
while(idx > 0){
if(list.get(idx).size() > 0) break;
idx--;
}
if(idx == 0 && list.get(0).size() == 0) return -1;
Deque<Integer> cur = list.get(idx);
int res = cur.poll();
list.set(idx, cur);
return res;
}
public int popAtStack(int idx) {
if(idx > list.size()) return -1;
Deque<Integer> cur = list.get(idx);
if(cur.size() == 0) return -1;
int res = cur.poll();
list.set(idx, cur);
return res;
}
}
懒删除堆:
题解:https://leetcode.cn/problems/dinner-plate-stacks/solution/yu-qi-wei-hu-di-yi-ge-wei-man-zhan-bu-ru-sphs/
假如一开始连续执行了很多次 push 操作,就会得到一排整整齐齐的栈;然后再执行一些 popAtStack 操作,随机挑选 index,就会把这些栈弄得参差不齐。
这个时候再交替执行 push 和 popAtStack,「从左往右第一个没有满的栈」就没有什么规律可言了。如果把第一个未满栈填满,就要暴力寻找下一个未满栈了。
如何优化?
格局打开:与其维护第一个未满栈,不如维护所有未满栈。
假设可以用一个数据结构来维护这些未满栈(的下标),看看需要哪些操作:
-
**对于 push 来说,需要知道这些下标中的最小值。**如果入栈后,栈满了,那么这个栈就不能算作未满栈,此时这个最小下标就需要从数据结构中移除
-
对于 popAtsStack 来说,如果操作的是一个满栈,操作后就得到了一个未满栈,那么就往数据结构中添加这个栈的下标
-
对于 pop 来说,它等价于 popAtStack(lastIndex),其中lastIndex 是最后一个非空栈的下标
所以我们需要一个支持添加元素、查询最小值和移除最小值的数据结构。最小堆,就你了。
此外还需要一个列表 stacks,它的每个元素都是一个栈。上面说的lastIndex 就是 stacks 的长度减一。
如何维护 stacks 呢?
-
如果 push 时最小堆为空,说明所有栈都是满的,那么就需要向 stacks 的末尾添加一个新的栈。如果 capacity >1,就把这个新的未满栈的下标入堆
-
如果 popAtstack 操作的是最后一个栈,且操作后栈为空,那么就从 stacks 中移除最后一个栈。如果移除后 stacks 的最后一个栈也是空的,就不断移除直到 stacks 为空或者最后一个栈不为空。细节: 需要同步移除最小堆中的下标吗? 其实不需要如果在 push 时发现堆顶的下标大于等于 stacks 的长度,把整个堆清空即可。 (这个技巧叫懒删除。)
-
此外,如果 popAtstack 操作越界或者操作的栈为空,则返回 -1
问: 懒删除是否会导致堆中有重复的下标?
答: 不会有重复下标。
假设重复下标是在 push 时插入的,说明此前所有栈都是满的,堆中残留的下标必然都大于等于 stacks 的长度但这种情况下 push 会清空堆,不会导致重复下标。
假设重复下标是在 popAtstack 时插入的,这只会发生在 tackslindet 是满栈的情况下,而 push 保证在满栈时会去掉这个下标,所以也不会导致重复下标。
class DinnerPlates {
// 懒删除堆:在pop元素时,该栈中 size=0 的时候不同步进行移除最小堆中的下标,只移除stacks列表中末尾的空栈
// 在push时判断堆顶下标是否越界了(idx.peek() >= stacks.size()),越界则删除
// 栈的容量
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()){ // 所有的栈都是满的
Deque<Integer> st = new ArrayDeque<>();
st.push(val);
stacks.add(st); // 添加一个新的栈
if(capacity > 1){ // 新的栈没有满
idx.add(stacks.size() - 1); // 入堆
}
}else{ // 还有未满栈,在最小下标对应的栈中添加元素
Deque<Integer> 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; // 非法操作
Deque<Integer> 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;
}
}