LeetCode 每日一题381:O(1) 时间插入、删除和获取随机元素 - 允许重复

381.O(1) 时间插入、删除和获取随机元素 - 允许重复

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

注意: 允许出现重复元素。

  1. insert(val):向集合中插入元素 val。
  2. remove(val):当 val 存在时,从集合中移除一个 val。
  3. getRandom:从现有集合中随机获取一个元素。每个元素被返回的概率应该与其在集合中的数量呈线性相关。

示例:

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

// 向集合中插入 1 。返回 true 表示集合不包含 1 。
collection.insert(1);

// 向集合中插入另一个 1 。返回 false 表示集合包含 1 。集合现在包含 [1,1] 。
collection.insert(1);

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

// getRandom 应当有 2/3 的概率返回 1 ,1/3 的概率返回 2 。
collection.getRandom();

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

// getRandom 应有相同概率返回 1 和 2 。
collection.getRandom();

解题思路:

首先,应该思考用来存数据的「数据结构」。根据题目要求:O(1)时间复杂度的插入、删除、随机获取以及允许重复,首先想到数组。

然后,分析数组是否满足题目要求:

  • 插入:数组是实打实的O(1)
  • 随机获取:生成 数组长度 以内随机数做为下标,返回下标对应的元素。满足O(1)且人人平等
  • 删除:数组不支持元素删除,仅支持角标删除,且删除元素会进行数组拷贝
  • 允许重复:自带属性

针对删除的弊端思考解决方案:

  • 角标删除问题:如果我们能通过「元素」能找到「元素对应的角标」,就能达成目的。使用Hash表来存,key 为当前元素,value 为元素对应的下标,因为允许重复,所以 value 应该是一个集合。Hash表的 put 和 get 时间复杂度均为O(1)。
  • 数组拷贝问题:数组在删除元素时,会进行「数组拷贝」,把删除索引之后的数据往前移一位,导致时间复杂度变为O(N)。如果我们每次删除都是删除数组最后一位,就不会发生「数组拷贝」了。所以在删除时,我们取到数组最后一位的值,并set到需要删除的索引位置,再删除数组最后一位。就能保证时间复杂度为O(1)。

这样数据结构就出来了:一个用于存放元素的数组,一个用于存放元素索引的Hash表

方法一:

数据结构:

// 数字集合
private List<Integer> nums;
// 数字索引的集合
private Map<Integer, Set<Integer>> idx;

构造函数:

public RandomizedCollection() {
    // 初始化
    nums = new ArrayList<>();
    idx = new HashMap<>();
}

insert方法:

// 需要做2件事:1.添加元素 2.更新该值对应的索引集合
public boolean insert(int val) {
    // 添加元素
    nums.add(val);
    // 更新该值对应的索引集合
    Set<Integer> set = idx.getOrDefault(val, new HashSet<>());
    set.add(nums.size() - 1);
    idx.put(val, set);
    return set.size() == 1;
}

remove方法:需要注意的地方参考注释

public boolean remove(int val) {
    if (!idx.containsKey(val)) {
        return false;
    }

    // 取出该数字的索引,从索引中取出第一个(题目要求仅删除一个)并移除
    Set<Integer> set = idx.get(val);
    int delIndex = set.iterator().next();
    set.remove(delIndex);
    // 若只有一个索引,删除该集合
    if (set.size() == 0) {
        idx.remove(val);
    }

    // 删除的索引不在末尾
    if (delIndex < nums.size() - 1) {
        // 获取末尾的值,放到当前被删除索引位置
        int lastNum = nums.get(nums.size() - 1);
        nums.set(delIndex, lastNum);

        // 更新末尾的值的索引
        Set<Integer> lastNumIdx = idx.get(lastNum);
        lastNumIdx.remove(nums.size() - 1);
        lastNumIdx.add(delIndex);
    }

    // 删除末尾元素
    nums.remove(nums.size() - 1);
    return true;
}

getRandom方法:

public int getRandom() {
	// 题有个小问题,并未提示没有元素时返回什么,经测试需要返回-1
    if (nums.size() == 0) {
        return -1;
    }
 
    int index = (int) (Math.random() * nums.size());
    return nums.get(index);
}

执行结果:

在这里插入图片描述

附:ArrayList. remove 方法源码

当 size - index - 1 > 0 ,即 index < size - 1 时,才会执行 System.arraycopy。

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值