题目简述
给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点 被选中的概率一样 。
实现 Solution 类:
Solution(ListNode head) 使用整数数组初始化对象。
int getRandom() 从链表中随机选择一个节点并返回该节点的值。链表中所有节点被选中的概率相等。
示例:
输入
[“Solution”, “getRandom”, “getRandom”, “getRandom”, “getRandom”, “getRandom”]
[[[1, 2, 3]], [], [], [], [], []]
输出
[null, 1, 3, 2, 2, 3]
解释
Solution solution = new Solution([1, 2, 3]);
solution.getRandom(); // 返回 1
solution.getRandom(); // 返回 3
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 3
// getRandom() 方法应随机返回 1、2、3中的一个,每个元素被返回的概率相等。
提示:
链表中的节点数在范围 [1, 10⁴] 内
-10⁴ <= Node.val <= 10⁴
至多调用 getRandom 方法 10⁴ 次
进阶:
如果链表非常大且长度未知,该怎么处理?
你能否在不使用额外空间的情况下解决此问题?
思路分析
最直接的方法是使用额外的与链表同长度的数组来记录每个节点的值再从数组中随机抽样。我们在这里看进阶方法(主要为了拓展其他解决方法并理解和应用该新算法)。由于题目要求不使用额外空间,我们不能使用最直接的方法了。
因此需要我们在遍历链表的过程中就进行抽样。这个问题很陌生,是的,但其实这个问题具有样本总数不确定、要求等概率抽样的性质,是蓄水池抽样算法的典型应用场景。
下面先介绍什么是蓄水池抽样算法
蓄水池抽样算法(Reservoid Sampling Algorithm)🌟
算法思想:
从数据流中逐个读取元素,当读取第 i 个元素时,以 1 i \frac{1}{i} i1 的概率选择当前元素作为样本,或者以 1 − 1 i 1-\frac{1}{i} 1−i1 的概率保留之前已选择的样本。这样,每个元素最终被选中作为样本的概率是相等的 1 n \frac{1}{n} n1(等于当时被选中的概率 [ 1 i \frac{1}{i} i1] × \times × 后续不被替换的概率 [ ( 1 − 1 i + 1 ) × ( 1 − 1 i + 2 ) × . . × ( 1 − 1 n ) (1-\frac{1}{i+1})\times(1-\frac{1}{i+2})\times..\times(1-\frac{1}{n}) (1−i+11)×(1−i+21)×..×(1−n1)],n为样本总数)。
适用场景:
数据集大小未知或过大,需要等概率地从数据集中抽取样本。
应用场景:
从大规模数据集中抽取固定大小的样本(例如本题中,抽取样本数为1),无法一次性加载到内存中时需要随机抽样,常用于大数据处理和抽样分析。
通用算法说明-等概率随机抽取k个数:
假设问题目标是:在一个规模非常大且未知具体规模的数据流中,随机等概率抽样k个数。
蓄水池算法的通用实现是:我们可以先抽取数据流的前k个元素,然后从第k+1个元素开始,当读取到第 i 个元素时(i > k),以 k i \frac{k}{i} ik的概率选择当前元素作为样本、并以 1 k \frac{1}{k} k1的概率去替换之前已选择作为样本的任意某个元素。这样就可以实现在读取无限数据流时实时等概率随机抽取k个样本(概率为 k n × 1 k = 1 n \frac{k}{n}\times\frac{1}{k}=\frac{1}{n} nk×k1=n1,n为数据规模)。
下面说明为何这种操作是满足随机等概率的要求的:
主流程是 从数据流中依次读取第 i 个元素。下面分别分析第 i 个元素被选中作为k个抽样之一的概率,以及被选中后不被后续读取的元素替换掉的概率。
被选中的概率:
当 i <= k ,第 i 个元素被选中的概率是 1;
当 i = k + 1 时,第 k + 1 个元素被选中的概率是
k
k
+
1
\frac{k}{k+1}
k+1k;
当 i = k + 2 时,第 k + 2 个元素被选中的概率是
k
k
+
2
\frac{k}{k+2}
k+2k;
以此类推,第 n 个元素被选中的概率是
k
n
\frac{k}{n}
nk。
不被替换的概率:
当 i <= k ,已被选中的元素不会被替换,因此被替换的概率为0;
当 i = k + 1 时,前 k 个元素中某元素被第 k + 1 个元素(即 当前元素)替换的概率是
1
k
\frac{1}{k}
k1;
当 i = k + 2 时,前 k 个元素中某元素被第 k + 2 个元素替换的概率也是
1
k
\frac{1}{k}
k1;
以此类推,也就是说所有的 k 个已抽样元素被后续读取的元素替换的概率都是
1
k
\frac{1}{k}
k1。相对的,不被替换的概率是
1
−
1
k
1-\frac{1}{k}
1−k1。
因此对于前 k 个数,每个数最终被选择的概率都是:
1 * 不被 k + 1 替换的概率 * 不被 k + 2 替换的概率 * … * 不被 n 替换的概率
即:1 * (1 - 被 k + 1 替换的概率) * (1 - 被 k + 2 替换的概率) * … (1 - 被 n 替换的概率)
即:
1
×
(
1
−
k
k
+
1
×
1
k
)
×
(
1
−
k
k
+
2
×
1
k
)
×
.
.
.
×
(
1
−
k
n
×
1
k
)
=
k
n
1\times(1 - \frac{k}{k+1}\times \frac{1}{k})\times (1 - \frac{k}{k+2} \times \frac{1}{k}) \times ... \times (1 - \frac{k}{n} \times \frac{1}{k}) = \frac{k}{n}
1×(1−k+1k×k1)×(1−k+2k×k1)×...×(1−nk×k1)=nk
对于第 i (i > k) 个数,最终被选择的概率是:
第 i 步被选中的概率 * 不被第 i + 1 步替换的概率 * … * 不被第 n 步被替换的概率,
即:
k
k
+
1
×
(
1
−
k
k
+
2
×
1
k
)
×
.
.
.
×
(
1
−
k
n
×
1
k
)
=
k
n
\frac{k}{k+1} \times (1 - \frac{k}{k+2} \times \frac{1}{k}) \times ... \times (1 - \frac{k}{n} \times \frac{1}{k}) = \frac{k}{n}
k+1k×(1−k+2k×k1)×...×(1−nk×k1)=nk
因此可以得到结论:在未知样本数量或大样本数据量的情况下,随机选择k个数的概率都是 k n \frac{k}{n} nk,符合等概率抽样的要求。
本题算法说明-等概率随机抽取1个数:
对于此题,在遍历非常大且长度未知的链表的过程中需要进行等概率随机抽样。
即是蓄水池算法k=1的特殊场景。其算法过程如下:
初始化第一个链表节点值为随机抽样的结果,然后逐个遍历链表节点,在遍历过程中,读取到第 i 个节点时,以
1
i
\frac{1}{i}
i1的概率替换已抽样元素。(根据蓄水池抽样通用算法,当k=1时,以
1
i
\frac{1}{i}
i1的概率选择当前元素作为样本、并以1的概率去替换之前已选择作为样本的任意某个元素,因此在该问题中直接以
1
i
\frac{1}{i}
i1的概率替换已抽样元素即可)
下面证明该算法是等概率随机抽样的:
当读取到第 i 个元素时:这个元素被选中的概率为
P
=
1
i
P=\frac{1}{i}
P=i1;
继续读取,当读取到第 i + 1 个元素时:不选择第 i + 1 个元素的概率为(1-选择第 i+1个元素的概率),即
P
=
1
−
1
i
+
1
P=1-\frac{1}{i+1}
P=1−i+11;
假设数据流在读取到第 i + 1 个元素时结束了,则 最终选择的是第 i 个元素的概率 就是 之前读取到第 i 个元素时被选中 且 读取到第 i + 1 个元素时不选择用其替换第 i 个元素 的概率,即
P
=
1
i
×
(
1
−
1
i
+
1
)
=
1
i
+
1
P=\frac{1}{i} \times(1-\frac{1}{i+1})=\frac{1}{i+1}
P=i1×(1−i+11)=i+11 。
按照这个推理,若数据流规模为n(n可能是未知的,对应未知规模的数据),最终选择的是第 i 个元素(i ∈ \in ∈ [1,n])的概率是保留之前读取的第 i 个元素 且 不选择第 i+1 个元素、不选择第 i+2 个元素、… 、不选择第 n 个元素的概率,即: P = 1 i × ( 1 − 1 i + 1 ) × ( 1 − 1 i + 2 ) × . . . × ( 1 − 1 n ) = 1 n P=\frac{1}{i} \times(1-\frac{1}{i+1}) \times(1-\frac{1}{i+2})\times ...\times(1-\frac{1}{n})=\frac{1}{n} P=i1×(1−i+11)×(1−i+21)×...×(1−n1)=n1 。
至此说明,读取到当前元素(第 i个元素)时以 1 i \frac{1}{i} i1的概率替换已抽样元素的方法,符合等概率抽样的要求。
代码示例
import random
class Solution:
def __init__(self, head: Optional[ListNode]):
self.head = head
def getRandom(self) -> int:
head = self.head
sample = head.val # 初始化采样结果为第一个节点的值
n = 1
while head:
randid = random.randint(1, n)
if randid == n: # 这里和上一行代码实现了以1/n为概率来替换已选择的样本
sample = head.val
head = head.next
n += 1
return sample
总结:蓄水池抽样算法适用于数据流大小未知或过大但需要实时等概率地从数据流中随机抽取若干个样本的问题场景。假设需要抽样k个样本,解决办法为先将读取的前k个数作为初始抽样样本,从第k+1个数开始以1/i (i>k) 的概率选择当前数并以 1/k 的概率替换已抽样的数,这样不论当前读取到数据流哪个位置,当读取完全部数据流以后,已读取的每个数作为最终被抽取的样本的概率都是相等的1/n (n是数据流规模)。