题目要求保证每个函数的平均时间复杂度为 O(1),这里可以联想到用哈希表来实现添加和删除操作,具体详解见文末 Hash 表拓展
这里用 List 作为变长数组存储元素,用 HashMap 作为哈希表,哈希表存储的是元素及其在 list 中对应的下标
添加 insert:使用哈希表判断 val 是否存在,存在返回 false;不存在则添加至 list 末尾,同时更新哈希表,存入 val 及其在 list 中的下标(添加 val 之前 list 的长度)
public boolean insert(int val) {
if (map.containsKey(val)) return false;
int index = list.size();
list.add(val);
map.put(val, index);
return true;
}
删除 remove:使用哈希表判断 val 是否存在,不存在返回 false;存在则删除,由于 val 不一定在 list 的末尾,为了维护 list,这里的删除可以采用替换的形式,将 list 末尾的元素移动到 val 的下标处,同时更新哈希表中该元素的下标;最后删除 list 中的最后一个元素和哈希表中的 val
public boolean remove(int val) {
if (!map.containsKey(val)) return false;
int index = map.get(val);
int last = list.get(list.size() - 1);
list.set(index, last);
map.put(last, index);
list.remove(list.size() - 1);
map.remove(val);
return true;
}
返回随机元素 getRandom:在删除 remove 中我们维护了 list,保证在 list 长度内存在有效值,因此这里可以直接在 [0, list.size()) 中取随机下标获得随机值
random.nextInt(int x):生成一个 [0, x) 范围的任意正整数
public int getRandom() {
int rIndex = random.nextInt(list.size());
return list.get(rIndex);
}
总代码:
class RandomizedSet {
List<Integer> list;
Map<Integer, Integer> map;
Random random;
public RandomizedSet() {
list = new ArrayList<>();
map = new HashMap<>();
random = new Random();
}
public boolean insert(int val) {
if (map.containsKey(val)) return false;
int index = list.size();
list.add(val);
map.put(val, index);
return true;
}
public boolean remove(int val) {
if (!map.containsKey(val)) return false;
int index = map.get(val);
int last = list.get(list.size() - 1);
list.set(index, last);
map.put(last, index);
list.remove(list.size() - 1);
map.remove(val);
return true;
}
public int getRandom() {
int rIndex = random.nextInt(list.size());
return list.get(rIndex);
}
}
- Hash 表拓展
Hash 表是一种数据结构,表中数据以 key、value 的形式存储,实际存储的物理形式是数组
存储时将 key、value 写入 Hash 表,读取时根据 key 计算出数组下标快速找到 value
为什么用 Hash 表查找的时间复杂度就是 O(1) 呢?
我们已经知道 Hash 表的实际存储形式是数组,而数组只有通过下标访问数据的时间复杂度才是 O(1),那么可以知道 Hash 表的查找应该是通过下标进行访问,进而思考它又是怎么知道下标的呢?
Java 中可以通过 HashCode 方法返回任意对象的哈希值 HashCode(int类型)。当我们已知 key,得到 key 的 HashCode,并对 Hash 表的数组长度求余,得到的余数就是 key 在该 Hash 表中的下标,那么就可以根据下标来快速访问到存储在 Hash 表的 key、value 了。
问题:Hash 冲突,即不同 key 的 HashCode 对数组长度求余得到的是相同值
解决方法:链表法
原理:事实上 key、value 数据并不会直接存储在 Hash 表的数组中,因为数组要求存储固定的数据类型,主要目的是每个数组元素中要存放固定长度的数据。
所以数组中实际存储的是 key、value 数据元素的地址指针。
当发生 Hash 冲突时,将相同下标不同 key 的数据元素添加到对应的链表中,查找时遍历链表匹配正确的 key
严格来说,由于 Hash 冲突的存在,“Hash 表查找的时间复杂度就是 O(1)” 并不严谨,因为当所有 key 的下标都冲突,那么通过 Hash 表查找的时间复杂度会对应改变至 O(n)
参考:数据结构原理:Hash表的时间复杂度为什么是O(1)?
使用HashMap的containsKey查找键,时间复杂度为什么是O(1)?