目录
232.用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(
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(双端队列)来模拟一个栈,只要是标准的栈操作即可。
示例 1:
输入: ["MyQueue", "push", "push", "peek", "pop", "empty"] [[], [1], [2], [], [], []] 输出: [null, null, null, 1, 1, false] 解释: MyQueue myQueue = new MyQueue(); myQueue.push(1); // queue is: [1] myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue) myQueue.peek(); // return 1 myQueue.pop(); // return 1, queue is [2] myQueue.empty(); // return false提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、peek
和empty
- 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop
或者peek
操作)
原理
该实现利用了两个栈的特性来模拟队列的先进先出(FIFO)行为:
核心思想
- 栈的特点是先进后出(LIFO)。
- 使用两个栈,一个负责存储入队元素(
stackIn
),一个用于逆序出队(stackOut
)。关键操作
- 入队(
push
)
入队时,直接将元素压入stackIn
,无需额外操作。- 出队(
pop
)
出队时,如果stackOut
为空,就将stackIn
的所有元素依次弹出并压入stackOut
。这样,stackOut
栈顶的元素就变成了队列的头部元素。
如果stackOut
不为空,则直接弹出栈顶元素即可。- 取队首元素(
peek
)
与pop
类似,但不弹出栈顶元素,仅返回栈顶元素的值。- 队列是否为空(
empty
)
当且仅当两个栈都为空时,队列为空。时间复杂度分析
- 入队(
push
):O(1)
入队操作始终只向stackIn
中压入元素。- 出队(
pop
):摊还时间复杂度 O(1)
元素从stackIn
转移到stackOut
的操作最多只会在每个元素入队和出队过程中发生一次。- 取队首元素(
peek
):摊还时间复杂度 O(1)
和pop
类似,转移操作的耗时可以摊销到每个元素的操作中。- 判断是否为空(
empty
):O(1)
只需检查两个栈的大小是否为 0。
代码
import java.util.Stack;
class MyQueue {
// 定义两个栈,一个用于存储新元素(stackIn),一个用于模拟队列的出队操作(stackOut)
private Stack<Integer> stackIn;
private Stack<Integer> stackOut;
// 构造函数,初始化两个栈
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
// push 方法:将元素 x 推到队列的末尾
public void push(int x) {
// 直接将元素推入 stackIn
stackIn.push(x);
}
// pop 方法:移除并返回队列开头的元素
public int pop() {
// 如果 stackOut 为空,将 stackIn 的所有元素依次弹出并推入 stackOut
// 这样就可以实现逆序,使得队列头部的元素位于 stackOut 栈顶
if (stackOut.size() == 0) {
int len = stackIn.size(); // 获取 stackIn 的大小
for (int i = 0; i < len; i++) {
stackOut.push(stackIn.pop());
}
}
// 弹出 stackOut 栈顶的元素,相当于移除队列的头部元素
return stackOut.pop();
}
// peek 方法:返回队列开头的元素但不移除
public int peek() {
// 如果 stackOut 为空,将 stackIn 的所有元素逆序存入 stackOut
if (stackOut.size() == 0) {
int len = stackIn.size();
for (int i = 0; i < len; i++) {
stackOut.push(stackIn.pop());
}
}
// 返回 stackOut 栈顶的元素,相当于队列的头部元素
return stackOut.peek();
}
// empty 方法:判断队列是否为空
public boolean empty() {
// 如果两个栈都为空,队列为空
return stackIn.size() == 0 && stackOut.size() == 0;
}
}
225.用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(
push
、top
、pop
和empty
)。实现
MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。注意:
- 你只能使用队列的标准操作 —— 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作。- 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
输入: ["MyStack", "push", "push", "top", "pop", "empty"] [[], [1], [2], [], [], []] 输出: [null, null, null, 2, 2, false] 解释: MyStack myStack = new MyStack(); myStack.push(1); myStack.push(2); myStack.top(); // 返回 2 myStack.pop(); // 返回 2 myStack.empty(); // 返回 False提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、top
和empty
- 每次调用
pop
和top
都保证栈不为空
原理
该实现利用了单个队列的特性,通过调整元素的排列顺序模拟栈的后入先出(LIFO)行为:
核心思想
- 队列的特点是先进先出(FIFO),但栈需要后入先出(LIFO)。
- 为了模拟栈的特性,每次
push
操作后,调整队列中元素的顺序,使得最后插入的元素始终在队列的最前面。关键操作
- 入栈(
push
)
将新元素插入队列尾部后,通过循环将队列中之前的元素逐个出队再入队,使得新元素成为队列的第一个元素(即栈顶)。- 出栈(
pop
)
队列的第一个元素即为栈顶元素,直接出队即可。- 查看栈顶元素(
top
)
队列的第一个元素即为栈顶元素,直接返回即可,不改变队列状态。- 判断是否为空(
empty
)
判断队列的大小是否为 0,如果是,则表示栈为空。时间复杂度
- push:O(n),因为需要将所有旧元素重新排列。
- pop:O(1),直接出队。
- top:O(1),直接访问队首元素。
- empty:O(1),判断队列是否为空。
代码
import java.util.Queue;
import java.util.LinkedList;
class MyStack {
// 定义一个队列 Qu,用于实现栈的功能
private Queue<Integer> Qu;
// 构造函数,初始化队列 Qu
public MyStack() {
Qu = new LinkedList<>();
}
// push 方法:将元素 x 压入栈顶
public void push(int x) {
int n = Qu.size(); // 获取当前队列的大小
Qu.offer(x); // 将元素 x 入队
// 将之前队列中的所有元素依次出队再重新入队,保持栈的后入先出特性
for (int i = 0; i < n; i++) {
Qu.offer(Qu.poll());
}
}
// pop 方法:移除并返回栈顶元素
public int pop() {
// 队列的首元素即为栈的栈顶元素,直接出队即可
return Qu.poll();
}
// top 方法:返回栈顶元素但不移除
public int top() {
// 队列的首元素即为栈的栈顶元素,直接返回即可
return Qu.peek();
}
// empty 方法:判断栈是否为空
public boolean empty() {
// 如果队列的大小为 0,表示栈为空
return Qu.size() == 0;
}
}
20.有效的括号
给你一个整数数组
nums
和一个整数k
,请你返回其中出现频率前k
高的元素。你可以按 任意顺序 返回答案。示例 1:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]示例 2:
输入: nums = [1], k = 1 输出: [1]提示:
1 <= nums.length <= 105
k
的取值范围是[1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前
k
个高频元素的集合是唯一的进阶:你所设计算法的时间复杂度 必须 优于
O(n log n)
,其中n
是数组大小。
原理
问题描述
给定一个只包含
(){}[]
的字符串,判断字符串中的括号是否成对出现且顺序正确。解题思路
栈的性质
栈是一种先进后出的数据结构,适合处理括号匹配问题。括号匹配要求:
- 左括号必须和对应的右括号匹配。
- 必须按照嵌套顺序成对出现。
具体实现
- 遍历字符串,遇到左括号时,将对应的右括号压入栈。
- 遇到右括号时,检查栈顶元素是否匹配。
- 如果栈为空或栈顶元素不匹配,返回
false
。- 如果匹配成功,弹出栈顶元素。
- 遍历结束后,检查栈是否为空。如果栈为空,表示所有括号都匹配;否则表示括号不匹配。
时间复杂度
- 时间复杂度:O(n),其中 n 是字符串的长度。每个字符最多入栈和出栈一次。
- 空间复杂度:O(n),最坏情况下,栈的大小与字符串长度相同。
代码
class Solution {
public boolean isValid(String s) {
// 使用双端队列(Deque)来模拟栈的操作
Deque<Character> deque = new LinkedList<>();
char ch;
// 遍历字符串中的每个字符
for (int i = 0; i < s.length(); i++) {
ch = s.charAt(i);
// 如果是左括号,将对应的右括号入栈
if (ch == '(') {
deque.push(')');
} else if (ch == '{') {
deque.push('}');
} else if (ch == '[') {
deque.push(']');
}
// 如果是右括号,检查栈顶是否匹配或者栈是否为空
else if (deque.isEmpty() || deque.peek() != ch) {
return false;
}
// 如果匹配成功,弹出栈顶元素
else {
deque.pop();
}
}
// 最后判断栈是否为空,空则匹配有效,否则无效
return deque.isEmpty();
}
}
1047.删除字符串中的所有相邻重复项
给出由小写字母组成的字符串
s
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在
s
上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca" 输出:"ca" 解释: 例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。提示:
1 <= s.length <= 105
s
仅由小写英文字母组成。
原理
问题描述
给定一个仅由小写字母组成的字符串
s
,重复项删除操作选择两个相邻且相同的字母并删除,重复执行该操作直到无法继续删除为止,返回最终的字符串。解题思路
栈的性质
- 栈是一种先进后出的数据结构。
- 在本问题中,利用栈存储非重复字符,并根据相邻相同的规则动态更新栈。
具体实现
- 遍历字符串
s
的每个字符。- 如果栈为空或栈顶元素与当前字符不相同,则将字符压入栈。
- 如果栈顶元素与当前字符相同,弹出栈顶元素,表示删除这一对重复字符。
- 最后,栈中剩余的元素即为去除所有重复项后的结果。
结果构建
- 栈中的字符按照后入先出的顺序存储,所以在拼接结果字符串时,需要从栈顶开始,逐个拼接到字符串的前面。
时间复杂度
- 时间复杂度:O(n),每个字符最多入栈和出栈一次。
- 空间复杂度:O(n),最坏情况下栈中存储所有字符(当没有重复字符时)。
代码
class Solution {
public String removeDuplicates(String S) {
// 使用 ArrayDeque 作为栈,比 LinkedList 更高效(除删除操作外)
ArrayDeque<Character> deque = new ArrayDeque<>();
char ch;
// 遍历字符串中的每个字符
for (int i = 0; i < S.length(); i++) {
ch = S.charAt(i);
// 如果栈为空或栈顶元素与当前字符不相同,则将字符压入栈
if (deque.isEmpty() || deque.peek() != ch) {
deque.push(ch);
}
// 如果栈顶元素与当前字符相同,则弹出栈顶元素,表示删除重复项
else {
deque.pop();
}
}
// 拼接栈中剩余的字符为结果字符串
String str = "";
while (!deque.isEmpty()) {
// 因为栈是后入先出,所以拼接时需将栈顶字符放在前面
str = deque.pop() + str;
}
// 返回最终的字符串
return str;
}
}
150.逆波兰表达式求值
给你一个字符串数组
tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = ["2","1","+","3","*"] 输出:9 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9示例 2:
输入:tokens = ["4","13","5","/","+"] 输出:6 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] 输出:22 解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = ((10 * (6 / (12 * -11))) + 17) + 5 = ((10 * (6 / -132)) + 17) + 5 = ((10 * 0) + 17) + 5 = (0 + 17) + 5 = 17 + 5 = 22提示:
1 <= tokens.length <= 104
tokens[i]
是一个算符("+"
、"-"
、"*"
或"/"
),或是在范围[-200, 200]
内的一个整数逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如
( 1 + 2 ) * ( 3 + 4 )
。- 该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) * )
。逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 + *
也可以依据次序计算出正确结果。- 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
原理
问题描述
逆波兰表达式(后缀表达式)是一种将运算符写在操作数后面的算术表达式。它的计算过程不需要括号,并且运算顺序是固定的。其优点包括表达式无歧义和适合用栈操作。
解题思路
栈的特点
栈是一种后进先出的数据结构,适合处理后缀表达式的运算顺序。
- 遇到操作数:将其压入栈。
- 遇到运算符:弹出栈顶的两个操作数,计算结果后压回栈。
计算过程
- 遍历逆波兰表达式中的每个符号。
- 对于操作数,直接压入栈。
- 对于运算符,从栈中弹出两个操作数,根据运算符执行计算后,将结果压入栈。
特殊注意
- 对于减法和除法,注意运算顺序:后弹出的数是左操作数,先弹出的数是右操作数。
- 加法和乘法的顺序对结果没有影响。
时间复杂度
- 时间复杂度:O(n),每个符号只需处理一次,栈的操作为 O(1)。
- 空间复杂度:O(n),栈的最大深度取决于操作数的数量。
代码
class Solution {
public int evalRPN(String[] tokens) {
// 使用栈来辅助计算逆波兰表达式的值
Deque<Integer> stack = new LinkedList<>();
// 遍历逆波兰表达式中的每个符号
for (String s : tokens) {
if ("+".equals(s)) { // 如果当前符号是加法运算符
// 弹出栈顶两个数相加后,将结果压入栈
stack.push(stack.pop() + stack.pop());
} else if ("-".equals(s)) { // 如果当前符号是减法运算符
// 弹出栈顶两个数,计算顺序为先弹出的数减去后弹出的数
stack.push(-stack.pop() + stack.pop());
} else if ("*".equals(s)) { // 如果当前符号是乘法运算符
// 弹出栈顶两个数相乘后,将结果压入栈
stack.push(stack.pop() * stack.pop());
} else if ("/".equals(s)) { // 如果当前符号是除法运算符
// 弹出栈顶两个数,计算顺序为先弹出的数除以后弹出的数
int temp1 = stack.pop(); // 被除数
int temp2 = stack.pop(); // 除数
stack.push(temp2 / temp1);
} else { // 如果当前符号是操作数
// 将操作数压入栈
stack.push(Integer.valueOf(s));
}
}
// 返回栈顶的最终结果
return stack.pop();
}
}
239.滑动窗口最大值
给你一个整数数组
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示例 2:
输入:nums = [1], k = 1 输出:[1]提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
原理
问题描述
给定一个大小为
k
的滑动窗口从数组的最左侧移动到最右侧,每次只向右移动一位,要求返回每个窗口中的最大值。该问题可通过滑动窗口技术和双端队列(deque)优化来求解。解题思路
使用双端队列:
双端队列允许我们在两端同时进行添加和删除操作。在这里,我们将队列存储数组元素的索引,并确保队列中的元素索引按其对应值从大到小排列。这样队列的首部元素即为当前窗口中的最大值。滑动窗口的操作:
- 对于每一个新元素(
nums[i]
),我们需要:
- 移除队列中超出当前滑动窗口范围的元素的索引。
- 移除队列中比当前元素小的元素的索引,因为这些元素将不会成为当前或未来窗口的最大值。
- 将当前元素的索引加入队列。
- 当滑动窗口的大小达到
k
时,我们将队列首部的元素对应的值(即当前窗口的最大值)存入结果数组中。优化:
- 每个元素只会被加入队列一次,并且最多被移除一次,因此时间复杂度为 O(n),其中
n
是数组的长度。时间复杂度
- 时间复杂度:O(n),每个元素被加入和移出队列的次数最多为1次,因此总的操作次数为O(n)。
- 空间复杂度:O(k),队列中最多存储
k
个元素的索引。
代码
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// 使用双端队列(ArrayDeque)来存储滑动窗口中的元素的索引
ArrayDeque<Integer> deque = new ArrayDeque<>();
// 用于存储滑动窗口中每次移动后最大值的结果
int[] ans = new int[nums.length - k + 1];
// 用于记录结果数组的索引
int index = 0;
// 特殊情况:如果窗口大小为1,直接返回原数组
if (k == 1) {
return nums;
}
// 遍历数组中的每个元素
for (int i = 0; i < nums.length; i++) {
// 删除不在当前滑动窗口范围内的元素的索引
while (!deque.isEmpty() && deque.peek() < (i - k + 1)) {
deque.poll(); // poll() 删除队列首部元素
}
// 删除队列中所有小于当前元素的索引,因为它们不可能成为未来滑动窗口的最大值
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast(); // pollLast() 删除队列尾部元素
}
// 将当前元素的索引加入队列
deque.offer(i);
// 当滑动窗口的大小达到k时,将队列首部元素对应的值存入结果数组
if (i >= k - 1) {
ans[index++] = nums[deque.peek()]; // peek() 获取队列首部的元素索引
}
}
// 返回结果数组
return ans;
}
}
347.前k个高频元素
给你一个整数数组
nums
和一个整数k
,请你返回其中出现频率前k
高的元素。你可以按 任意顺序 返回答案。示例 1:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]示例 2:
输入: nums = [1], k = 1 输出: [1]提示:
1 <= nums.length <= 105
k
的取值范围是[1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前
k
个高频元素的集合是唯一的进阶:你所设计算法的时间复杂度 必须 优于
O(n log n)
,其中n
是数组大小。
原理
问题描述
给定一个整数数组
nums
和一个整数k
,需要返回该数组中出现频率前k
高的元素,并可以按任意顺序返回这些元素。解题思路
使用 HashMap 统计频率:
- 首先,我们遍历数组
nums
,利用HashMap
来记录每个元素的出现频率。map.put(num, map.getOrDefault(num, 0) + 1)
用于对每个元素的频率进行更新。使用优先队列(最小堆)进行排序:
- 由于我们需要找到频率最高的
k
个元素,最直接的做法是使用优先队列。通过定义比较器(o1, o2) -> (o1[1] - o2[1])
,优先队列会根据频率值从小到大排序。- 在优先队列中,我们保存每个元素及其频率的数组
temp[0]
(元素)和temp[1]
(频率)。维持一个大小为
k
的优先队列:
- 每次将新的元素和其频率放入队列时,我们检查队列的大小。如果队列的大小超过
k
,则移除频率最低的元素,即queue.poll()
。这样可以确保队列始终只保存频率最高的k
个元素。返回结果:
- 最后,我们从优先队列中依次取出前
k
个频率最高的元素,并将它们放入结果数组res
中。时间复杂度
统计频率(HashMap 操作):
- 遍历
nums
数组一次,时间复杂度为 O(n),其中n
是数组的长度。维护优先队列:
- 对于每个不同的元素,我们将其插入到优先队列中,插入操作的时间复杂度为 O(log k)。如果数组中有
m
个不同的元素(m ≤ n
),则总的插入操作复杂度为 O(m log k)。最终结果提取:
- 从优先队列中提取出
k
个元素,时间复杂度为 O(k log k),这是一个相对较小的操作,通常认为是常数级别。因此,总体时间复杂度为 O(n + m log k),其中
n
是数组的长度,m
是不同元素的个数,k
是所需的频率最高的元素个数。空间复杂度
- 空间复杂度:主要使用了
HashMap
和PriorityQueue
,它们的空间复杂度分别为 O(m) 和 O(k)。因此,总的空间复杂度为 O(m + k)。
代码
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 使用 HashMap 来记录数组中每个元素出现的频率
HashMap<Integer, Integer> map = new HashMap<>();
// 使用优先队列(最小堆)来保存频率最高的 k 个元素
// 根据元素频率排序,频率较低的元素在队列头部
PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> (o1[1] - o2[1]));
// 用于存储返回的 k 个频率最高的元素
int[] res = new int[k];
// 遍历数组,统计每个元素的出现次数
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
// 将每个元素及其频率放入优先队列
for (var x : map.entrySet()) {
int[] temp = new int[2];
temp[0] = x.getKey(); // 元素值
temp[1] = x.getValue(); // 元素频率
// 将元素放入队列,保持队列中最多只包含 k 个元素
queue.offer(temp);
// 如果队列的大小超过 k,则移除频率最低的元素
if (queue.size() > k) {
queue.poll();
}
}
// 从优先队列中取出前 k 个频率最高的元素
for (int i = 0; i < k; i++) {
res[i] = queue.poll()[0]; // 取出元素值
}
// 返回结果数组
return res;
}
}