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 *将会严重影响整体应用程序性能:

  1. 列式数据库,很多数据库都是列式数据库,将不同的列存储到不同的数据结构中,如此使得SUM(column)更快,而SELECT *却会比较慢。
  2. 覆盖查询。对比 SELECT id, surname FROM user WHERE surname < 'c';SELECT * FROM user WHERE surname < 'c';,如果在id上存在索引,那么前者可以通过索引来快速读取到行中对应内容进行比较,而后者却不行:
  3. 列的数量,当一个表中列太多时,仅通过id依旧无法读取到所有列中数据
  4. 生成列/虚拟列。SQL语句执行时会计算生成虚拟列,有些情况下会写入磁盘进行保存,有些情况下会即使计算,当即使计算时,那么将会带来更大的开销
  5. 视图。视图建立在JOIN操作上,只选择需要的列可能会忽略一些不必要的表,从而使查询速度更快
  6. InnoDB 文本和 BLOB。在 InnoDB(MariaDB 和 MySQL 默认存储引擎)中,大的可变长度文本和二进制列存储在单独的内存页中。当长数据存储在单独的页面中,读取它至少需要一次物理读取。这会影响性能。

最后,SELECT * 不是一个好的做法,但它对查询性能和服务器资源使用的影响通常被夸大了。通常其他方面更为重要,即使用索引和避免使用内部临时表进行两步排序。

Tip

最近刚开始接触 Shell 脚本,Shell 脚本中的多行注释写法:

: '
This is a
comment
in shell script
'

Share

本周是隔离的第二周,预计接下来的几周还会继续被隔离,目前已基本适应了这样的生活。打算先休息停更一段时间,好好地整理、思考一下,望周知。

posted @ 2022-03-27 20:19  永远是萌新的阿岩  阅读(38)  评论(0编辑  收藏  举报