算法 | 第3章 栈与队列相关《程序员面试金典》#yyds干货盘点#
Posted 多氯环己烷
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法 | 第3章 栈与队列相关《程序员面试金典》#yyds干货盘点#相关的知识,希望对你有一定的参考价值。
@[TOC](第3章 栈与队列相关《程序员面试金典》)
前言
本系列笔记主要记录笔者刷《程序员面试金典》算法的一些想法与经验总结,按专题分类,主要由两部分构成:经验值点和经典题目。其中重点放在经典题目上;
0. *经验总结
0.1 程序员面试金典 P82
- 栈 - 后进先出(LIFO):
- 栈无法在常数时间复杂度内访问第i个元素。但因为栈不需要在添加和删除时移动元素,可以在常数时间复杂度内完成此操作;
- 对于递归算法:有时需要把临时变量加入到栈中,在回溯时删除;
-
队列 - 先进先出(FIFO):
- 更新队列第一个和最后一个节点容易出错,要再三确认;
- 队列常用于广度优先搜索或缓存的实现;
- 例如,在广度优先搜索中,可以使用队列来存储需要被处理的节点。每处理一个结点,就把其相邻节点加入队列尾部。这样可以按照发现顺序处理节点;
-
需要注意的共同点:
- 要理清下标的栈大小的区别;
- 涉及栈和队列的题目往往需要编写好几个方法,要理清楚前后逻辑;
0.2 Java常用数据结构API
详细见以下文章:
Java | 个人总结的Java常用API手册汇总
0.3 关于延迟移动
- 有些时候我们需要访问栈底或者队列底的数据,往往需要对数据次序进行反转操作;
- 我们可以每次都进行两次反转,因为要保证回到原来的样子,这样会产生大量重复操作;
- 另一种做法是我们先对栈或队列进行反转,需要访问栈顶或队列顶元素时才翻转回来;
- 第二种方法需要注意翻转的时机;
- 下面《4. 化栈为队》和《5. 栈排序》都用到类似的思想;
1. 三合一 [easy]
1.1 考虑点
- 注意看提示
0<=stackNum<=2
,说明三个栈在数组中排列是123123123; - 可以试着跟面试官谈谈扩展问题,如:
- 当运行栈块空间大小灵活可变时,要进行弹性分割,该怎么实现;
- 可以将数组设计成环形,最后一个栈可能从数组尾处开始,环绕到数组起始处;
- 方法实现起来比较复杂,可以试着提供伪代码或其中部分代码;
1.2 解法
1.2.1 三指针法
class TripleInOne
int[] stack;
int stackSize;
int t0;
int t1;
int t2;
public TripleInOne(int stackSize)
stack = new int[stackSize*3];
int t0 = -3;
int t1 = -2;
int t2 = -1;
this.stackSize = stackSize;
this.t0 = t0;
this.t1 = t1;
this.t2 = t2;
public void push(int stackNum, int value)
if( stackNum == 0 )
this.t0 = pushVal( t0, value );
else if( stackNum == 1 )
this.t1 = pushVal( t1, value);
else if( stackNum == 2)
this.t2 = pushVal( t2, value);
public int pushVal( int top, int value )
if( top + 3 < stackSize*3 )
top += 3;
stack[top] = value;
return top;
public int pop(int stackNum)
int val = peek(stackNum);
if( val != -1 )
if( stackNum == 0 )
stack[t0] = -1;
t0 -= 3;
else if( stackNum == 1 )
stack[t1] = -1;
t1 -= 3;
else if( stackNum == 2)
stack[t2] = -1;
t2 -= 3;
return val;
return -1;
public int peek(int stackNum)
if( stackNum == 0 && t0 >= 0 )
return stack[t0];
else if( stackNum == 1 && t1 >= 1)
return stack[t1];
else if( stackNum == 2 && t2 >= 2)
return stack[t2];
return -1;
public boolean isEmpty(int stackNum)
int value = peek(stackNum);
if( value == -1)
return true;
return false;
- 执行时间:34.80%;内存消耗:44.07%;
- 定义三个指针,分别指向三个队列在数组的索引;
1.2.2 二维数组法(优)
class TripleInOne
int N = 3;
// 3 * n 的数组,每一行代表一个栈
int[][] data;
// 记录每个栈「待插入」位置
int[] locations;
public TripleInOne(int stackSize)
data = new int[N][stackSize];
locations = new int[N];
public void push(int stackNum, int value)
int[] stk = data[stackNum];
int loc = locations[stackNum];
if (loc < stk.length)
stk[loc] = value;
locations[stackNum]++;
public int pop(int stackNum)
int[] stk = data[stackNum];
int loc = locations[stackNum];
if (loc > 0)
int val = stk[loc - 1];
locations[stackNum]--;
return val;
else
return -1;
public int peek(int stackNum)
int[] stk = data[stackNum];
int loc = locations[stackNum];
if (loc > 0)
return stk[loc - 1];
else
return -1;
public boolean isEmpty(int stackNum)
return locations[stackNum] == 0;
- 执行时间:97.06%;内存消耗:69.49%;
- 时间复杂度:所有的操作均为 O(1)。
- 空间复杂度:O(k*n)。k 为我们需要实现的栈的个数,n 为栈的容量;
- 建立一个二维数组 datadata ;二维数组的每一行代表一个栈,同时使用一个locationslocations 记录每个栈「待插入」的下标;
1.2.3 一维数组法
class TripleInOne
int N = 3;
int[] data;
int[] locations;
int size;
public TripleInOne(int stackSize)
size = stackSize;
data = new int[size * N];
locations = new int[N];
for (int i = 0; i < N; i++)
locations[i] = i * size;
public void push(int stackNum, int value)
int idx = locations[stackNum];
if (idx < (stackNum + 1) * size)
data[idx] = value;
locations[stackNum]++;
public int pop(int stackNum)
int idx = locations[stackNum];
if (idx > stackNum * size)
locations[stackNum]--;
return data[idx - 1];
else
return -1;
public int peek(int stackNum)
int idx = locations[stackNum];
if (idx > stackNum * size)
return data[idx - 1];
else
return -1;
public boolean isEmpty(int stackNum)
return locations[stackNum] == stackNum * size;
- 执行时间:97.06%;内存消耗:44.07%;
2. 栈的最小值 [easy]
2.1 考虑点
- 把加入一个元素看成一种状态,每种状态都有对应最小值,这样得到解法2;
2.2 解法
2.2.1 循环遍历法
class MinStack
Stack<Integer> stack;
Stack<Integer> minStack;
/** initialize your data structure here. */
public MinStack()
Stack<Integer> stack = new Stack<>();
Stack<Integer> minStack = new Stack<>();
this.stack = stack;
this.minStack = minStack;
public void push(int x)
stack.add(x);
if( minStack.isEmpty() )
minStack.add(x);
return;
Stack<Integer> cache = new Stack<>();
boolean isMatter = false;
while( !isMatter )
if( !minStack.isEmpty() && minStack.peek() < x )
cache.add( minStack.pop() );
else
minStack.add(x);
isMatter = true;
while( !cache.isEmpty() )
minStack.add(cache.pop());
public void pop()
if( stack.isEmpty() )
return;
int topNum = stack.pop();
Stack<Integer> cache = new Stack<>();
boolean isMatter = false;
while( !isMatter )
if( minStack.peek() == topNum )
minStack.pop();
isMatter = true;
else
cache.add(minStack.pop());
while( !cache.isEmpty() )
minStack.add(cache.pop());
public int top()
return stack.peek();
public int getMin()
return minStack.peek();
- 执行时间:5.02%;内存消耗:5.18%;
- 不推荐用此方法,不满足O(1)的时间复杂度;
2.2.2 主辅栈法(优)
class MinStack
Deque<Integer> xStack;
Deque<Integer> minStack;
public MinStack()
xStack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
minStack.push(Integer.MAX_VALUE);
public void push(int x)
xStack.push(x);
minStack.push(Math.min(minStack.peek(), x));
public void pop()
xStack.pop();
minStack.pop();
public int top()
return xStack.peek();
public int getMin()
return minStack.peek();
- 执行时间:91.63%;内存消耗:58.64%;
- 创建两个栈,一个栈是主栈 stackstack,另一个是辅助栈 minStackminStack,用于存放对应主栈不同时期的最小值;
3. 堆盘子 [medium]
- push是往最后一个栈中放数据,而不是从前遍历找到空位;
3.1 考虑点
- 注意讨论当传入cap=0时的情况;
- 可以跟面试官讨论push的两种情况:
- 第一种是:往最后一个栈中放数据(下诉解法);
- 第二种是:从前遍历找到空位(需要推出栈n+1的栈底元素到栈n,循环往复到最后一个栈);
- 前者优点是能很大程度上改善时间复杂度,后者适用一些要求所有栈(除最后一个)都填满的场景;
3.2 解法
3.2.1 单链表寻栈法
class StackOfPlates
int cap;
List<Stack<Integer>> list;
public StackOfPlates(int cap)
List<Stack<Integer>> list = new ArrayList<>();
this.cap = cap;
this.list = list;
public void push(int val)
if( cap == 0 )
return;
Stack<Integer> stack;
if( list.isEmpty() )
stack = new Stack<Integer>();
list.add(stack);
else
stack = list.get(list.size() - 1);
if( stack.size() >= cap )
stack = new Stack<Integer>();
list.add(stack);
if( stack.size() < cap )
stack.add(val);
public int pop()
if( cap == 0 )
return -1;
if( list.isEmpty() )
return -1;
Stack<Integer> stack = list.get(list.size() - 1);
int result = stack.pop();
if(stack.isEmpty())
list.remove( list.size() - 1 );
return result;
public int popAt(int index)
if( cap == 0 )
return -1;
if( index > list.size()-1 || index < 0 || list.isEmpty() )
return -1;
Stack<Integer> stack = list.get(index);
int result = stack.pop();
if(stack.isEmpty())
list.remove( index );
return result;
- 执行时间:60.44%;内存消耗:43.48%;
3.2.2 二维链表法(优)
class StackOfPlates
private LinkedList<LinkedList<Integer>> setOfStacks;
private int cap;
public StackOfPlates(int cap)
this.setOfStacks = new LinkedList<>();
this.cap = cap;
private boolean setIsEmpty()
return setOfStacks.isEmpty();
private boolean lastStackIsFUll()
if (setIsEmpty())
return true;
return setOfStacks.getLast().size()>=cap;
private boolean lastStackIsEmpty()
return setOfStacks.getLast().isEmpty();
public void push(int val)
if (cap <= 0)
return;
if (setIsEmpty() || lastStackIsFUll())
setOfStacks.addLast(new LinkedList<>());
setOfStacks.getLast().addLast(val);
public int pop()
int val = -1;
if (setIsEmpty())
return val;
val = setOfStacks.getLast().removeLast();
if (lastStackIsEmpty())
setOfStacks.removeLast();
return val;
public int popAt(int index)
int val = -1;
if (setIsEmpty() ||setOfStacks.size()-1<index )
return val;
val = setOfStacks.get(index).removeLast();
if (setOfStacks.get(index).isEmpty())
setOfStacks.remove(index);
return val;
- 执行时间:96.23%;内存消耗:60.59%;
- 使用二维的链表存储,每次当前栈满了就新增;
4. 化栈为队 [easy]
4.1 考虑点
- 下诉第一种解法会存在大量重复操作,可以使用第二种延迟移动的方法,在有必要时才反转次序;
4.2 解法
4.2.1 双栈法
class MyQueue
Stack<Integer> stack;
Stack<Integer> cache;
/** Initialize your data structure here. */
public MyQueue()
Stack<Integer> stack = new Stack<>();
Stack<Integer> cache = new Stack<>();
this.stack = stack;
this.cache = cache;
/** Push element x to the back of queue. */
public void push(int x)
stack.add(x);
/** Removes the element from in front of queue and returns that element. */
public int pop()
if(stack.isEmpty())
return -1;
while( !stack.isEmpty() )
cache.add(stack.pop());
int result = cache.pop();
while( !cache.isEmpty() )
stack.add(cache.pop());
return result;
/** Get the front element. */
public int peek()
if(stack.isEmpty())
return -1;
while( !stack.isEmpty() )
cache.add(stack.pop());
int result = cache.peek();
while( !cache.isEmpty() )
stack.add(cache.pop());
return result;
/** Returns whether the queue is empty. */
public boolean empty()
return stack.isEmpty();
- 执行时间:81.59%;内存消耗:53.96%;
4.2.2 延迟移动法(优)
class MyQueue
Deque<Integer> inStack;
Deque<Integer> outStack;
public MyQueue()
inStack = new LinkedList<Integer>();
outStack = new LinkedList<Integer>();
public void push(int x)
inStack.push(x);
public int pop()
if (outStack.isEmpty())
in2out();
return outStack.pop();
public int peek()
if (outStack.isEmpty())
in2out();
return outStack.peek();
public boolean empty()
return inStack.isEmpty() && outStack.isEmpty();
private void in2out()
while (!inStack.isEmpty())
outStack.push(inStack.pop());
- 执行时间:100.00%;内存消耗:35.19%;
- 只有进行pop()和peek()操作,并且输出栈为空时才需要翻转次序;
- 当有必要反转次序时才移动元素,用户进行连续pop()操作时不需要反转次序;
5. 栈排序 [medium]
5.1 考虑点
- 需要注意细节;
- 可以考虑延迟移动;
5.2 解法
5.2.1 单栈法
class SortedStack
Stack<Integer> stack;
Stack<Integer> cache;
public SortedStack()
Stack<Integer> stack = new Stack<>();
Stack<Integer> cache = new Stack<>();
this.stack = stack;
this.cache = cache;
public void push(int val)
if( stack.isEmpty() )
stack.add(val);
return;
if( stack.peek() > val )
stack.add(val);
else
cache.add(stack.pop());
stack.add(val);
stack.add(cache.pop());
public void pop()
if( stack.isEmpty() )
return;
stack.pop();
int val;
if( !stack.isEmpty() )
val = stack.peek(); //忘记peek
else
return;
while(!stack.isEmpty())
if( stack.peek() >= val )
cache.add(stack.pop());
else
val = stack.peek();
boolean isEliminate = false;
while( !cache.isEmpty() )
if( cache.peek() == val && !isEliminate )
cache.pop();
isEliminate = true; //忘记上锁
else
stack.add( cache.pop() );
stack.add(val);
public int peek()
if(stack.isEmpty())
return -1;
return stack.peek();
public boolean isEmpty()
return stack.isEmpty();
- 执行时间:5.04%;内存消耗:70.80%;
- 这里的单栈指每次操作后数据都存储在一个栈,另一个栈只做辅助作用,可以用其他数据结构代替;
5.2.2 根据插入值分离栈法(优)
class SortedStack
//原始栈
Deque<Integer> stack = new LinkedList<>();
//辅助栈
Deque<Integer> tmp = new LinkedList<>();
public SortedStack()
public void push(int val)
int max = stack.isEmpty() ? Integer.MAX_VALUE : stack.peek();
int min = tmp.isEmpty() ? Integer.MIN_VALUE : tmp.peek();
//比较原始栈与辅助栈栈顶值,使其满足 辅助栈 <= val <= 原始栈
while(true)
if(val > max)
tmp.push(stack.pop());
max = stack.isEmpty() ? Integer.MAX_VALUE : stack.peek();
else if(val < min)
stack.push(tmp.pop());
min = tmp.isEmpty() ? Integer.MIN_VALUE : tmp.peek();
else
stack.push(val);
break;
public void pop()
//将辅助栈元素push回原始栈
while (!tmp.isEmpty())
stack.push(tmp.pop());
if (!stack.isEmpty())
stack.pop();
public int peek()
//将辅助栈元素push回原始栈
while (!tmp.isEmpty())
stack.push(tmp.pop());
return stack.isEmpty() ? -1 : stack.peek();
public boolean isEmpty()
return stack.isEmpty() && tmp.isEmpty();
- 执行时间:99.65%;内存消耗:81.59%;
- push()操作后数据可以分布在两个栈中,只有pop()或peek()操作时才考虑将储存值比较小的栈归位;
6. 动物收容所 [easy]
6.1 考虑点
- 可以问问面试官允许使用多少个链表(或其他数据结构);
- 这个问题有多种解法,如果使用一个队列,对
dequeueAny()
操作简单,而dequeueCat()
和dequeueDog()
则需要遍历整个队列,降低执行效率;(对应解法一) - 另一种解法是猫狗各自创建一个队列,放进包装类里,包装类有个时间戳属性,用来标记进入时间,在执行
dequeueAny()
操作时只需要查看两个队列的的首部,比较时间戳,返回较老那位;(对应解法二)
6.2 解法
6.2.1 单链表法
class AnimalShelf
List<String> list;
public AnimalShelf()
List<String> list = new ArrayList<>();
this.list = list;
public void enqueue(int[] animal)
if( animal[0] < 0 || animal[1] < 0 || animal[1] > 2)
return;
String animalStr = String.valueOf(animal[0]) + String.valueOf(animal[1]);
list.add(animalStr);
public int[] dequeueAny()
if( list.isEmpty() )
return new int[]-1, -1;
String result = list.get(0);
list.remove(0);
int num = Integer.parseInt(result);
return new int[]num/10, num%10;
public int[] dequeueDog()
if( list.isEmpty() )
return new int[]-1, -1;
for( int i = 0; i < list.size(); i++)
int num = Integer.parseInt( list.get(i) );
if( num%10 == 1 )
list.remove(i);
return new int[]num/10, num%10;
return new int[]-1, -1;
public int[] dequeueCat()
if( list.isEmpty() )
return new int[]-1, -1;
for( int i = 0; i < list.size(); i++)
int num = Integer.parseInt( list.get(i) );
if( num%10 == 0 )
list.remove(i);
return new int[]num/10, num%10;
return new int[]-1, -1;
- 执行时间:17.07%;内存消耗:97.72%;
6.2.2 双队列法(优)
class AnimalShelf
LinkedList<int[]> queueCat;
LinkedList<int[]> queueDog;
public AnimalShelf()
queueCat = new LinkedList<>();
queueDog = new LinkedList<>();
public void enqueue(int[] animal)
// 判断种类后入队
if (animal[1] == 0)
queueCat.addLast(animal);
else if (animal[1] == 1)
queueDog.addLast(animal);
public int[] dequeueAny()
// 取出cat的队首,判空则直接返回
int[] headCat;
if (!queueCat.isEmpty())
headCat = queueCat.getFirst();
else if (!queueDog.isEmpty())
return queueDog.removeFirst();
else
return new int[]-1,-1;
// 取出dog的队首,判空则直接返回
int[] headDog;
if (!queueDog.isEmpty())
headDog = queueDog.getFirst();
else
return queueCat.removeFirst();
// 比较后返回
if (headCat[0]<=headDog[0])
return queueCat.removeFirst();
else
return queueDog.removeFirst();
public int[] dequeueDog()
if (!queueDog.isEmpty())
return queueDog.removeFirst();
else
return new int[]-1,-1;
public int[] dequeueCat()
if (!queueCat.isEmpty())
return queueCat.removeFirst();
else
return new int[]-1,-1;
- 执行时间:98.86;内存消耗:29.43%;
- 建立两个队列,分别存储猫和狗。dequeCat和dequeDog分别取对应的队首;
最后
::: hljs-center
新人制作,如有错误,欢迎指出,感激不尽!
:::
::: hljs-center
欢迎关注公众号,会分享一些更日常的东西!
:::
::: hljs-center
如需转载,请标注出处!
:::
::: hljs-center
:::
以上是关于算法 | 第3章 栈与队列相关《程序员面试金典》#yyds干货盘点#的主要内容,如果未能解决你的问题,请参考以下文章
算法 | 第5章 位操作相关《程序员面试金典》#yyds干货盘点#
算法 | 第2章 链表相关《程序员面试金典》#yyds干货盘点#
算法 | 第1章 数组与字符串相关《程序员面试金典》#yyds干货盘点#