你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。
比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops
,其中 ops[i]
是你需要记录的第 i
项操作,ops
遵循下述规则:
- 整数
x
- 表示本回合新获得分数x
"+"
- 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。"D"
- 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。"C"
- 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。
请你返回记录中所有得分的总和。
示例 1:
输入:ops = ["5","2","C","D","+"] 输出:30 解释: "5" - 记录加 5 ,记录现在是 [5] "2" - 记录加 2 ,记录现在是 [5, 2] "C" - 使前一次得分的记录无效并将其移除,记录现在是 [5]. "D" - 记录加 2 * 5 = 10 ,记录现在是 [5, 10]. "+" - 记录加 5 + 10 = 15 ,记录现在是 [5, 10, 15]. 所有得分的总和 5 + 10 + 15 = 30
示例 2:
输入:ops = ["5","-2","4","C","D","9","+","+"] 输出:27 解释: "5" - 记录加 5 ,记录现在是 [5] "-2" - 记录加 -2 ,记录现在是 [5, -2] "4" - 记录加 4 ,记录现在是 [5, -2, 4] "C" - 使前一次得分的记录无效并将其移除,记录现在是 [5, -2] "D" - 记录加 2 * -2 = -4 ,记录现在是 [5, -2, -4] "9" - 记录加 9 ,记录现在是 [5, -2, -4, 9] "+" - 记录加 -4 + 9 = 5 ,记录现在是 [5, -2, -4, 9, 5] "+" - 记录加 9 + 5 = 14 ,记录现在是 [5, -2, -4, 9, 5, 14] 所有得分的总和 5 + -2 + -4 + 9 + 5 + 14 = 27
示例 3:
输入:ops = ["1"] 输出:1
提示:
1 <= ops.length <= 1000
ops[i]
为"C"
、"D"
、"+"
,或者一个表示整数的字符串。整数范围是[-3 * 104, 3 * 104]
- 对于
"+"
操作,题目数据保证记录此操作时前面总是存在两个有效的分数 - 对于
"C"
和"D"
操作,题目数据保证记录此操作时前面总是存在一个有效的分数
思路:
To solve this problem, we can use a stack, which is perfectly suited for situations where we need to keep track of a list of elements and frequently add or remove items from the end.
为了解决这个问题,我们可以使用堆栈 ,它非常适合我们需要跟踪元素列表并经常在末尾添加或删除项目的情况。
- Iterate through each operation in the
operations
list.
遍历operations
列表中的每个作。 - If the operation is a positive or negative integer, convert it to an integer and push it onto the stack.
如果运算是正整数或负整数,请将其转换为整数并将其推送到堆栈上。 - If the operation is a
'+'
, calculate the sum of the last two scores in the stack and push the result back onto the stack.
如果作是'+'
,则计算堆栈中最后两个分数的总和,并将结果推送回堆栈。 - If the operation is a
'D'
, double the last score in the stack (using the left shift operation<< 1
for efficiency, which is equivalent to multiplying by 2) and push the result onto the stack.
如果运算是'D'
,则将堆栈中的最后一个分数加倍(使用左移运算<< 1
以提高效率,相当于乘以 2)并将结果推送到堆栈上。 - If the operation is a
'C'
, pop the last score off the stack to remove it from the record.
如果作是'C'
,则从堆栈中弹出最后一个分数以将其从记录中删除。 - After processing all operations, the stack will contain all the valid scores. Summing these scores will give us the final result required.
处理完所有作后,堆栈将包含所有有效分数。将这些分数相加将得到所需的最终结果。
class Solution {
public int calPoints(String[] ops) {
// 创建一个双端队列作为栈,用于记录得分
Deque<Integer> stack = new ArrayDeque<>();
// 遍历所有操作
for (String op : ops) {
switch (op) {
case "+": // 操作符为"+"时,计算最后两个得分的和
int last = stack.pop(); // 弹出最后一个得分(临时保存)
int newTop = stack.peek(); // 查看当前栈顶元素(不弹出)
stack.push(last); // 将刚才弹出的得分重新压入栈
stack.push(last + newTop); // 压入最后两个得分的和
break;
case "D": // 操作符为"D"时,将最后一个得分翻倍
stack.push(stack.peek() * 2); // 取栈顶元素翻倍后压入栈
break;
case "C": // 操作符为"C"时,删除最后一个得分
stack.pop(); // 弹出并删除栈顶元素
break;
default: // 其他情况为数字,直接解析为整数压入栈
stack.push(Integer.parseInt(op)); // 将字符串转为整数后压入栈
break;
}
}
// 计算栈中所有得分的总和
int sum = 0;
for (int score : stack) {
sum += score; // 累加每个得分
}
return sum; // 返回最终总分
}
}
在 Java 中,Deque
是一个接口,代表双端队列(Double Ended Queue),它允许在队列的两端(头部和尾部)高效地插入、删除和访问元素。Deque
接口继承自Queue
接口,提供了比普通队列更丰富的操作方法。
关键知识点:
-
Deque
接口:- 支持在队列两端进行元素操作(插入、删除、查看)
- 常用方法:
java
// 头部操作 addFirst(E e); // 在头部插入元素(失败抛异常) offerFirst(E e); // 在头部插入元素(返回布尔值) removeFirst(); // 删除并返回头部元素(队列为空时抛异常) pollFirst(); // 删除并返回头部元素(队列为空时返回null) getFirst(); // 返回头部元素(队列为空时抛异常) peekFirst(); // 返回头部元素(队列为空时返回null) // 尾部操作(与头部操作对称) addLast(E e); offerLast(E e); removeLast(); pollLast(); getLast(); peekLast();
-
ArrayDeque
类:ArrayDeque
是Deque
接口的动态数组实现,内部使用循环数组存储元素- 特点:
- 无容量限制,按需自动扩容
- 不允许存储
null
元素 - 线程不安全(适合单线程环境)
- 插入、删除、访问操作均为 O (1) 时间复杂度
-
作为栈使用:
Deque
可以直接作为栈(Stack)使用,替代 Java 早期的Stack
类- 对应方法映射:
java
push(E e) → addFirst(e) // 入栈 pop() → removeFirst() // 出栈 peek() → peekFirst() // 查看栈顶元素
为什么选择Deque<Integer> stack = new ArrayDeque<>();
?
在你提供的代码中,使用Deque
接口和ArrayDeque
实现的原因是:
- 栈操作需求:代码中需要进行栈操作(如
push
、pop
、peek
) - 性能考虑:
ArrayDeque
比传统的Stack
类性能更高(Stack
继承自Vector
,线程安全但有额外开销) - 代码规范:面向接口编程(使用
Deque
接口而非具体实现类)
示例对比:
java
// 传统方式(不推荐)
Stack<Integer> stack = new Stack<>();
// 推荐方式(使用Deque接口)
Deque<Integer> stack = new ArrayDeque<>();
其他常见实现类:
LinkedList
:基于链表实现的双端队列,支持null
元素ConcurrentLinkedDeque
:线程安全的双端队列,适合多线程环境
通过使用Deque
接口和ArrayDeque
实现,代码获得了更好的灵活性和性能,同时遵循了 Java 的最佳实践。
ArrayDeque是Deque接口的动态数组实现,除了ArrayDeque,还有什么常见的Deque接口的动态数组实现??
在 Java 中,Deque
接口的动态数组实现主要以循环数组为核心结构,通过自动扩容机制支持动态增长。除了ArrayDeque
外,没有其他标准库中的动态数组实现。但可以从以下角度补充说明:
1. 唯一的标准实现:ArrayDeque
- 核心特点:
- 使用循环数组存储元素,通过头尾指针实现双端操作。
- 初始容量为 16,每次扩容时翻倍(
newCapacity = oldCapacity << 1
)。 - 不允许
null
元素,线程不安全,适合单线程环境。
- 性能优势:
- 随机访问、头部 / 尾部插入删除均为 O (1) 时间复杂度。
- 相比
LinkedList
(链表实现),在批量操作时内存局部性更好。
2. 其他Deque
实现类对比
类名 | 数据结构 | 是否动态扩容 | 允许null | 线程安全 |
---|---|---|---|---|
ArrayDeque | 循环数组 | 是 | 否 | 否 |
LinkedList | 双向链表 | 是(按需新增节点) | 是 | 否 |
ConcurrentLinkedDeque | 无界双向链表 | 是 | 否 | 是(CAS 实现) |
3. 为什么没有其他动态数组实现?
- 设计权衡:
- 动态数组实现需要处理扩容、数据搬移等复杂性,而
ArrayDeque
已优化至极致(如使用位运算取模)。 - 链表实现(如
LinkedList
)更适合频繁插入删除中间元素的场景,动态数组在这方面并无优势。
- 动态数组实现需要处理扩容、数据搬移等复杂性,而
- 第三方库:
- 若需线程安全的动态数组实现,可考虑 Apache Commons Collections 中的
CircularFifoQueue
(固定容量循环队列),但它仅实现Queue
接口,不支持双端操作。
- 若需线程安全的动态数组实现,可考虑 Apache Commons Collections 中的
4. 使用建议
- 优先选择
ArrayDeque
:- 若需栈或队列功能,且无需存储
null
。 - 例如:LeetCode 题目中常见的栈操作(如逆波兰表达式求值)。
- 若需栈或队列功能,且无需存储
- 选择
LinkedList
:- 需频繁插入 / 删除中间元素。
- 需支持
null
元素。
- 选择
ConcurrentLinkedDeque
:- 多线程环境下的双端队列操作。
总结
Java 标准库中,ArrayDeque
是唯一的动态数组实现的双端队列。其他实现类(如LinkedList
)基于链表结构,各有适用场景。若需线程安全,可考虑ConcurrentLinkedDeque
(链表实现)或第三方库。
这种代码写法称为增强 for 循环(Enhanced For Loop),也被称为for-each 循环。它是 Java 5 引入的语法糖,用于简化集合和数组的遍历。
1. 语法格式
java
for (元素类型 变量名 : 可迭代对象) {
// 使用变量处理每个元素
}
示例(遍历数组和集合):
java
// 遍历数组
int[] numbers = {1, 2, 3};
for (int num : numbers) {
System.out.println(num);
}
// 遍历集合
List<String> names = List.of("Alice", "Bob");
for (String name : names) {
System.out.println(name);
}
2. 使用条件
-
可迭代对象:
- 必须实现
java.lang.Iterable
接口(如Collection
的所有子类:List
、Set
、Queue
等)。 - 或为数组(包括基本类型数组和对象数组)。
- 必须实现
-
元素类型匹配:
- 声明的变量类型必须与集合 / 数组中的元素类型兼容(可隐式转换)。
错误示例:
java
List<Integer> list = List.of(1, 2, 3);
for (String s : list) { // 编译错误:类型不匹配
System.out.println(s);
}
3. 常见应用场景
-
遍历集合:
java
Set<String> fruits = new HashSet<>(Set.of("apple", "banana")); for (String fruit : fruits) { System.out.println(fruit); }
-
遍历数组:
java
double[] prices = {10.5, 20.3}; for (double price : prices) { System.out.println(price); }
-
处理结果集:
java
Deque<Integer> stack = new ArrayDeque<>(); // ... 添加元素到栈 for (int score : stack) { // 遍历栈中的所有元素 sum += score; }
4. 优点
-
代码简洁:
- 无需显式声明迭代器或索引变量,减少样板代码。
-
减少错误:
- 避免手动操作索引导致的越界异常(如
IndexOutOfBoundsException
)。
- 避免手动操作索引导致的越界异常(如
-
支持多态遍历:
- 可统一处理不同实现的
Iterable
对象(如ArrayList
、LinkedList
、TreeSet
等)。
- 可统一处理不同实现的
-
语法清晰:
- 直观表达 “对每个元素执行操作” 的语义,增强可读性。
5. 局限性
-
单向遍历:
- 只能从前到后顺序遍历,无法反向遍历或跳过元素。
-
不可修改集合:
- 在遍历过程中修改集合结构(如添加、删除元素)会抛出
ConcurrentModificationException
。
- 在遍历过程中修改集合结构(如添加、删除元素)会抛出
-
无索引访问:
- 无法直接获取当前元素的索引,需额外维护计数器。
6. 替代方案
-
传统 for 循环(需索引时):
java
List<String> names = List.of("Alice", "Bob"); for (int i = 0; i < names.size(); i++) { System.out.println("Index " + i + ": " + names.get(i)); }
-
迭代器(需修改集合时):
java
List<String> names = new ArrayList<>(List.of("Alice", "Bob")); Iterator<String> it = names.iterator(); while (it.hasNext()) { if (it.next().equals("Bob")) { it.remove(); // 安全删除元素 } }
-
Java 8+ Stream API(函数式风格):
java
List<Integer> numbers = List.of(1, 2, 3); numbers.forEach(System.out::println); // 方法引用
总结
增强 for 循环是 Java 中遍历集合和数组的首选方式,适用于无需索引、顺序遍历、只读操作的场景。它通过简化语法提高了代码的可读性和安全性,但在需要索引或修改集合结构时需使用其他方式。