Java中的栈和队列
Java中的栈
Stack
empty()
:检查
push()
:往栈中放入元素
peek()
:查看栈顶元素
pop()
:弹出栈顶元素
search()
:搜索栈中的元素,并返回下标。下标从1开始,栈顶下标为1。
使用Deque来作为栈
Stack
这个类官方都已经不推荐使用了,所以可以使用Deque
来作为栈。需要注意的是,Deque
是一个接口,但是Stack
并不是接口,而是一个类。
Deque
的详情可以看下面队列中的介绍,如下是Deque
作为Stack
时使用的方法。
Java中的队列
Queue
注意看图:Deque属于Queue的子接口。
Queue中的方法总结
Insert类
方法:插入元素到队尾
Remove类
方法:移除队头元素
Examine类
方法:查看对头元素
PriorityQueue
优先队列,使用得比较多的一种队列。
优先队列的实现方法主要有两种:堆
(Heap)和二叉搜索树
(Binary Search Tree)
堆的实现方式也有很多种,如下表所示:Java中的PriorityQueue
是使用严格斐波那契堆实现的,且默认是小顶堆。
二叉搜索树本质上就是二叉树,只不过多了一些约束,得保证任意一个节点,它的左子树的节点都要小于它本身,它的右子树的节点都要大于它本身。
如下所示便是一颗二叉搜索树,不过其构造过于特殊,也是一棵二叉平衡树,不仅要满足二叉搜索树的约束,还得保证左右子树的高度不能相差太大。
- 这是一颗二叉搜索树也是一颗二叉平衡树。
- 这是一颗二叉搜索树,但不是一颗二叉平衡树。
Deque
Deque作为Stack时,可以使用如下方法:
Deque作为Queue时,可以使用如下方法
Deque的常用方法示意图
本篇文章涉及到的题目
题目练习(包含代码)
栈相关的题目
LeetCode232. 用栈实现队列
题目链接:LeetCode232. 用栈实现队列
题目描述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x)
将元素 x 推到队列的末尾
int pop()
从队列的开头移除并返回元素
int peek()
返回队列开头的元素
boolean empty()
如果队列为空,返回 true ;否则,返回 false
说明:
你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
我使用了Java中的Deque来实现队列。用两个栈来模拟一个队列。
- Deque中的
addFirst
方法等效于Stack中的push
方法 - Deque中的
removeFirst
方法等效于Stack中的pop
方法 - Deque中的
getFirst
方法等效于Stack中的peek
方法
我用两个栈来实现一个队列。
push
的时候,将push
的元素放入inputStack
中。一旦要pop
或者是peek
,则将inputStack
中的元素全部出栈依次放入outputStack
中。然后从outputStack
中取出栈顶元素,即相当于取出队列的队头元素。
class MyQueue {
Deque<Integer> inputStack;
Deque<Integer> outputStack;
/**
* Initialize your data structure here.
*/
public MyQueue() {
inputStack = new LinkedList<>();
outputStack = new LinkedList<>();
}
/**
* Push element x to the back of queue.
*/
public void push(int x) {
inputStack.addFirst(x);
}
/**
* Removes the element from in front of queue and returns that element.
*/
public int pop() {
if (!outputStack.isEmpty()) {
return outputStack.removeFirst();
}
while (!inputStack.isEmpty()) {
outputStack.addFirst(inputStack.removeFirst());
}
if (!outputStack.isEmpty()) {
return outputStack.removeFirst();
} else {
return -1;
}
}
/**
* Get the front element.
*/
public int peek() {
if (!outputStack.isEmpty()) {
return outputStack.getFirst();
}
while (!inputStack.isEmpty()) {
outputStack.addFirst(inputStack.removeFirst());
}
if (!outputStack.isEmpty()) {
return outputStack.getFirst();
} else {
return -1;
}
}
/**
* Returns whether the queue is empty.
*/
public boolean empty() {
return (inputStack.isEmpty() && outputStack.isEmpty()) ? true : false;
}
}
LeetCode155.最小栈
题目链接:LeetCode155.最小栈
题目概要
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
push(x)
—— 将元素 x 推入栈中。
pop()
—— 删除栈顶的元素。
top()
—— 获取栈顶元素。
getMin()
—— 检索栈中的最小元素。
需要注意的是获取最小元素的过程是一个动态的过程,当我们往栈中放元素时,可以用一个变量记录最小值。但是当我们从栈顶pop一个元素时,此时栈中元素最小值是多少?又pop了一个元素,此次栈中元素的最小值是多少?然后又push了一个元素,此次栈中元素的最小值又是多少?在我们每次放栈中push元素或者是pop元素时,我们都要重新计算最小值。
方法一:使用List + Sort进行排序
采用这种解法时间复杂度很高,每次push一个元素都要对栈中的元素进行排序,获得最小值。
class MinStack {
private List<Integer> list;
public MinStack() {
this.list = new ArrayList<>();
}
public void push(int x) {
this.list.add(x);
}
public void pop() {
if (this.list.size() == 0) {
return;
} else {
this.list.remove(this.list.size() - 1);
}
}
public int top() {
if (this.list.isEmpty()) {
return Integer.MIN_VALUE;
} else {
return this.list.get(this.list.size() - 1);
}
}
public int getMin() {
if (this.list.isEmpty()) {
return Integer.MIN_VALUE;
}
Object[] objects = list.toArray();
Arrays.sort(objects);
return (Integer) objects[0];
}
}
方法二:使用一个辅助栈
这种方法效率很高,可以O(1)获取栈中的最小值。使用一个辅助栈—minStack
,用来存储栈中的最大值。
- 每次
push
一个元素进栈时,首先从minStack
中peek
栈顶元素,如果发现当前的值比minStack
中的栈顶元素大时,将该元素放入stack的时候同时也将其放入minStack
中。如果当前要放入的元素比minStack
的栈顶元素小时,则读出(注意是peek不是pop)minStack
的栈顶元素,再将其放入minStack
。 - 每次获取栈中的最大元素时,只需要返回
minStack
的栈顶元素就可以。
class MinStack1 {
//存储数据
Deque<Integer> stack;
//存储截止到当前元素,栈中的最小元素
Deque<Integer> minStack;
public MinStack1() {
stack = new LinkedList<>();
minStack = new LinkedList<>();
}
public void push(int x) {
//要个栈要么都是空的,要么都有元素
if (stack.isEmpty() && minStack.isEmpty()) {
minStack.offerFirst(x);
} else {
minStack.offerFirst(Math.min(stack.peekFirst(), x));
}
stack.offerFirst(x);
}
public void pop() {
if (!stack.isEmpty() && !minStack.isEmpty()) {
stack.pollFirst();
minStack.pollFirst();
}
}
public int top() {
return stack.isEmpty() ? -1 : stack.peekFirst();
}
public int getMin() {
return minStack.isEmpty() ? -1 : minStack.peekFirst();
}
}
LeetCode20. 有效的括号
题目链接:LeetCode20. 有效的括号
题目概要
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
使用栈来解决这个问题。
public boolean isValid(String s) {
if (s == null || s.length() == 0) {
return false;
}
Deque<String> stack = new LinkedList<>();
String leftBrackets = "({[";
Map<String, String> matching = new HashMap<>();
matching.put(")", "(");
matching.put("]", "[");
matching.put("}", "{");
for (int i = 0; i < s.length(); ++i) {
String bracket = s.substring(i, i + 1);
//遇到左括号直接将其入栈
if (leftBrackets.contains(bracket)) {
stack.addFirst(bracket);
} else {
//当遍历字符串时遇到右括号,栈顶元素出栈,看是否与该右括号配对
if (stack.isEmpty()) {
return false;
} else if (!matching.get(bracket).equals(stack.removeFirst())) {
return false;
}
}
}
if (stack.isEmpty()) {
return true;
} else {
return false;
}
}
队列相关的题目
LeetCode225.用队列实现栈
题目链接:LeetCode225.用队列实现栈
题目概要
使用队列实现栈的下列操作:
push(x)
– 元素 x 入栈
pop()
– 移除栈顶元素
top()
– 获取栈顶元素
empty()
– 返回栈是否为空
注意:
你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。
我用两个队列来实现一个栈。
当发生push
操作时。检查bufferQueue1
和bufferQueue2
,如一个队列为空,另一个不为空,则将元素放入不为空的队列中。如果都为空,则默认放入第一个队列(第一次push
操作时,两个队列都为空)
当发生pop
或者是top
操作时,则将一个队列的元素全部出队(仅保留最后一个元素
)放入另一个队列,这时,剩下的这个元素便是出栈的栈顶元素。pop
与top
的操作及其类似,唯一的不同是:一个得到了栈顶元素,然后将其删掉;而另一个得到了了栈顶元素,不用将其删掉。(注意:队列的特点是先入先出)
class MyStack {
private Queue<Integer> bufferQueue1;
private Queue<Integer> bufferQueue2;
/**
* Initialize your data structure here.
*/
public MyStack() {
bufferQueue1 = new LinkedList<>();
bufferQueue2 = new LinkedList<>();
}
/**
* Push element x onto stack.
*/
public void push(int x) {
//有两个队列,如果两个队列都为空,则往bufferQueue1添加,
// 如果一个为空,一个不为空,则往不为空的队列中添加元素
if ((bufferQueue1.isEmpty()) && (!bufferQueue2.isEmpty())) {
bufferQueue2.offer(x);
} else if ((!bufferQueue1.isEmpty()) && (bufferQueue2.isEmpty())) {
bufferQueue1.offer(x);
} else if ((bufferQueue1.isEmpty()) && (bufferQueue2.isEmpty())) {
bufferQueue1.offer(x);
}
}
/**
* Removes the element on top of the stack and returns that element.
*/
public int pop() {
if (this.empty() == true) {
return -1;
}
//bufferQueue1不为空,bufferQueue2不为空
if (!bufferQueue1.isEmpty() && bufferQueue2.isEmpty()) {
while (bufferQueue1.size() != 1) {
bufferQueue2.offer(bufferQueue1.poll());
}
return bufferQueue1.poll();
}
//bufferQueue2不为空,bufferQueue1为空
if (bufferQueue1.isEmpty() && !bufferQueue2.isEmpty()) {
while (bufferQueue2.size() != 1) {
bufferQueue1.offer(bufferQueue2.poll());
}
return bufferQueue2.poll();
}
return -1;
}
/**
* Get the top element.
*/
public int top() {
if (this.empty() == true) {
return -1;
}
//bufferQueue1不为空,bufferQueue2不为空
if (!bufferQueue1.isEmpty() && bufferQueue2.isEmpty()) {
while (bufferQueue1.size() != 1) {
bufferQueue2.offer(bufferQueue1.poll());
}
int tmp = bufferQueue1.poll();
bufferQueue2.offer(tmp);
return tmp;
}
if (bufferQueue1.isEmpty() && !bufferQueue2.isEmpty()) {
while (bufferQueue2.size() != 1) {
bufferQueue1.offer(bufferQueue2.poll());
}
int tmp = bufferQueue2.poll();
bufferQueue1.offer(tmp);
return tmp;
}
return -1;
}
/**
* Returns whether the stack is empty.
*/
public boolean empty() {
return ((bufferQueue1.isEmpty()) && (bufferQueue2.isEmpty())) ? true : false;
}
}
LeetCode703.数据流中的第 K 大元素
题目链接:LeetCode703. 数据流中的第 K 大元素
题目描述
设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
请实现 KthLargest 类:
KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
int add(int val) 返回当前数据流中第 k 大的元素。
示例:
输入:
[“KthLargest”, “add”, “add”, “add”, “add”, “add”]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]
解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 5
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8
示意图
LeetCode239. 滑动窗口最大值
题目链接:LeetCode239. 滑动窗口最大值
题目概要
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
方法一:使用排序找每个窗口的最大值(未通过)
这是拿到这个题目最朴素的想法。在每个窗口中都进行排序,然后取该窗口中的元素排序以后的最后一个元素
,由于是从小到达进行排序的,最后一个元素就是该窗口的最大值。但是遇到数据量大的情况,就不行了,最终因为超时没有通过。
问题出在每个窗口都要排序,有很多重复劳作,太费时间了。
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return null;
}
//当数组长度小于窗口长度时,直接返回数组中的最大值
if (nums.length < k) {
Arrays.sort(nums);
return new int[]{nums[nums.length - 1]};
}
int start = 0, numsLength = nums.length, index = 0;
int[] array = new int[numsLength - k + 1];
while ((start + k - 1) < numsLength) {
int end = start + k - 1;
int max = findSlindingWindowMaxValue(nums, start, end);
array[index++] = max;
start++;
}
return array;
}
private int findSlindingWindowMaxValue(int[] nums, int start, int end) {
int tmpLength = end - start + 1;
int[] tmpArray = new int[tmpLength];
int j = 0;
for (int i = start; i <= end; ++i) {
tmpArray[j++] = nums[i];
}
//从小到大进行排序,最大值则是数组最后一个元素
//Arrays.sort使用的是快排,平均时间复杂度为o(NlogN)
Arrays.sort(tmpArray);
return tmpArray[tmpLength - 1];
}
来看看这个数据量:
图上只是一小部分。
方法二:使用双端队列Deque
版本一:Deque中存储滑动窗口中的值
/**
* Deque中存储的是数字
* @param nums
* @param k
* @return
*/
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return null;
}
Deque<Integer> deque = new LinkedList<>();
int[] array = new int[nums.length - k + 1];
int index = 0;
for (int i = 0; i < nums.length; ++i) {
//不能只通过deque的size大小来判断是否需要remove队头的元素
//这样的做法欠妥,应该是存入数组的下标,而不是存入数组的值
//TODO 感觉用nums[i - k] == deque.peekFirst()来判断窗口中的元素是否大于窗口的容量有点勉强
if (deque.size() >= k || ((i >= k) && (nums[i - k] == deque.peekFirst()))) {
deque.removeFirst();
}
while (!deque.isEmpty() && nums[i] > deque.peekLast()) {
deque.pollLast();
}
deque.addLast(nums[i]);
if (i >= k - 1) {
array[index++] = deque.peekFirst();
}
}
return (nums.length < k) ? new int[]{deque.peekFirst()} : array;
}
版本二:Deque中存储窗口中值的下标
个人更推荐这种写法,同时也为以后做这样的题提供了一个很好的思路:不要总是想着存储值,我们可以存储该值所对应的数组的下标,最后通过下标来获取值。这样的好处是,下标是唯一的,能唯一的区分每个元素。
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return null;
}
Deque<Integer> deque = new LinkedList<>();
int[] array = new int[nums.length - k + 1];
int index = 0;
for (int i = 0; i < nums.length; ++i) {
//当deque为空时,deque.peekFirst()返回null
if (!deque.isEmpty() && ((i - deque.peekFirst()) >= k)) {
deque.removeFirst();
}
while (!deque.isEmpty() && (nums[i] > nums[deque.peekLast()])) {
deque.pollLast();
}
//offerLast:插入成功返回true,插入失败可能会返回false(因容量问题插入失败会返回false,其它问题也会抛出异常)
//addLast:插入失败直接抛出异常
deque.offerLast(i);
if (i >= k - 1) {
array[index++] = nums[deque.peekFirst()];
}
}
return (nums.length < k) ? new int[]{deque.peekFirst()} : array;
}
方法三:结合剑指 Offer 59 - II. 队列的最大值中的最大队列
把剑指 Offer 59 - II. 队列的最大值
中写的那个能获取队列中最大值的MaxQueue
拿过来。每次从队头出一个元素,然后从队尾进一个元素,然后O(1)获得队列中的最大值,放入结果集中。
class Solution {
MaxQueue queue = new MaxQueue();
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return null;
}
int[] ans = new int[nums.length - k + 1];
for (int i = 0; i < k; ++i) {
queue.push_back(nums[i]);
}
ans[0] = queue.max_value();
int index = 1;
for (int j = k; j < nums.length; ++j) {
queue.pop_front();
queue.push_back(nums[j]);
ans[index++] = queue.max_value();
}
return ans;
}
/**
* 优化版
*/
class MaxQueue {
private Queue<Integer> queue;
private Deque<Integer> maxQueue;
public MaxQueue() {
queue = new LinkedList<Integer>();
maxQueue = new LinkedList<Integer>();
}
public int max_value() {
if (maxQueue.isEmpty()) {
return -1;
}
return maxQueue.peekFirst();
}
public void push_back(int value) {
while (!maxQueue.isEmpty() && maxQueue.peekLast() < value) {
maxQueue.pollLast();
}
maxQueue.offerLast(value);
queue.offer(value);
}
public int pop_front() {
if (queue.isEmpty()) {
return -1;
}
int ans = queue.poll();
if (ans == maxQueue.peekFirst()) {
maxQueue.pollFirst();
}
return ans;
}
}
}
能通过题解,但是比第二种方法的性能差一些。
剑指 Offer 59 - II. 队列的最大值
题目概要
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
思路与LeetCode225.用队列实现栈有些相似。我总共用了三个队列。其中用了两个双端队列来实现一个maxQueue
,用来存储当前队列中的最大值。然后再使用一个队列用来存储放入队列中的值。
当往队列中添加一个元素时,对应的maxQueue
中也要放入相应的元素,maxQueue
有两个,一个总是保持空的状态,一个用来存储最大值。每次往队列中放入元素时,也需要往maxQueue
中添加当前队列的最大值,不管是从maxQueue
中读取最大值,还是将元素放入maxQueue
,都总是操作不为空的那个maxQueue
。
每次往队列中放入元素,先从maxQueue
中peek
出队尾(peekLast
)的元素。
-
如果要放入队列中的元素大于队尾的元素,则会发生迁移操作。将该
maxQueue
中的元素取出(pollLast
),依次放入另一个maxQueue
,如果取出的元素比要放入队列中的元素大,则将取出的元素直接放入(offerFirst
)另一个maxQueue
。如果比要放入队列中的元素小,则在对应位值放入(offerFirst
)要放入队列中的元素。 -
如果要放入队列中的元素小于队尾的元素则直接放入(
offerLast
)不为空的那个maxQueue
中。
文字叙述有点繁杂,可以看图
整个示意图如下:
代码
/**
* 定义一个队列并实现函数 max_value 得到队列里的最大值
*/
public class MaxQueue {
Queue<Integer> queue;
Deque<Integer> maxQueue1;
Deque<Integer> maxQueue2;
public MaxQueue() {
queue = new LinkedList<>();
maxQueue1 = new LinkedList<>();
maxQueue2 = new LinkedList<>();
}
public int max_value() {
return getValueFromMaxQueue();
}
public void push_back(int value) {
queue.offer(value);
if (maxQueue1.isEmpty() && maxQueue2.isEmpty()) {
maxQueue1.offerLast(value);
} else if (!maxQueue1.isEmpty() && maxQueue2.isEmpty()) {
pushValueInMaxQueue(value, maxQueue1, maxQueue2);
} else if (maxQueue1.isEmpty() && maxQueue1.isEmpty()) {
pushValueInMaxQueue(value, maxQueue2, maxQueue1);
}
}
/**
* @param value
* @param queue1 queu1不为空
* @param queue2 queue2为空
*/
public void pushValueInMaxQueue(int value, Deque<Integer> queue1, Deque<Integer> queue2) {
if (value <= queue1.peekLast()) {
queue1.offerLast(value);
} else {
while (!queue1.isEmpty()) {
int tmp = queue1.pollLast();
if (tmp <= value) {
queue2.addFirst(value);
} else {
queue2.addFirst(tmp);
}
}
queue2.addLast(value);
}
}
public void deleteValueFromMaxQueue() {
if (!maxQueue1.isEmpty() && maxQueue2.isEmpty()) {
maxQueue1.pollFirst();
} else if (maxQueue1.isEmpty() && !maxQueue2.isEmpty()) {
maxQueue2.pollFirst();
}
}
public int getValueFromMaxQueue() {
if (maxQueue1.isEmpty() && maxQueue2.isEmpty()) {
return -1;
} else if (!maxQueue1.isEmpty() && maxQueue2.isEmpty()) {
return maxQueue1.peekFirst();
} else if (maxQueue1.isEmpty() && !maxQueue2.isEmpty()) {
return maxQueue2.peekFirst();
}
return -1;
}
public int pop_front() {
if (!queue.isEmpty()) {
deleteValueFromMaxQueue();
return queue.poll();
} else {
return -1;
}
}
}
通过LeetCode倒是没问题,可以耗时太长了,得优化。
主要耗时地方在于我的程序设计中,是两个队列来实现maxQueue
,会发生很多数据的搬移。
看看官方的题解:maxQueue
只用了一个队列便实现了,没有发生大量的数据迁移的动作,类似于上文 LeetCode239. 滑动窗口最大值 题解中的方法二。
class MaxQueue {
Queue<Integer> queue;
Deque<Integer> maxQueue;
public MaxQueue() {
queue = new LinkedList<Integer>();
maxQueue = new LinkedList<Integer>();
}
public int max_value() {
if (maxQueue.isEmpty()) {
return -1;
}
return maxQueue.peekFirst();
}
public void push_back(int value) {
while (!maxQueue.isEmpty() && maxQueue.peekLast() < value) {
maxQueue.pollLast();
}
maxQueue.offerLast(value);
queue.offer(value);
}
public int pop_front() {
if (queue.isEmpty()) {
return -1;
}
int ans = queue.poll();
if (ans == maxQueue.peekFirst()) {
maxQueue.pollFirst();
}
return ans;
}
}