本文已参与「新人创作礼」活动, 一起开启掘金创作之路
前言
- 大家好,我是新人掘金博主:「掘金」 正在坚持每日更新LeetCode每日一题,发布的题解有些会参考其他大佬的思路(参考资料的链接会放在最下面),欢迎大家关注我 ~ ~ ~
- 同时也在进行其他专项类型题目的刷题与题解活动,相关资料也会同步到「GitHub」上面 ~
- 今天是坚持写题解的20天(haha,从21年圣诞节开始的),大家一起加油!
- 每日一题:LeetCode:382.链表随机节点
- 时间:2022-01-16
- 力扣难度:Medium
- 个人难度:Medium-
- 数据结构:链表
- 算法:水塘抽样、概率论与数理统计
2022-01-16:LeetCode:382.链表随机节点
1. 题目描述
-
题目:原题链接
- 给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点 被选中的概率一样 。
- 实现 Solution 类:
Solution(ListNode head)使用整数数组初始化对象。int getRandom()从链表中随机选择一个节点并返回该节点的值。链表中所有节点被选中的概率相等
-
输入输出规范:设计题无需考虑输入输出
2. 方法一:两次遍历链表
-
思路:遍历两次链表
- 为了实现等概率的随机访问,最简单直观的思路就是对链表进行两次遍历
- 第一次遍历链表,得到其长度,然后在生成
[0, n)之间的随机数作为索引 - 根据该随机索引,再次遍历链表,返回对应这个索引的链表元素
-
题解:直接遍历
class Solution { ListNode head; Random random; public Solution(ListNode head) { this.head = head; random = new Random(); } public int getRandom() { int len = 0; ListNode cur = head; while(cur != null) { cur = cur.next; len++; } int index = (int) random.nextInt(len); cur = head; while(index > 0) { index--; cur = cur.next; } return cur.val; } } -
复杂度分析:n 是链表的长度
- 时间复杂度:
- 空间复杂度:
3. 方法二:利用集合的随机访问
-
思路:通过集合实现随机访问
- 对于方法一,需要遍历两次链表,复杂度较高,其原因在于链表的随机访问是的
- 那么,我们可以通过集合(数组、列表)这种级别随机访问的结构来提高运算效率
- 实际上,就是先遍历一次链表,将元素都放入List中,再生成随机数,获取对应元素即可,属于空间换时间的常规操作
-
题解
class Solution { ListNode head; Random random; public Solution(ListNode head) { this.head = head; random = new Random(); } public int getRandom() { List<Integer> list = new ArrayList<>(); ListNode cur = head; while(cur != null) { list.add(cur.val); cur = cur.next; } // nextInt(1)*(b-a+1)+a int index = (int) random.nextInt(list.size()); return list.get(index); } } -
复杂度分析:n 是链表的长度
- 时间复杂度:
- 空间复杂度:
4. 方法三:水塘抽样
-
思路:概率论:随机抽样
- 实际上,本题属于数据流中获取随机元素的问题,在数学上有一种专门用于解决该类问题的算法:水塘抽样算法,也称为蓄水池抽样算法
- 该算法的主要思想是
- 维护一个初始值为0的变量 i,表示当前遍历到的元素的序号,遍历整个数据流(链表),在遍历的过程中每次对维护的序号变量进行++操作
- 然后生成一个
[0, i)的随机数,当这个随机数等于0的时候,就选择当前遍历到的元素作为结果返回 - 这样以来,我们就不需要知道整个元素的集合(无论是链表,还是抽象意义上的一个数据流)的大小,也不需要额外的空间去存放元素,非常适合在大数据场景下不知道数据具体个数时,随机返回指定个数(可以不为1)元素
- 简单证明
- 对于 n 个元素,随机返回其中一个,只需要让每个元素被获取到的几率为 即可
- 那么,运用水塘抽样算法,我们可以发现,对于第 i 个元素,其随机值为0的概率,这只是表示该元素此时被选中的概率,最终如果确定选择该元素,则还需要后续元素不被选中(否则会被替换掉)
- 此时,元素 i 被选中的概率为:
- 因此,每个元素被选中的概率都是,实现随机抽样
- 扩展:对于抽样元素不为1时,假设抽样个数为 m
- 同样地,需要每个元素被抽到的概率为
- 首先,初始时保留前 m 个元素,且各个元素等概率,同样遍历所有元素,此时随机数取值为
[0, m)时,表示本轮被选中 - 遍历第 m+1 个元素时,随机数满足0 ~ m 的概率为:
- 而原来的 m 个元素中的某一个元素 k 存在的概率为:m+1被抽到*替换的元素是其他元素 + m+1没被抽到
- 公式为:
- 递推到所有元素遍历完,该元素 k 还存在的概率:
-
题解
class Solution { ListNode head; Random random; public Solution(ListNode head) { this.head = head; random = new Random(); } public int getRandom() { int i = 0; int res = 0; ListNode cur = head; while(cur != null) { i++; if(random.nextInt(i) == 0){ res = res.val; } cur = cur.next; } return res; } } -
复杂度分析:n 是链表的长度
- 时间复杂度:
- 空间复杂度:
最后
如果本文有所帮助的话,欢迎大家可以给个三连「点赞」&「收藏」&「关注」 ~ ~ ~ 也希望大家有空的时候光临我的其他平台,上面会更新Java面经、八股文、刷题记录等等,欢迎大家光临交流,谢谢!