今日内容
- 栈与队列理论基础
- leetcode. 232 用栈实现队列
- leetcode. 225 用队列实现栈
- leetcode. 20 有效的括号
- leetcode. 1047 删除字符串中的所有相邻重复项
栈与队列理论基础
栈和队列都是数据结构,其中栈的特点是先入后出,队列是先入先出。
但是我们往往都是直接使用它们,却很少对它们的底层原理进行研究。
在Java中,栈 可以使用Stack来实现。但是官方实际上并不太推荐使用Stack,而是推荐使用Deque(双向队列)来实现栈的功能。Deque里是包含了栈的操作的。
理由就在于,Stack继承自Vector,它的底层由数组实现,并且它也是JDK 1.0 时代的产物了。Vector在方法上添加了synchronized来达到线程安全的目的,但是它特别耗资源,有时候可能栈的实现并不要求线程安全。
而队列继承自Collection接口,比较常见的是用 ArrayQueue 或 LinkedList 来实现,即分别由数组和链表实现。
Leetcode. 232 用栈实现队列
该题用于熟悉栈和队列的操作,对算法并没有什么要求。
栈和队列的区别就在于,前者先入后出,后者先入先出。要想让栈实现队列,就需要先把输出次序对齐。
所以我们可以做两个栈,入队操作就按照入栈操作执行,将元素推入stackIn栈。出队操作则先将stackIn的元素推出到stackOut的栈,最后再将stackOut栈的元素推出。在两个栈之间进行元素转移时,元素的输出次序也就对齐了。
实现代码如下:
class MyQueue {
Deque<Integer> stackIn; // 模拟队列输入
Deque<Integer> stackOut; // 模拟队列输出
// 初始化
public MyQueue() {
stackIn = new LinkedList<>();
stackOut = new LinkedList<>();
}
public void push(int x) {
while (!stackOut.isEmpty()){ // 先将输出栈的元素全部移回来,保证原先次序
stackIn.push(stackOut.pop());
}
stackIn.push(x);
}
public int pop() {
while (!stackIn.isEmpty()){ // 将输入栈的元素全部移到输出栈,保证符合队列输出次序
stackOut.push(stackIn.pop());
}
int out = stackOut.pop();
return out;
}
public int peek() {
while (!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
int out = stackOut.peek();
return out;
}
public boolean empty() {
if (stackIn.isEmpty() && stackOut.isEmpty()){ // 若输入栈和输出栈全空则说明空了
return true;
} else {
return false;
}
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
- 时间复杂度:O(n)
- 空间复杂度:O(n)
Leetcode. 225 用队列实现栈
和上一题差不多思路,不过队列比较特殊,既可以用两个队列实现,也可以直接用一个队列实现。
两个队列的思路和上题差不多,注意好输出次序就好,代码如下:
class MyStack {
Queue<Integer> data;
Queue<Integer> backup;
// 初始化
public MyStack() {
data = new LinkedList<>();
backup = new LinkedList<>();
}
public void push(int x) {
data.offer(x);
}
public int pop() {
int index = top();
transToBackup(data, backup);
data.poll();
transToData(data, backup);
return index;
}
public int top() {
transToBackup(data, backup);
int index = data.peek();
transToData(data, backup);
return index;
}
public boolean empty() {
if (data.size() == 0 && backup.size() == 0){return true;}
return false;
}
// 将data队列中除最后一个元素都移动到backup队列中
public void transToBackup(Queue<Integer> data, Queue<Integer> backup){
while (data.size() != 1){
backup.offer(data.peek());
data.poll();
}
}
// 将backup队列中所有元素都移回data队列中
public void transToData(Queue<Integer> data, Queue<Integer> backup){
// 如果data队列不为空就挪到backup队列中
if (!data.isEmpty()){
backup.offer(data.peek());
data.poll();
}
// 此时backup队列中的元素就是按照原先顺序排列
// 再挪回data队列中
while (backup.size() != 0){
data.offer(backup.peek());
backup.poll();
}
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
- 时间复杂度:pop 和 top 操作为O(n),其余操作为O(1)
- 空间复杂度:O(n)
还可以直接使用一个双向队列来实现,代码如下:
class MyStack {
Deque<Integer> queue;
// 初始化
public MyStack() {
queue = new LinkedList<>();
}
public void push(int x) {
queue.offerLast(x);
}
public int pop() {
int out = queue.peekLast();
queue.pollLast();
return out;
}
public int top() {
int out = queue.peekLast();
return out;
}
public boolean empty() {
if (queue.isEmpty()){
return true;
} else {
return false;
}
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
- 时间复杂度:O(1)
- 空间复杂度:O(n)
Leetcode. 20 有效的括号
括号匹配是经典的用栈解决的问题。
在面对这种问题时,需要事先将可能出现的情况列出来,否则会越做越糊涂。深有体会啊,写着写着就被莫名其妙绕昏了。
不匹配的情况有如下几种:
- 字符串遍历完,但栈还有元素。即字符串中的左括号多余了。
- 还没遍历完,栈就已经空了。即字符串右括号有多余。
- 括号没有多余,但是类型没有匹配上。
根据上述列出的情况,我们写出如下代码:
class Solution {
public boolean isValid(String s) {
if (s.length() % 2 != 0){return false;} // 字符串长度为奇数的话,肯定配不了对
Deque<Character> stack = new LinkedList<>();
char[] array = s.toCharArray();
for (int i = 0; i < array.length; i++){
char ch = array[i];
if (ch == '('){
stack.push(')');
} else if (ch == '{'){
stack.push('}');
} else if (ch == '['){
stack.push(']');
} else if (stack.isEmpty() || stack.peek() != ch){ // 判断情况 2 和 3
return false;
} else {
stack.pop();
}
}
return stack.isEmpty(); // 判断情况 1
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
Leetcode. 1047 删除字符串中的所有相邻重复项
其实这也可以视作一个匹配问题。思路就是先将字符串元素推入栈中,然后比较栈顶元素和当前字符,如果相同,则说明是相邻重复项,推出栈;如果不同则不是,入栈。
根据该思路,写出如下代码:
class Solution {
public String removeDuplicates(String s) {
Deque<Character> stack = new LinkedList<>();
for (int i = 0; i < s.length(); i++){
char ch = s.charAt(i);
if (i == 0){ // 第一个元素直接入栈
stack.push(ch);
continue;
}
if (stack.isEmpty() || stack.peek() != ch){ // 当栈空了或者不是相邻重复项时入栈
stack.push(ch);
} else {
stack.pop(); // 当栈非空且是相邻重复项时出栈
}
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()){
sb.append(stack.pop());
}
return sb.reverse().toString(); // 将栈中元素返回
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
总结
开始栈和队列,栈和队列一直以比较隐蔽的方式出现在我们的生活中,所以还是比较重要的数据结构。
通过本节对栈和队列的Java底层有所了解了,并且对它们的操作也更加熟悉了。最近马上要开学了,估计要忙起来了,继续坚持!