232 用栈实现队列
题目描述
两个栈模拟队列的思路是利用栈(后进先出结构)的特性来实现队列(先进先出结构)的行为。这种方法依赖于两个栈来逆转元素的入队和出队顺序,从而实现队列的功能。
入队操作(使用stackIn):所有新加入的元素都直接推入stackIn。因为栈支持后进先出,所以此时不需要考虑元素的顺序。
出队操作(使用stackOut):当需要进行出队操作(即移除队列的最前端元素)时,我们先检查stackOut:如果stackOut为空,则将stackIn中所有元素逐一弹出并推入stackOut。这样,最先进入stackIn的元素(也就是最早入队的元素)会位于stackOut的顶部。如果stackOut不为空,则直接从stackOut弹出顶部元素(队列的前端元素)。
通过这种方式,stackOut的栈顶始终保持为队列的最前端,而stackIn用于处理新的入队操作。
class MyQueue {
Stack<Integer> stackIn;
Stack<Integer> stackOut;
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
public void push(int x) {
stackIn.push(x);
}
public int pop() {
// 将in栈的内容全部转移到out栈,从out栈进行输出
// 如果out栈有内容就先输出
if(stackOut.empty()){
while(!stackIn.empty()){
stackOut.push(stackIn.pop());
}
}
return stackOut.pop();
}
public int peek() {
if(stackOut.empty()){
while(!stackIn.empty()){
stackOut.push(stackIn.pop());
}
}
return stackOut.peek();
}
public boolean empty() {
return (stackIn.empty())&&(stackOut.empty());
}
}
/**
* 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();
*/
225 用队列实现栈
题目链接
这题也是利用两个队列来进行元素顺序的调整。
queue2是辅助队列,queue1存放进入栈的元素,当想要得到栈顶(队尾)元素,即把queue1的元素放入queue2,知道queue1只剩一个元素,该元素则为栈顶元素。将其弹出即可。剩余操作也是类似。
class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
queue2.offer(x); // 先放在辅助队列中
while (!queue1.isEmpty()){
queue2.offer(queue1.poll());
}
Queue<Integer> queueTemp;
queueTemp = queue1;
queue1 = queue2;
queue2 = queueTemp; // 最后交换queue1和queue2,将元素都放到queue1中
}
/** Removes the element on top of the stack and returns that element. */
public int pop() {
return queue1.poll(); // 因为queue1中的元素和栈中的保持一致,所以这个和下面两个的操作只看queue1即可
}
/** Get the top element. */
public int top() {
return queue1.peek();
}
/** Returns whether the stack is empty. */
public boolean empty() {
return queue1.isEmpty();
}
}
/**
* 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();
*/
python版本:
from queue import Queue
class MyStack:
def __init__(self):
self.queue1 = Queue()
def push(self, x):
# 临时队列,用于转移元素
temp_queue = Queue()
temp_queue.put(x) # 先放入新元素(栈顶元素)
# 将原队列中的元素转移到临时队列中,确保新元素始终在队列头部
while not self.queue1.empty():
temp_queue.put(self.queue1.get())
self.queue1 = temp_queue # 更新队列为新的队列
def pop(self):
# 直接从 queue1 中取出元素,因为 queue1 的队头是栈顶
return self.queue1.get()
def top(self):
# 获取队头元素即栈顶元素
top_element = self.queue1.get()
# 为保持队列状态,将该元素重新放回队头
temp_queue = Queue()
temp_queue.put(top_element)
while not self.queue1.empty():
temp_queue.put(self.queue1.get())
self.queue1 = temp_queue # 更新队列
return top_element
def empty(self):
# 如果 queue1 为空,则栈为空
return self.queue1.empty()
20 有效的括号
题目描述
很经典的栈的题目。
如果遇到左括号则要入栈,遇到右括号则与栈顶的元素配对,配对失败则是false,反之继续配对。这里要特别注意,右括号来的适合左括号可能为空,这是false。或者最后左括号剩余,这也是false。
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(int i=0; i<s.length(); i++){
char tmp = s.charAt(i);
if (tmp == '(' || tmp == '{' || tmp == '[') {
stack.push(tmp);
} else {
if (stack.empty()) return false; // 先检查栈是否为空
char top = stack.pop(); // 弹出栈顶元素以匹配
if (tmp == ')' && top != '(') return false;
if (tmp == '}' && top != '{') return false;
if (tmp == ']' && top != '[') return false;
}
}
return stack.empty();
}
}
python版本:
class Solution:
def isValid(self, s: str) -> bool:
stack = []
for char in s:
if char in '({[':
stack.append(char)
else:
if not stack:
return False # 检查栈是否为空
top = stack.pop() # 弹出栈顶元素以匹配
if char == ')' and top != '(':
return False
if char == '}' and top != '{':
return False
if char == ']' and top != '[':
return False
return not stack # 栈空则有效,非空则无效
当然,这题也可以用set(map)进行查找的优化,但意义不太大。比如如下代码:
import java.util.HashMap;
import java.util.Stack;
public class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
HashMap<Character, Character> map = new HashMap<>();
// 存储括号对应关系
map.put(')', '(');
map.put('}', '{');
map.put(']', '[');
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 如果是右括号
if (map.containsKey(c)) {
// 栈为空或栈顶元素不匹配当前右括号对应的左括号
if (stack.isEmpty() || stack.pop() != map.get(c)) {
return false;
}
} else {
// 否则为左括号,压入栈中
stack.push(c);
}
}
// 如果栈为空,说明所有括号都匹配成功
return stack.isEmpty();
}
}
1047 删除字符串中的所有相邻重复项
题目链接
第一眼还以为要双指针或者滑动窗口,但并不用,双指针往往是对数组/字符串/链表进行操作,滑动窗口则是找子序列/最大长度这种。
这题实际上就是栈的应用,没遇到一个新元素就入栈,如果栈顶元素与新的元素相同,则把栈顶元素出栈,以此类推。
关于java的StringBuider,看这篇:链接
class Solution {
public String removeDuplicates(String s) {
Stack<Character> stack = new Stack<>();
for(int i=0; i<s.length(); i++){
char tmp = s.charAt(i);
if(!stack.isEmpty() && stack.peek()==tmp){
stack.pop();
}else{
stack.push(tmp);
}
}
StringBuilder res = new StringBuilder();
for (char ch : stack) {
res.append(ch);
}
return res.toString();
}
}
python版本:join可以方便的把列表转换为字符串。如果不用join那会浪费一些时间。
class Solution:
def removeDuplicates(self, s: str) -> str:
stack = []
for char in s:
if stack and stack[-1] == char:
stack.pop()
else:
stack.append(char)
return ''.join(stack)
#或者这样
res = ''
for c in stack:
res = res + c
return res
150 逆波兰表达式求值
题目链接
题目很简单,如果了解后缀表达式很轻松能写出来,将数字存在栈中,遇到符号取出栈顶的2个元素计算,再将结果放回栈内即可。
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (String token : tokens) {
if (token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/")) {
int b = stack.pop(); // 先弹出的是第二个操作数
int a = stack.pop(); // 再弹出的是第一个操作数
switch (token) {
case "+":
stack.push(a + b);
break;
case "-":
stack.push(a - b);
break;
case "*":
stack.push(a * b);
break;
case "/":
stack.push(a / b);
break;
}
} else {
// 直接将字符串转换为整数并压栈
stack.push(Integer.parseInt(token));
}
}
// 最终栈顶元素就是表达式的结果
return stack.peek();
}
}
python版本:
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
res = []
print(int(6/(-132)))
for token in tokens:
if token not in {'+', '-', '*', '/'}:
res.append(int(token))
else:
a = res.pop()
b = res.pop()
if token == '+':
res.append(a+b)
elif token == '-':
res.append(b-a)
elif token == '*':
res.append(a*b)
elif token == '/':
res.append(int(b/a))
return res[0]
在Python中,对于整数除法,/ 操作符执行的是真除法(返回浮点结果),而 // 操作符执行的是地板除(即对结果向下取整到最近的整数)。因此,当使用 / 并将结果强制转换为 int 时,它只是简单地去掉了小数部分,不进行四舍五入,而且对于负数结果也只是截断小数部分。而使用 //,则是在计算结果后直接返回一个整数,且结果总是向下取整,这种方式与C++和Java中的整数除法一致。
对于正数除法:
- 5 / 2 结果为 2.5,int(5 / 2) 结果为 2
- 5 // 2 结果为 2。
对于负数除法:
- -5 / 2 结果为 -2.5,int(-5 / 2) 结果为 -2。
- -5 // 2 结果为 -3,因为 -2.5 向下取整是 -3。
因此这里要使用转换为int,而不是//。
239 滑动窗口最大值
题目描述
如果直接做这个题,不难发现时间复杂度是O(n*k),即遍历+遍历窗口即可。
但又没有能够线性时间内完成的方法呢?
单调队列是一种特殊的队列数据结构,其主要特点是队列中的元素单调递增或单调递减。这种队列在处理滑动窗口类型的问题时非常有用,能够高效地维护一个固定大小的窗口内最大值或最小值的集合。
单调队列的主要应用场景是解决滑动窗口类型的问题,例如:
- 查找滑动窗口中的最大值或最小值:给定一个数组和一个窗口大小,找出每个窗口内的最大值或最小值。
- 动态维护数据流中的最大值或最小值:在数据流不断到来时,动态更新当前窗口内的最大值或最小值。
- 优化某些动态规划问题:在动态规划问题中,如果状态转移涉及到一个固定大小的窗口,可以使用单调队列优化计算过程
那么怎么实现一个单调队列呢?
我们可以使用linkedlist来实现:
public class MonotonicQueue {
private LinkedList<int[]> deque;
//每个 int[] 数组包含两个元素:
//index:元素在原数组中的位置。
//value:元素的实际值。
public MonotonicQueue() {
deque = new LinkedList<>();
}
public void push(int index, int value) {
// 维护队列的单调递减性
while (!deque.isEmpty() && value > deque.getLast()[1]) {
// 如果大于队列末尾的值,则队尾值出队列,为value入队做铺垫
deque.removeLast();
}
deque.addLast(new int[]{index, value});
}
public void pop(int index) {
// 移除队列中过期的元素
if (!deque.isEmpty() && deque.getFirst()[0] == index) {
deque.removeFirst();
}
}
public int max() {
// 获取队列中的最大值
return deque.isEmpty() ? Integer.MIN_VALUE : deque.getFirst()[1];
}
}
实现单调队列后,就可以处理窗口的最大值。我们的单调队列里只保存窗口的最大值,窗口滑动时,队列里的值也会发生改动。因此,最后的代码为:
class MonotonicQueue {
private LinkedList<int[]> deque;
//每个 int[] 数组包含两个元素:
//index:元素在原数组中的位置。
//value:元素的实际值。
public MonotonicQueue() {
deque = new LinkedList<>();
}
public void push(int index, int value) {
// 维护队列的单调递减性
while (!deque.isEmpty() && value > deque.getLast()[1]) {
// 如果大于队列末尾的值,则队尾值出队列,为value入队做铺垫
deque.removeLast();
}
deque.addLast(new int[]{index, value});
}
public void pop(int index) {
// 移除队列中过期的元素
if (!deque.isEmpty() && deque.getFirst()[0] == index) {
deque.removeFirst();
}
}
public int max() {
// 获取队列中的最大值
return deque.isEmpty() ? Integer.MIN_VALUE : deque.getFirst()[1];
}
}
public class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
MonotonicQueue mq = new MonotonicQueue();
int[] result = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
mq.push(i, nums[i]);
if (i >= k - 1) {
result[i - k + 1] = mq.max();
mq.pop(i - k + 1);
}
}
return result;
}
}
python版本:
from collections import deque
class MonotonicQueue:
def __init__(self):
self.deque = deque()
def push(self, index, value):
# 维护队列的单调递减性
while self.deque and value > self.deque[-1][1]:
self.deque.pop()
self.deque.append((index, value))
def pop(self, index):
# 移除队列中过期的元素
if self.deque and self.deque[0][0] == index:
self.deque.popleft()
def max(self):
# 获取队列中的最大值
return self.deque[0][1] if self.deque else float('-inf')
class Solution:
def maxSlidingWindow(self, nums, k):
mq = MonotonicQueue()
result = []
for i, num in enumerate(nums):
mq.push(i, num)
if i >= k - 1:
result.append(mq.max())
mq.pop(i - k + 1)
return result
347 前k个高频元素
题目描述
题目思路很简单,可以用哈希表存储数字和频率,进而找到前k个频率输出即可。
如果不会用复杂的数据结构(比如堆),那可以用二维数组:
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
// 创建二维数组,一行存储一个数字及其频率
int[][] freqArray = new int[frequencyMap.size()][2];
int index = 0;
for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {
freqArray[index][0] = entry.getKey();
freqArray[index][1] = entry.getValue();
index++;
}
// 根据频率对二维数组进行排序
Arrays.sort(freqArray, (a, b) -> b[1] - a[1]);
// 从排序后的数组中取出前k个数字
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = freqArray[i][0];
}
return result;
}
}
但免不了的,这样效率很低下,所以我们想一想有没有别的办法。我们都知道,如果要找前k大/小的元素,用堆排序是很方便的。那么对于这题来说,不过是把找大小变为了频率的高低,其实也是找大小。
堆(Heap) 是一种完全二叉树,它满足两个特性:
- 结构性:堆是一个完全二叉树,即除了最后一层外,每一层都被完全填满,最后一层的节点从左向右填入。
- 堆序性:对于每个节点i,除了根节点外,都有父节点i/2(向下取整)。对于最小堆,每个节点的值都小于或等于其子节点的值;对于最大堆,每个节点的值都大于或等于其子节点的值。
在java中,PriorityQueue
类实现了堆的数据结构,并且提供了操作堆的方法。默认情况下,PriorityQueue
是一个最小堆,这意味着队列中的最小元素会优先被取出。具体内容可以看链接。
这里还有一个很关键的问题:我们的思路是用hashmap存储数字和对应的频率,然后用堆来筛选频率前k高的元素,那么我们要使用大根堆还是小根堆呢?
也许你会不假思索认为是大根堆,因为我们要保留的是前k大的元素,好像大根堆的性质更符合。但实际上不是这样的:如果用大根堆,我们只能确定最大值,对于根节点(最大值)的左右孩子谁更大这点不得而知。也许左子树的根节点小于右子树的根节点,同时也小于右子树的根节点的子节点,这样我们无法确认前k大的值是谁,我们只能很方便的确定最大值。如果大根堆目前有5个值,我们也要找前5大的频率,此时又来了一个值,我们需要遍历全部值才知道谁是最小的,然后将其剔除,这显而易见是很麻烦的。
如果用小根堆呢?小根堆我们每次都能精确的找到最小的值,我们也就不需要在剔除最小值是犯难了,而一次操作的时间复杂度也就是删除堆元素的时间复杂度而已。这无疑会优化我们的时间复杂度。
我们可以总结出如下的内容:
- 使用大根堆(Max Heap):
- 用于筛选或保留最大值。
- 常见于需要频繁访问或删除最大值的场景(保留小值)。
- 使用小根堆(Min Heap):
- 用于筛选或保留最小值。
- 常见于需要频繁访问或删除最小值的场景(保留大值)。
因此,代码如下:
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>(
(a, b) -> a.getValue() - b.getValue()
);
for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {
minHeap.add(entry);
if (minHeap.size() > k) {
minHeap.poll();
}
}
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--) {
result[i] = minHeap.poll().getKey();
}
return result;
}
}
如果用python就是这样:
import heapq
from collections import Counter
class Solution:
def topKFrequent(self, nums, k):
# 统计每个元素的频率
frequency_map = Counter(nums)
# 使用最小堆来保持频率最高的前k个元素
min_heap = []
for num, freq in frequency_map.items():
heapq.heappush(min_heap, (freq, num))
if len(min_heap) > k:
heapq.heappop(min_heap)
# 从最小堆中取出元素
result = [heapq.heappop(min_heap)[1] for _ in range(k)]
result.reverse()
return result