leetcode
leetcode 2051 ~ 2100
设计数字容器系统

设计数字容器系统

难度:

标签:

题目描述

代码结果

运行时间: 341 ms, 内存: 72.1 MB


/*
 * 思路:
 * 1. 使用两个映射来实现:一个映射indexToNumber记录每个下标对应的数字;另一个映射numberToIndices记录每个数字对应的所有下标。
 * 2. change方法将数字插入或替换到指定下标,并相应更新两个映射。
 * 3. find方法返回给定数字的最小下标。
 * 4. 使用Java Stream API简化find方法。
 */
import java.util.*;
import java.util.stream.*;

public class NumberContainers {
    private Map<Integer, Integer> indexToNumber;
    private Map<Integer, TreeSet<Integer>> numberToIndices;

    public NumberContainers() {
        indexToNumber = new HashMap<>();
        numberToIndices = new HashMap<>();
    }

    public void change(int index, int number) {
        if (indexToNumber.containsKey(index)) {
            int oldNumber = indexToNumber.get(index);
            if (numberToIndices.containsKey(oldNumber)) {
                numberToIndices.get(oldNumber).remove(index);
                if (numberToIndices.get(oldNumber).isEmpty()) {
                    numberToIndices.remove(oldNumber);
                }
            }
        }
        indexToNumber.put(index, number);
        numberToIndices.putIfAbsent(number, new TreeSet<>());
        numberToIndices.get(number).add(index);
    }

    public int find(int number) {
        return numberToIndices.getOrDefault(number, new TreeSet<>()).stream().findFirst().orElse(-1);
    }
}

解释

方法:

此题解采用了散列表和最小堆的组合来实现数字容器系统。散列表 `m` 用于保存 `index` 和 `number` 的映射,以便在 O(1) 时间内访问或更新任何索引处的数字。另一方面,`ms` 也是一个散列表,但其值是一个最小堆,用于存储每个数字的所有索引。通过最小堆,我们可以快速访问每个数字的最小索引。在 `change` 方法中,每次改变或插入一个数字时,都会更新散列表 `m` 和对应数字的堆。在 `find` 方法中,为了找到某个数字的最小索引,我们检查堆顶元素是否仍然有效(即堆顶索引处的数字与查询的数字匹配),如果不匹配则将堆顶元素弹出,直到堆顶元素有效或堆为空。

时间复杂度:

O(log n) for `change`, O(log n) average for `find`, but can degrade to O(n) in worst case

空间复杂度:

O(I + N) where I is the number of unique indices and N is the total number of operations

代码细节讲解

🦆
为什么选择使用散列表来存储索引到数字的映射?是否有其他数据结构能实现相同的功能?
散列表被选用是因为它提供了极快的平均时间复杂度为 O(1) 的访问和更新操作。这对于频繁变更和查询索引对应的数值是非常有效的。虽然像平衡二叉搜索树(如 AVL 树或红黑树)等数据结构也可以用于索引到数字的映射,并且提供 O(log n) 的时间复杂度进行查找、插入和删除操作,但其性能不如散列表。因此,散列表是实现快速访问和更新的首选数据结构。
🦆
在`change`方法中,如果同一个索引多次更新为不同的数字,最小堆中是否会存在过期的索引?这会如何影响性能?
是的,如果同一个索引被多次更新为不同的数字,最小堆中确实会存在过期的索引。这是因为在`change`方法中,每次更改索引处的数字时,该索引都会被添加到新数字对应的最小堆中,而老的数字在其对应的堆中的索引并没有被移除。这会导致堆中存在一些不再有效的索引,从而在`find`方法中影响性能,因为可能需要多次弹出这些过时的堆顶元素来找到一个有效的最小索引。
🦆
题解中提到在`find`操作中可能需要连续弹出多个过时的堆顶元素,为什么不在`change`方法中直接处理这些过时的堆顶元素?
在`change`方法中不直接处理过时的堆顶元素是为了保持方法的简单性和高效性。每次在`change`方法中处理过时元素需要持续检查和更新所有相关的堆,这可能会导致每次更改操作的成本增加,特别是在高更新率的应用场景中。相反,将清理操作延迟到`find`方法中可以在不频繁查询时避免不必要的性能开销,只有在真正需要时才清理过时的索引。
🦆
这种设计是否支持并发操作?如果需要支持并发,应该如何修改现有的实现?
原始设计并不支持并发操作,因为散列表和最小堆的操作未加锁,可能会在多线程环境下导致数据竞争和一致性问题。要支持并发,可以通过添加适当的锁来确保线程安全。例如,可以为散列表和每个数字对应的最小堆分别使用读写锁(读多写少场景)或互斥锁(写多场景)。此外,还可以考虑使用并发数据结构,如 Java 中的 ConcurrentHashmap,来代替标准的散列表和堆结构,以自动管理并发操作的复杂性。

相关问题