一、题目是啥?一句话说清
给定一个单链表,需要随机选择一个节点并返回其值,保证每个节点被选中的概率相同。
示例:
- 输入:链表 [1,2,3]
- 输出:随机返回1、2或3,每个概率为1/3
二、解题核心
使用水库抽样算法:遍历链表,对于每个节点,以1/i的概率选择当前节点(i是当前节点的索引),从而保证每个节点被选中的概率相等。
这就像抽奖时,每个人依次上台,但每个人中奖的概率会动态调整,确保最终每个人中奖的概率相同。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 水库抽样算法
- 是什么:一种随机抽样算法,用于从未知大小的数据流中随机选取一个样本,保证每个数据点被选中的概率相等。
- 为什么重要:因为链表长度未知,无法事先知道节点数量,所以需要一种在线算法,水库抽样正好满足要求。
2. 概率动态调整
- 是什么:遍历链表时,对于第i个节点,以1/i的概率选择它作为当前选中的节点。
- 为什么重要:通过这种概率调整,可以数学证明每个节点被选中的概率都是1/n,其中n是链表长度。
3. 随机数生成
- 是什么:使用随机数生成器来生成随机数,决定是否替换当前选中的节点。
- 为什么重要:随机数的质量直接影响抽样的公平性,需要确保随机数均匀分布。
四、看图理解流程(通俗理解版本)
假设链表为:1 → 2 → 3
- 初始化:当前选中的节点为null,计数器i=0。
- 处理节点1:
- i=1,以1/1的概率选择节点1,所以当前选中的节点为1。
- 处理节点2:
- i=2,生成一个随机数,如果随机数小于1/2,则选择节点2;否则保持节点1。假设随机数小于1/2,则当前选中的节点变为2。
- 处理节点3:
- i=3,生成一个随机数,如果随机数小于1/3,则选择节点3;否则保持当前节点。假设随机数大于1/3,则当前选中的节点仍为2。
- 返回结果:返回当前选中的节点值2。
每个节点被选中的概率都是1/3:
- 节点1被选中的概率:选择节点1的概率是1/1,但之后不被替换的概率是(1 - 1/2) × (1 - 1/3) = (1/2) × (2/3) = 1/3。
- 节点2被选中的概率:在i=2时被选中的概率是1/2,之后不被替换的概率是(1 - 1/3) = 2/3,所以总概率是(1/2) × (2/3) = 1/3。
- 节点3被选中的概率:在i=3时被选中的概率是1/3。
五、C++ 代码实现(附详细注释)
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
// 链表节点定义
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
private:
ListNode* head;
public:
Solution(ListNode* head) {
this->head = head;
// 初始化随机数种子,使用当前时间确保随机性
srand(time(0));
}
int getRandom() {
int count = 0;
int result = 0;
ListNode* current = head;
while (current != nullptr) {
count++;
// 生成一个随机数在[0, count-1]范围内,如果随机数等于0,则选择当前节点
// 这等价于以1/count的概率选择当前节点
if (rand() % count == 0) {
result = current->val;
}
current = current->next;
}
return result;
}
};
// 测试代码
int main() {
// 创建示例链表:1->2->3
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
Solution solution(head);
// 多次调用以验证随机性
cout << solution.getRandom() << endl;
cout << solution.getRandom() << endl;
cout << solution.getRandom() << endl;
// 释放内存
while (head != nullptr) {
ListNode* temp = head;
head = head->next;
delete temp;
}
return 0;
}
六、时间空间复杂度
- 时间复杂度:O(n),其中n是链表长度。每次调用getRandom都需要遍历整个链表一次。
- 空间复杂度:O(1),只使用了常数额外空间(几个变量),没有使用额外数据结构。
七、注意事项
- 随机数种子:在构造函数中初始化随机数种子,以确保每次运行随机性不同。但注意,如果多次创建Solution实例,可能会用相同的时间种子,但在本例中,通常只创建一个实例。
- 多次调用:每次调用getRandom都会遍历整个链表,如果链表很长且调用频繁,可能效率不高。但如果需要优化,可以考虑存储链表长度或使用其他方法,但题目没有要求。
- 概率正确性:水库抽样算法数学上保证每个节点被选中的概率相等,确保代码正确实现随机数生成逻辑。
- 空链表处理:题目假设链表至少有一个节点,所以不需要处理空链表情况。
算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解
【算法】【链表】148.排序链表--通俗讲解
【算法】【链表】160.相交链表--通俗讲解
【算法】【链表】203.移除链表元素--通俗讲解
【算法】【链表】206.反转链表--通俗讲解
【算法】234.回文链表--通俗讲解
【算法】【链表】237.删除链表中的节点--通俗讲解
【算法】【链表】328.奇偶链表--通俗讲解
【算法】【链表】给单链表加一--通俗讲解
关注公众号,获取更多底层机制/ 算法通俗讲解干货!