【LeetCode 每日一题】2349. 设计数字容器系统
Problem: 2349. 设计数字容器系统
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度
- 空间复杂度
整体思路
这个 NumberContainers
类的设计目标是实现一个特殊的数据容器,它需要支持两种核心操作:
change(index, number)
:在指定的index
处放置一个number
。这个操作可能会覆盖该索引上原有的数字。find(number)
:找出存储着特定number
的所有索引中,最小的那个索引。
为了高效地实现这两个操作,该类巧妙地使用了两种 HashMap
结合一种 TreeSet
的数据结构组合。
-
数据结构的选择与职责:
Map<Integer, Integer> indexToValue
: 这个HashMap
负责直接映射。它的键是index
,值是在该索引处存储的number
。- 职责:提供 O(1) 平均时间复杂度的能力来查询任意
index
上的值。这在change
操作中用于查找需要被替换的旧值。
- 职责:提供 O(1) 平均时间复杂度的能力来查询任意
Map<Integer, TreeSet<Integer>> valueToIndices
: 这是整个设计的核心。它是一个嵌套的数据结构。- 外层
HashMap
:键是number
,值是一个TreeSet
。它负责将一个数字与所有存储它的索引关联起来。 - 内层
TreeSet
:TreeSet
是一个基于红黑树实现的、自动保持元素有序的集合。 - 职责:
- 高效地存储一个数字出现的所有索引。
- 自动对这些索引进行升序排序。
- 提供 O(log N) 时间复杂度的
add
和remove
操作。 - 提供 O(1) 时间复杂度的
first()
操作,以快速获取最小的索引,这正是find
操作所需要的。
- 外层
-
change(index, number)
操作的逻辑:- 处理旧值:首先,需要检查
index
处之前是否已经有值。通过indexToValue.get(index)
查询。- 如果存在一个
oldValue
,就必须从valueToIndices
中oldValue
对应的TreeSet
里移除index
。这是为了保持数据的一致性,因为index
处的值即将改变。
- 如果存在一个
- 更新新值:
- 在
indexToValue
中,更新或插入(index, number)
的映射。 - 在
valueToIndices
中,首先要找到number
对应的TreeSet
。computeIfAbsent(number, k -> new TreeSet<>())
是一个优雅的写法:如果number
的TreeSet
不存在,就创建一个新的;如果存在,就返回现有的。- 然后,将
index
添加到这个TreeSet
中。TreeSet
会自动将其放在正确的位置以维持排序。
- 在
- 处理旧值:首先,需要检查
-
find(number)
操作的逻辑:- 查找索引集合:直接从
valueToIndices
中获取number
对应的TreeSet
。 - 处理不存在的情况:如果获取到的
TreeSet
是null
(表示这个number
从未被存储过)或者isEmpty()
(表示这个number
曾经存在但现在所有位置都被替换了),则根据题目要求返回-1
。 - 返回最小索引:如果
TreeSet
存在且不为空,调用其first()
方法。由于TreeSet
是有序的,first()
能在 O(1) 时间内返回其中最小的元素,即最小的索引。
- 查找索引集合:直接从
完整代码
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;class NumberContainers {// 映射1:索引 → 当前存储的数字。// 提供 O(1) 时间复杂度的索引值查询。private final Map<Integer, Integer> indexToValue = new HashMap<>();// 映射2:数字 → 所有包含该数字的索引集合(使用 TreeSet 自动升序排列)。// Key: number, Value: a sorted set of indices where this number is located.private final Map<Integer, TreeSet<Integer>> valueToIndices = new HashMap<>();public NumberContainers() {// 构造函数,此处无需特殊初始化}/*** 在指定索引处更改或放置一个数字。* @param index 要操作的索引* @param number 要放置的数字*/public void change(int index, int number) {// 步骤 1: 处理可能存在的旧值// 从 indexToValue 中查找该索引之前存储的值Integer oldValue = indexToValue.get(index);if (oldValue != null) {// 如果存在旧值,则必须从旧值对应的索引集合中移除当前索引valueToIndices.get(oldValue).remove(index);}// 步骤 2: 更新两个映射以反映新值// 更新 index -> number 的映射indexToValue.put(index, number);// 更新 number -> {..., index, ...} 的映射// computeIfAbsent: 如果 number 还没有对应的 TreeSet,则创建一个新的// .add(index): 将当前索引添加到该 TreeSet 中,它会自动保持排序valueToIndices.computeIfAbsent(number, k -> new TreeSet<>()).add(index);}/*** 查找存储指定数字的最小索引。* @param number 要查找的数字* @return 最小的索引;如果该数字不存在,则返回 -1。*/public int find(int number) {// 从 valueToIndices 中获取该数字对应的所有索引TreeSet<Integer> indices = valueToIndices.get(number);// 检查两种“不存在”的情况:// 1. 该数字从未被 change 过 (indices == null)// 2. 该数字曾经存在,但所有位置都已被其他数字覆盖 (indices.isEmpty())if (indices == null || indices.isEmpty()) {return -1;} else {// TreeSet.first() 可以在 O(1) 时间内返回集合中最小的元素return indices.first();}}
}/*** Your NumberContainers object will be instantiated and called as such:* NumberContainers obj = new NumberContainers();* obj.change(index,number);* int param_2 = obj.find(number);*/
时空复杂度
- 设
N
为change
操作的总调用次数。 - 设
M
为find
操作的总调用次数。 - 在任意时刻,设某个数字
k
对应的索引数量为C_k
。
时间复杂度
-
change(index, number)
:indexToValue.get(index)
: O(1) 平均。valueToIndices.get(oldValue)
: O(1) 平均。TreeSet.remove(index)
: O(logC_{oldValue}
)。C_{oldValue}
是旧值对应的索引数。indexToValue.put(index, number)
: O(1) 平均。valueToIndices.computeIfAbsent(number, ...)
: O(1) 平均。TreeSet.add(index)
: O(logC_{number}
)。C_{number}
是新值对应的索引数。- 综合:
change
操作的时间复杂度主要由TreeSet
的操作决定,为 O(log K),其中K
是与操作相关的数字(oldValue
或number
)出现的次数。
-
find(number)
:valueToIndices.get(number)
: O(1) 平均。TreeSet.isEmpty()
: O(1)。TreeSet.first()
: O(1)。- 综合:
find
操作的时间复杂度为 O(1) 平均。
空间复杂度
- 主要存储开销:由两个
Map
决定。 indexToValue
: 存储了所有被change
操作接触过的唯一index
。如果change
操作涉及U
个不同的索引,则其空间为 O(U)。valueToIndices
:- 外层
Map
的键是所有出现过的唯一number
。 - 内层的
TreeSet
存储了这些number
对应的所有index
。 - 所有
TreeSet
中元素的总数,等于indexToValue
中元素的总数,即U
。 - 因此,
valueToIndices
的总空间复杂度也是 O(U)。
- 外层
综合分析:
类的总空间复杂度为 O(U),其中 U
是被 change
操作触及过的唯一索引的总数。