【算法挑战】常数时间插入、删除和获取随机元素(含解析、源码)

380.常数时间插入、删除和获取随机元素

https://leetcode-cn.com/problems/insert-delete-getrandom-o1/

题目描述

设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构。

insert(val):当元素 val 不存在时,向集合中插入该项。
remove(val):元素 val 存在时,从集合中移除该项。
getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。
示例 :

// 初始化一个空的集合。
RandomizedSet randomSet = new RandomizedSet();

// 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomSet.insert(1);

// 返回 false ,表示集合中不存在 2 。
randomSet.remove(2);

// 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomSet.insert(2);

// getRandom 应随机返回 1 或 2 。
randomSet.getRandom();

// 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomSet.remove(1);

// 2 已在集合中,所以返回 false 。
randomSet.insert(2);

// 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
randomSet.getRandom();

思路

首先得考虑的是,用数组还是用链表来存,先来复习一下数组和链表常见操作的时间复杂度吧。

数组操作 时间复杂度
随机访问 O ( 1 ) O(1) O(1)
插入数值到数组 O ( N ) O(N) O(N)
插入数值到数组最后 O ( 1 ) O(1) O(1)
从数组删除数值 O ( N ) O(N) O(N)
从数组最后删除数值 O ( 1 ) O(1) O(1)
链表操作 时间复杂度
访问 O ( N ) O(N) O(N)
插入数值到链表 O ( N ) O(N) O(N)
插入数值到链表开头 O ( 1 ) O(1) O(1)
从链表删除数值 O ( N ) O(N) O(N)
从链表开头删除数值 O ( 1 ) O(1) O(1)

很显然,链表时间复杂度为 O ( N ) O(N) O(N) 的访问操作并不符合我们的需求,所以我们还是选择数组来作为存储数据的容器。

插入

首先要实现常数时间插入元素,我们只能在数组最后插入。

在数组最后插入元素

在数组其他位置插入元素

获取随机元素

这个就很简单了,因为数组是可以通过下标随机访问的,我们只需要生成一个 0 ~ N-1 的随机数即可,N 为数组长度。

删除

删除元素的操作可以分为两种:

  1. 删除末尾元素,时间复杂度为 O(1)
  1. 删除非末尾元素,因为删除位置之后的每个元素都要向前移动一步,所以时间复杂度是 O(N)

显然,如果我们想实现题目要求的 O(1) 时间的删除,只能在数组末尾进行删除操作。具体做法就是把要删除的元素和末尾的元素换个位置,然后再从数组末尾删除。

那我们再来看看 API 是怎么用的:

  • set.insert(2) 表示往集合中插入数值 2,成功插入返回 true,如果 2 已经存在集合中返回 false
  • set.remove(2) 表示从集合中删除数值 2,成功删除返回 true,如果 2 不存在集合中返回 false

可以看到这两个方法的参数都是值,而在数组中,要在常数时间内找到一个元素,必须要知道它的下标。所以显然我们还需要一个结构来记录集合中的值和它所在的数组下标的关系,这样一系列 值->下标 的对应关系,你应该能想到用一个哈希表来记录。

数组中存放着真正的值,而哈希表中存放着每个值所对应的数组下标。

但是,还有一个问题,还记得删除操作么?我们是先把要删除的元素和最后的元素换了位置再删除,换了位置后,两个元素的下标也变了。

所以很显然的,删除某个元素后,我们的哈希表也需要更新。

代码

js 复制代码
class RandomizedSet {
    constructor() {
        // store the actual values
        this.array = [];
        // store the value-> index mapping
        this.map = {};
    }

    insert(val) {
        if (val in this.map) return false;
        this.array.push(val);
        this.map[val] = this._size() - 1;
        return true;
    }

    remove(val) {
        if (!(val in this.map)) return false;

        const index = this.map[val];
        const lastIndex = this._size() - 1;
        if (index < lastIndex) {
            this._swap(index, lastIndex);
            this.map[this.array[index]] = index;
        }
        this.array.pop();
        delete this.map[val];
        return true;
    }

    getRandom() {
        const size = this._size();
        if (size === 0) return false;
        let randomIndex = Math.floor(Math.random() * size);
        return this.array[randomIndex];
    }

    _size() {
        return this.array.length;
    }

    _swap(a, b) {
        const temp = this.array[b];
        this.array[b] = this.array[a];
        this.array[a] = temp;
    }
}

Originally posted by @suukii in https://github.com/leetcode-pp/91alg-1/issues/23#issuecomment-640231502

[参考题解](Originally posted by @azl397985856 in https://github.com/leetcode-pp/91alg-1/issues/23#issuecomment-640155651)

总结

以上就是本文所有内容了,希望能对你有所帮助,能够解决常数时间插入、删除和获取随机元素问题。

如果你喜欢本文,也请务必点赞、收藏、评论、转发,这会对我有非常大的帮助。请我喝杯冰可乐也是极好的!

已完结,欢迎持续关注。下次见~

相关推荐
网易独家音乐人Mike Zhou2 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
Swift社区6 小时前
LeetCode - #139 单词拆分
算法·leetcode·职场和发展
Kent_J_Truman7 小时前
greater<>() 、less<>()及运算符 < 重载在排序和堆中的使用
算法
IT 青年7 小时前
数据结构 (1)基本概念和术语
数据结构·算法
Dong雨7 小时前
力扣hot100-->栈/单调栈
算法·leetcode·职场和发展
SoraLuna8 小时前
「Mac玩转仓颉内测版24」基础篇4 - 浮点类型详解
开发语言·算法·macos·cangjie
liujjjiyun8 小时前
小R的随机播放顺序
数据结构·c++·算法
¥ 多多¥8 小时前
c++中mystring运算符重载
开发语言·c++·算法
trueEve9 小时前
SQL,力扣题目1369,获取最近第二次的活动
算法·leetcode·职场和发展
天若有情6739 小时前
c++框架设计展示---提高开发效率!
java·c++·算法