ARTS Week 31
Algorithm
本周的 LeetCode 题目为 380. O(1) 时间插入、删除和获取随机元素
实现RandomizedSet
类:
RandomizedSet()
初始化RandomizedSet
对象bool insert(int val)
当元素val
不存在时,向集合中插入该项,并返回true
;否则,返回false
。bool remove(int val)
当元素val
存在时,从集合中移除该项,并返回true
;否则,返回false
。int getRandom()
随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。
你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。
输入
["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]
输出
[null, true, false, true, 2, true, false, 2]
解释
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。
randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。
randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。
randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
为了要让平均时间复杂保持在 O(1),那么则需要使用哈希表来存储值和索引的映射关系。同时使用一个数组作为实际保存的数据的数据结构,同时有一个随机数类以便于生成随机数,下面具体介绍各个方法的实现方法:
- 初始化方法
RandomizedSet()
:分别初始化上面提到的数组、哈希表和随机数类 - 插入方法
bool insert(int val)
:先判断val
是否已经存在,若存在则返回false
,若不存在,将val
插入到数组尾部,并同时更新哈希表map.put(val, list.size()-1);
,最后返回true
- 删除方法
bool remove(int val)
:先判断val
是否已经存在,若不存在返回false
,若存在,先通过哈希表获取val
在数组中索引index,因为要求时间复杂度为O(1),那么不能使用数组的删除方法,而是将数组索引为index位置的值改为数组最后一个元素的值list.set(index, lastVal);
,并同时更新哈希表中的映射关系map.put(lastVal, index);
,之后再移除数组的最后一个元素(此操作为O(1)复杂度)和哈希表中关于val
的映射,最后返回true
- 随机返回一项
int getRandom()
:在数组索引范围内使用随机类随机生成一个数,返回该下标对应的数即可。
class RandomizedSet {
private List<Integer> list;
private Map<Integer, Integer> map;
Random rand;
public RandomizedSet() {
list = new ArrayList<>();
map = new HashMap<>();
rand = new Random();
}
public boolean insert(int val) {
if (map.containsKey(val) == true) {
return false;
} else {
list.add(val);
map.put(val, list.size()-1);
return true;
}
}
public boolean remove(int val) {
if (map.containsKey(val) == false) {
return false;
} else {
int index = map.get(val);
int lastVal = list.get(list.size()-1);
list.set(index, lastVal);
map.put(lastVal, index);
list.remove(list.size()-1);
map.remove(val);
return true;
}
}
public int getRandom() {
return list.get(rand.nextInt(list.size()));
}
}
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet obj = new RandomizedSet();
* boolean param_1 = obj.insert(val);
* boolean param_2 = obj.remove(val);
* int param_3 = obj.getRandom();
*/
Review
本周 Review 的英文文章为:SELECT *
有多慢
最广为人知的查询优化规则就是尽可能避免使用SELECT *
,即使需要用到所有列,那么也应该全部列出它们的名称。表面看起来SELECT *
会读取不必要的列,但更深层次的原因是:
- 这些列是从内存或磁盘中读取的,读取不必要的列会带来更高的资源使用率和更高的延迟
- 如果是从磁盘中读取,它可能会被缓存,那么将会浪费不必要的内存
- 列也有可能通过网络发送,将会给服务器和网络带来更多开销
在某些情况下SELECT *
将会严重影响整体应用程序性能:
- 列式数据库,很多数据库都是列式数据库,将不同的列存储到不同的数据结构中,如此使得
SUM(column)
更快,而SELECT *
却会比较慢。 - 覆盖查询。对比
SELECT id, surname FROM user WHERE surname < 'c';
和SELECT * FROM user WHERE surname < 'c';
,如果在id
上存在索引,那么前者可以通过索引来快速读取到行中对应内容进行比较,而后者却不行: - 列的数量,当一个表中列太多时,仅通过id依旧无法读取到所有列中数据
- 生成列/虚拟列。SQL语句执行时会计算生成虚拟列,有些情况下会写入磁盘进行保存,有些情况下会即使计算,当即使计算时,那么将会带来更大的开销
- 视图。视图建立在JOIN操作上,只选择需要的列可能会忽略一些不必要的表,从而使查询速度更快
- InnoDB 文本和 BLOB。在 InnoDB(MariaDB 和 MySQL 默认存储引擎)中,大的可变长度文本和二进制列存储在单独的内存页中。当长数据存储在单独的页面中,读取它至少需要一次物理读取。这会影响性能。
最后,SELECT *
不是一个好的做法,但它对查询性能和服务器资源使用的影响通常被夸大了。通常其他方面更为重要,即使用索引和避免使用内部临时表进行两步排序。
Tip
最近刚开始接触 Shell 脚本,Shell 脚本中的多行注释写法:
: '
This is a
comment
in shell script
'
Share
本周是隔离的第二周,预计接下来的几周还会继续被隔离,目前已基本适应了这样的生活。打算先休息停更一段时间,好好地整理、思考一下,望周知。