常数时间删除/查找数组中的任意元素

1时间插入、删除和获取随机元素

 

这些问题的一个技巧点在于,如何结合哈希表和数组,使得数组的删除操作时间复杂度也变成 O(1)?下面来一道道看。

代码如下:

class RandomizedSet {
   
    List<Integer> list=new ArrayList<>();
         Map<Integer,Integer> map= new HashMap<>();
      Random random=   new Random();
       public RandomizedSet() {
       }

       public boolean insert(int val) {
           if (map.containsKey(val)){
               return false;
           }
           list.add(val);
           map.put(val,list.size()-1);
           return true;
       }

       public boolean remove(int val) {
           if (!map.containsKey(val)){
               return false;
           }
           Integer index = map.get(val);
           Integer last = list.get(list.size() - 1);
           list.set(index,last);

           list.remove(list.size()-1);

           map.put(last,index);
           map.remove(val);
           return true;

       }

       public int getRandom() {
        return   list.get(random.nextInt(list.size()));
       }
   
}

2避开黑名单的随机数

给你输入一个正整数 N,代表左闭右开区间 [0,N),再给你输入一个数组 blacklist,其中包含一些「黑名单数字」,且 blacklist 中的数字都是区间 [0,N) 中的数字。

pick 函数会被多次调用,每次调用都要在区间 [0,N) 中「等概率随机」返回一个「不在 blacklist 中」的整数。

这应该不难理解吧,比如给你输入 N = 5, blacklist = [1,3],那么多次调用 pick 函数,会等概率随机返回 0, 2, 4 中的某一个数字。

而且题目要求,在 pick 函数中应该尽可能少调用随机数生成函数 rand()

这句话什么意思呢,比如说我们可能想出如下拍脑袋的解法:

int pick() {
    int res = rand() % N;
    while (res exists in blacklist) {
        // 重新随机一个结果
        res = rand() % N;
    }
    return res;
}

这个函数会多次调用 rand() 函数,执行效率竟然和随机数相关,不是一个漂亮的解法。

聪明的解法类似上一道题,我们可以将区间 [0,N) 看做一个数组,然后将 blacklist 中的元素移到数组的最末尾,同时用一个哈希表进行映射

根据这个思路,我们可以写出第一版代码(还存在几处错误):

class Solution {
public:
    int sz;
    unordered_map<int, int> mapping;
    
    Solution(int N, vector<int>& blacklist) {
        // 最终数组中的元素个数
        sz = N - blacklist.size();
        // 最后一个元素的索引
        int last = N - 1;
        // 将黑名单中的索引换到最后去
        for (int b : blacklist) {
            mapping[b] = last;
            last--;
        }
    }
};

如上图,相当于把黑名单中的数字都交换到了区间 [sz, N) 中,同时把 [0, sz) 中的黑名单数字映射到了正常数字。

根据这个逻辑,我们可以写出 pick 函数:

int pick() {
    // 随机选取一个索引
    int index = rand() % sz;
    // 这个索引命中了黑名单,
    // 需要被映射到其他位置
    if (mapping.count(index)) {
        return mapping[index];
    }
    // 若没命中黑名单,则直接返回
    return index;
}

这个 pick 函数已经没有问题了,但是构造函数还有两个问题。

第一个问题,如下这段代码:

int last = N - 1;
// 将黑名单中的索引换到最后去
for (int b : blacklist) {
    mapping[b] = last;
    last--;
}

我们将黑名单中的 b 映射到 last,但是我们能确定 last 不在 blacklist 中吗?

比如下图这种情况,我们的预期应该是 1 映射到 3,但是错误地映射到 4:

 

在对 mapping[b] 赋值时,要保证 last 一定不在 blacklist 中,可以如下操作:

// 构造函数
Solution(int N, vector<int>& blacklist) {
    sz = N - blacklist.size();
    // 先将所有黑名单数字加入 map
    for (int b : blacklist) { 
        // 这里赋值多少都可以
        // 目的仅仅是把键存进哈希表
        // 方便快速判断数字是否在黑名单内
        mapping[b] = 666;
    }

    int last = N - 1;
    for (int b : blacklist) {
        // 跳过所有黑名单中的数字
        while (mapping.count(last)) {
            last--;
        }
        // 将黑名单中的索引映射到合法数字
        mapping[b] = last;
        last--;
    }
}

第二个问题,如果 blacklist 中的黑名单数字本身就存在区间 [sz, N) 中,那么就没必要在 mapping 中建立映射,比如这种情况:

我们根本不用管 4,只希望把 1 映射到 3,但是按照 blacklist 的顺序,会把 4 映射到 3,显然是错误的。

我们可以稍微修改一下,写出正确的解法代码:java版本

 class Solution {
        int sz;
        Map<Integer,Integer> map=new HashMap<>();
      Random r = new Random();

        public Solution(int n, int[] blacklist) {
          sz=n-blacklist.length;
            for (int i : blacklist) {
                map.put(i,666);
            }
            int last =blacklist.length-1;
            for (int i : blacklist) {
                if (i>=sz){
                    continue;
                }
                while (map.containsKey(last)){
                    last--;
                }
                map.put(i,last);
                last--;
            }

        }

        public int pick() {
            // 随机选取一个索引
            int i = r.nextInt(sz);

            return map.getOrDefault(i,i);
        }
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值