题目来源:LeetCode 382. 链表随机节点
问题抽象: 设计一个支持 单链表随机节点等概率抽取 的数据结构,给定链表的头节点(链表非空),要求实现以下核心操作:
-
功能定义:
- 构造函数
Solution(ListNode head)
:初始化数据结构; getRandom()
:等概率随机返回 链表中的一个节点(每个节点被选中的概率为1/N
,N
为链表长度)。
- 构造函数
-
操作约束:
getRandom
时间复杂度 O(N)(N
为链表长度,需遍历链表),空间复杂度 O(1)(仅常数空间);- 禁止预先存储链表节点数组(空间复杂度 O(N) 不可接受);
- 需通过 蓄水池抽样算法(Reservoir Sampling)实现:
- 遍历链表,对第
i
个节点(i≥1
)以概率1/i
替换当前选中节点; - 遍历结束时,当前选中节点即为随机结果。
- 遍历链表,对第
-
概率要求:
- 对长度为
N
的链表,每个节点被返回的概率严格等于1/N
(数学证明:- 第
k
个节点被选中概率 =(1/k) × (k/(k+1)) × ... × ((N-1)/N) = 1/N
);
- 第
- 多次调用
getRandom
的结果相互独立(无状态依赖)。
- 对长度为
-
边界处理:
- 链表长度
N=1
时,getRandom
恒返回头节点; - 长链表验证:
- 对节点值
[1,2,3]
,调用1000
次getRandom
,每个节点出现频率接近333
次;
- 对节点值
- 特殊链表:
- 含重复值链表(如
[1,1,2]
)→ 节点值可重复,但每个节点实例概率独立(如两个1
节点各占1/3
概率)。
- 含重复值链表(如
- 链表长度
输入:构造函数:链表头节点 head
(节点数 ≥1
);getRandom()
:无参数。
输出:getRandom()
返回随机节点值(整数)。
解题思路
本题需要在未知长度的单链表中实现随机节点抽取,且保证每个节点被选中的概率相等。最优解法是使用水塘抽样算法(Reservoir Sampling),特别适用于数据流或长度未知的场景。算法核心思想如下:
-
遍历过程概率更新:
- 遍历链表时,对第
i
个节点(从1开始计数) - 以
1/i
的概率选择当前节点替换结果 - 以
(i-1)/i
的概率保留之前的结果
- 遍历链表时,对第
-
概率均等证明:
- 第1个节点:选中概率 = 1/1 = 1
- 第2个节点:选中概率 = 1/2,第1节点保留概率 = 1 × (1 - 1/2) = 1/2
- 第3个节点:选中概率 = 1/3,前节点保留概率 = 1/2 × (1 - 1/3) = 1/3
- 最终每个节点概率均为 1/n(n为链表长度)
优势:空间复杂度 O(1):仅使用常数空间;无需预处理:适合动态链表;完全随机:严格保证概率均等;多次调用独立:每次调用重新抽样。
代码实现(Java版)🔥点击下载源码
class Solution {
private ListNode head; // 链表头节点
private Random rand; // 随机数生成器
public Solution(ListNode head) {
this.head = head;
this.rand = new Random();
}
public int getRandom() {
ListNode cur = head; // 当前遍历节点
int res = 0; // 存储结果
int count = 0; // 节点计数器
while (cur != null) {
count++;
// 生成[0, count)的随机整数,若为0则更新结果(概率1/count)
if (rand.nextInt(count) == 0) {
res = cur.val;
}
cur = cur.next; // 移动到下一节点
}
return res;
}
}
代码说明
-
核心逻辑:
getRandom()
每次调用遍历整个链表- 对第
i
个节点:用rand.nextInt(i) == 0
判断是否选中(概率1/i
) - 遍历结束后返回最终选中的节点值
-
变量作用:
count
:记录当前遍历的节点序号(从1开始)res
:动态存储当前选中的节点值cur
:链表遍历指针
-
算法特性:
- 时间复杂度:每次调用 O(n)
- 空间复杂度:O(1)(仅用常数空间)
- 完全随机性:n个节点每个被选中概率严格为 1/n
- 数据流友好:无需提前知道链表长度