栈实现队列
栈实现队列
栈和队列共同的特点就是支持基于头或尾的操作,其中队列支持“一侧进,另一侧出”,而栈仅能对一侧进行进出,即栈无法直接删除栈底节点。
而此时我们需要一个另外一个辅助栈,让这个辅助栈存储当前栈的倒序序列,那么栈顶不就是辅助栈的栈顶元素了吗
LinkedList<Integer> stack1 = new LinkedList<>();
LinkedList<Integer> stack2 = new LinkedList<>();
其中stack1存放正序序列,而stack2在用于存放倒序序列(仅在删除时作为一个辅助栈去使用即可)
public void appendTail(int value) {
stack1.push(value);
}
元素往1放入,每次delete就往2里面导入,寻找的时候先往2中找(因为2充当的实际上是一个队列首部的角色,而1充当的是一个队列尾部的角色)
public int deleteHead() {
if(!stack2.isEmpty()){
return stack2.pop(); //stack2的栈顶就是队列的头
}
if(stack1.isEmpty()){
return -1;
}
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
return stack2.pop();
}
元素不存在冗余的部分,要么在stack1,要么在stack2,因此删除时先从2中(逻辑上的队头一侧)寻找,否则将1中元素全部导入2后再取出队首元素
队列实现栈
队列实现栈
假如现在由4个元素 1 2 3 4依次进入队列和栈,那么对应[1,2,3,4],其中队列出队的顺序是1 2 3 4,而栈则是 4 3 2 1
因此可以看出来,队列和栈在出队的时候,序列是反的,那么可不可以在将元素入队的时候就执行一个反向操作呢
Queue<Integer> queue=new LinkedList<>();
我们使用一个队列去实现栈,每次元素入栈的时候,我们将之前的元素依次弹出,并且再次加入队中
public void push(int x) {
queue.offer(x);
for (int i = 0; i < queue.size() - 1; i++) {//刚加入的x不进行操作
queue.add(queue.poll());//一边弹一边加入
}
}
我们依次加入1 2 3 4,那么第一次加入[1],然后是[2,1],接着[3,2,1],最后的队列为[4,3,2,1]。其实就是相当于每次进行头插来进行一个反序操作,但是根据队列的特性我们只能进行头删和尾插,因为我们通过将队列现有元素依次弹出再插入当新元素的后面,模拟一个新元素头插的操作。
因为我们每次push都可以保证“即使底层数据结构是队列,元素顺序依然对应栈的顺序”,因此其他操作都是常规的删除和查看。
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.peek() == null ? true : false;
}
最小栈
最小栈
为了能够在O(1)的时间复杂度拿到栈中最小值,我们需要使用一个专门的数据结构去维护栈中元素的最小值。
LinkedList<Integer> min=new LinkedList<>();
LinkedList<Integer> arr=new LinkedList<>();
我们可以维护一个递减的单调栈,例如-1 3 -2 0 -3 8,我们使用一个常规的栈维护元素[-1,3,-2,0,-3,8],而最小栈维护一个栈顶保存最小值的结构[-1,-2,-3]
public void push(int val) {
if(min.isEmpty()||min.peek()>=val){
min.push(val);
}
arr.push(val);
}
最小值总是单调的,栈顶元素保存当前栈的最小值,并且可以直接在O(1)拿到最小值
public int getMin() {
return min.peek();
}
如果当前删除的值就是最小值的栈顶元素,那么此时的最小值也需要被更新。
public void pop() {
Integer p = arr.pop();
if(p.equals(min.peek())){
min.pop();
}
}
支持增量更新的栈
设计一个支持增量更新的栈
增量更新就是渐进式的更新,更新操作不是一次完成的。例如redis的哈希表扩容就是基于渐进式的,一旦发生扩容行为,增删改查方法都会同时在两个哈希表中进行,并且捎带进行扩容直到扩容完毕。
以inc方法为例:
void inc(int k, int val):栈底的 k 个元素的值都增加 val 。如果栈中元素总数小于 k ,则栈中的所有元素都增加 val 。
我们的最终目标是将inc方法的时间复杂度进行O(1)实现。
int maxSize=0;
int[] stack;
int index=0;
//记录增量更新
int[] add;
public CustomStack(int maxSize) {
this.maxSize=maxSize;
stack = new int[maxSize];
add = new int[maxSize];
}
arr[index++]=val,index指向待插入的位置
我们使用一个辅助数组add记录每次inc()的增量,如果inc操作将栈底K个元素增加val,那么我们就在第k-1的位置记录这个增量val(下标从0开始)。基于差分思想,当我们需要获取这个值的时候,只需从add数组中获取增量并更新数组中的值即可
public void increment(int k, int val) {
int limit = Math.min(k-1,index-1);
if(limit>0){
add[limit]+=val;
}
}
仅存储增量,不对数组中的元素进行实际的更新操作。
public void push(int x) {
if(isFull())return;
stack[index++]=x;
}
push的时候,不涉及获取元素的值,因此不需要将增量更新到对应元素。
public int pop() {
if(isEmpty()) return -1;
//仅在pop时才获取具体值
index--;
//将增量累加到元素上
int res = stack[index]+add[index];
if(index-1>=0){
//增量更新
add[index-1]+=add[index];
}
add[index]=0;//增量复原
return res;
}
因为pop()调用,我们需要返回元素的值,这个时候将增量更新到元素上,并且将对应位置的增量重置为0
注意:从栈顶到栈底方向,增量是叠加的,而这个叠加也是“懒更新”的,例如add数组记录[0,1,3,0,0],意味着栈底的三个元素需要最终被累加:1+3,1+3和3 。并且一旦更新完毕增量就可以复原为0(累加任务进行完毕了,清除任务列表)
private boolean isEmpty(){
return index==0;
}
private boolean isFull(){
return index==maxSize;
}