题目描述:
给定一个单链表,随机选择链表的一个节点,并返回相应的节点值。保证每个节点被选的概率一样。
进阶:
如果链表十分大且长度未知,如何解决这个问题?你能否使用常数级空间复杂度实现?
解题思路:
这道题最简单的解法应该是用一个vector保存所有节点的值,然后在vector中求取随机值就相当容易了,但如进阶条件所说,当链表十分大的时候时间复杂度和空间复杂度都是O(n).
这时我们就要用到蓄水池抽样法:
查了半天蓄水池采样的资料,看的一脸懵。
很多材料上来就告诉你第 i 个节点要以1/i 概率选取,然后再去证明这个值是对的。但问题是我要怎么想到这个值呢?
这时候就要用到终局思维了:当走到最后一个节点 n 时,要保证最后一个节点被选取概率是1/n,那么这时就以1/n 概率选取这个值,如果n被选中就用n替换之前被选取的值。
这时候再往前推一步,要保证 n-1最终被选取的概率是也是1/n,那么当走到n-1时,要以什么概率选取n-1?
假设这个概率是 x,那么 n-1最终被选取的概率就是 (n-1) / n * x,其含义是:被选点在[1, n-1]内的概率 * 在[1, n-1]范围内n-1被选中的概率。
那么我们就推出来 x * (1-1/n) == 1/n,x 的值就是1/n-1。有没有霍然开朗的感觉?
以上思维方式,也可以推广到选取 k 个,稍微一点改变是,在看 n-1最终是否被保留下来时,等式是
x * (1-k/n * 1/k) == k / n,k/n * 1/k的含义是 n被选取了,并且将集合中的 n-1给替换掉了(概率就是从 k 个里选1个),
再解释一下就是: n-1先被以 x 的概率选取,而后又被 n替换掉。
因此n-1最终保留在集合中的概率就是 x - x * k/n * 1/k,这个概率要等于 k/n。
有了以上推导,终于能放心的使用1/i 这个概率值选取第 i 个节点了。代码很简单。
class Solution {
public:
ListNode* head_;
/** @param head The linked list's head.
Note that the head is guaranteed to be not null, so it contains at least one node. */
Solution(ListNode* head) {
head_ = head;
}
/** Returns a random node's value. */
int getRandom() {
if (head_ == nullptr)
return -1;
int res = head_->val;
ListNode* cur = head_->next;
int count = 1;
while (cur != nullptr)
{
++count;
if (rand() % count == 0) //以 1/count 概率选取当前节点
res = cur->val;
cur = cur->next;
}
return res;
}
};