LeetCode算法小抄--O(1)时间下删除-查找数组中任意元素

简介: LeetCode算法小抄--O(1)时间下删除-查找数组中任意元素

O(1)时间下删除-查找数组中任意元素

380. O(1) 时间插入、删除和获取随机元素

实现RandomizedSet 类:

  • RandomizedSet() 初始化 RandomizedSet 对象
  • bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false
  • bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false
  • int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。

你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1)

本题的难点在于两点:

1、插入,删除,获取随机元素这三个操作的时间复杂度必须都是 O(1)

2、getRandom 方法返回的元素必须等概率返回随机元素,也就是说,如果集合里面有 n 个元素,每个元素被返回的概率必须是 1/n

分析:

1、对于插入,删除,查找这几个操作,哪种数据结构的时间复杂度是 O(1)?

HashSet 算一个,哈希集合的底层原理就是一个大数组,我们把元素通过哈希函数映射到一个索引上;如果用拉链法解决哈希冲突,那么这个索引可能连着一个链表或者红黑树。HashSet做不到 O(1) 时间「等概率」随机获取元素。LinkedHashSet也不能满足要求

2、对于 getRandom 方法,如果想「等概率」且「在 O(1) 的时间」取出元素,一定要满足:底层用数组实现,且数组必须是紧凑的。可以直接生成随机数作为索引,从数组中取出该随机索引对应的元素,作为随机元素

3、但如果用数组存储元素的话,插入,删除的时间复杂度怎么可能是 O(1) 呢

可以做到!对数组尾部进行插入和删除操作不会涉及数据搬移,时间复杂度是 O(1)。

所以,如果我们想在 O(1) 的时间删除数组中的某一个元素 val,可以先把这个元素交换到数组的尾部,然后再 pop。交换两个元素必须通过索引进行交换对吧,那么我们需要一个哈希表 valToIndex 来记录每个元素值对应的索引。

class RandomizedSet {
    // 存储元素的值
    List<Integer> nums;
    // 记录每个元素对应在 nums 中的索引
    Map<Integer, Integer> valToIndex;
    public RandomizedSet() {
        nums = new ArrayList<>();
        valToIndex = new HashMap<>();
    }
    public boolean insert(int val) {
        // 若 val 已存在,不用再插入
        if (valToIndex.containsKey(val)) {
            return false;
        }
        // 若 val 不存在,插入到 nums 尾部,
        // 并记录 val 对应的索引值
        valToIndex.put(val, nums.size());
        nums.add(val);
        return true;        
    }
    public boolean remove(int val) {
        // 若 val 不存在,不用再删除
        if (!valToIndex.containsKey(val)) {
            return false;
        }
        // 先拿到 val 的索引
        int index = valToIndex.get(val);
        // 将最后一个元素对应的索引修改为 index
        valToIndex.put(nums.get(nums.size() - 1), index);
        // 交换 val 和最后一个元素
        Collections.swap(nums, index, nums.size() - 1);
        // 在数组中删除元素 val
        nums.remove(nums.size() - 1);
        // 删除元素 val 对应的索引
        valToIndex.remove(val);
        return true;
    }
    public int getRandom() {
        // 随机获取 nums 中的一个元素
        return nums.get((int) (Math.random() * nums.size()));        
    }
}

注意 remove(val) 函数,对 nums 进行插入、删除、交换时,都要记得修改哈希表 valToIndex,否则会出现错误。

每个操作的复杂度都是 O(1),且随机抽取的元素概率是相等的。

710. 黑名单中的随机数[hard]

给定一个整数 n 和一个 无重复 黑名单整数数组 blacklist 。设计一种算法,从 [0, n - 1] 范围内的任意整数中选取一个 未加入 黑名单 blacklist 的整数。

任何在上述范围内且不在黑名单 blacklist 中的整数都应该有 同等的可能性 被返回。

优化你的算法,使它最小化调用语言 内置 随机函数的次数。

实现 Solution 类:

  • Solution(int n, int[] blacklist) 初始化整数 n 和被加入黑名单 blacklist 的整数
  • int pick() 返回一个范围为 [0, n - 1] 且不在黑名单 blacklist 中的随机整数
  • 翻译:pick 函数会被多次调用,每次调用都要在区间 [0,N) 中「等概率随机」返回一个「不在 blacklist 中」的整数。要求,在 pick 函数中应该尽可能少调用随机数生成函数 rand()
  • 思路:可以将区间 [0,N) 看做一个数组,然后将 blacklist 中的元素移到数组的最末尾,同时用一个哈希表进行映射
/**
 * 根据黑名单生成的随机数
 */
class Solution {
    // 最终紧凑数组中的元素个数
    int sz;
    // mapping 用于记录哪些黑名单索引需要被替换成白名单索引
    Map<Integer, Integer> mapping;
    /**
     * 构造函数,时间复杂度 O(n)
     * 
     * @param N         数组中的元素个数
     * @param blacklist 黑名单中的元素索引集合
     */
    public Solution(int N, int[] blacklist) {
        sz = N - blacklist.length;
        mapping = new HashMap<>();
        // 先将所有黑名单数字加入 map
        for (int b : blacklist) {
            // 这里value赋值多少都可以
            // 目的仅仅是把键存进哈希表
            // 方便快速判断数字是否在黑名单内            
            mapping.put(b, 666);
        }
        int last = N - 1;
        for (int b : blacklist) {
            // 如果 b 已经在区间 [sz, N)
            // 可以直接忽略
            if (b >= sz) {
                continue;
            }
            while (mapping.containsKey(last)) {
                last--;
            }
            // 将黑名单中的索引映射到合法数字
            mapping.put(b, last);
            last--;
        }
    }
    /**
     * 获取随机数,时间复杂度 O(logn)
     * 
     * @return 随机数
     */
    public int pick() {
        // 随机选取一个索引
        int index = (int)(Math.random() * sz);
        // 这个索引命中了黑名单,
        // 需要被映射到其他位置
        if (mapping.containsKey(index)) {
            return mapping.get(index);
        }
        // 若没命中黑名单,则直接返回
        return index;
    }
}
/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(n, blacklist);
 * int param_1 = obj.pick();
 */

总结:

1、如果想高效地,等概率地随机获取元素,就要使用数组作为底层容器。

2、如果要保持数组元素的紧凑性,可以把待删除元素换到最后,然后 pop 掉末尾的元素,这样时间复杂度就是 O(1) 了。当然,我们需要额外的哈希表记录值到索引的映射。

3、对于数组中含有「空洞」(黑名单数字),也可以利用哈希表巧妙处理映射关系,让数组在逻辑上是紧凑的,方便随机取元素。

–end–

相关文章
|
12月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
205 3
|
7月前
|
存储 监控 算法
关于员工上网监控系统中 PHP 关联数组算法的学术解析
在当代企业管理中,员工上网监控系统是维护信息安全和提升工作效率的关键工具。PHP 中的关联数组凭借其灵活的键值对存储方式,在记录员工网络活动、管理访问规则及分析上网行为等方面发挥重要作用。通过关联数组,系统能高效记录每位员工的上网历史,设定网站访问权限,并统计不同类型的网站访问频率,帮助企业洞察员工上网模式,发现潜在问题并采取相应管理措施,从而保障信息安全和提高工作效率。
103 7
|
8月前
|
存储 人工智能 算法
C 408—《数据结构》算法题基础篇—数组(通俗易懂)
408考研——《数据结构》算法题基础篇之数组。(408算法题的入门)
321 23
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
192 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
11月前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
245 4
|
12月前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
271 4
|
12月前
|
算法
每日一道算法题(Leetcode 20)
每日一道算法题(Leetcode 20)
121 2
|
存储 算法 定位技术
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
这篇文章主要介绍了稀疏数组和队列的概念、应用实例以及如何使用数组模拟队列和环形队列的实现方法。
140 0
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
|
21天前
|
传感器 机器学习/深度学习 编解码
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
137 3
|
26天前
|
存储 编解码 算法
【多光谱滤波器阵列设计的最优球体填充】使用MSFA设计方法进行各种重建算法时,图像质量可以提高至多2 dB,并在光谱相似性方面实现了显著提升(Matlab代码实现)
【多光谱滤波器阵列设计的最优球体填充】使用MSFA设计方法进行各种重建算法时,图像质量可以提高至多2 dB,并在光谱相似性方面实现了显著提升(Matlab代码实现)

热门文章

最新文章