【算法】【链表】382.链表随机节点--通俗讲解

95 阅读6分钟

一、题目是啥?一句话说清

给定一个单链表,需要随机选择一个节点并返回其值,保证每个节点被选中的概率相同。

示例:

  • 输入:链表 [1,2,3]
  • 输出:随机返回1、2或3,每个概率为1/3

二、解题核心

使用水库抽样算法:遍历链表,对于每个节点,以1/i的概率选择当前节点(i是当前节点的索引),从而保证每个节点被选中的概率相等。

这就像抽奖时,每个人依次上台,但每个人中奖的概率会动态调整,确保最终每个人中奖的概率相同。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 水库抽样算法

  • 是什么:一种随机抽样算法,用于从未知大小的数据流中随机选取一个样本,保证每个数据点被选中的概率相等。
  • 为什么重要:因为链表长度未知,无法事先知道节点数量,所以需要一种在线算法,水库抽样正好满足要求。

2. 概率动态调整

  • 是什么:遍历链表时,对于第i个节点,以1/i的概率选择它作为当前选中的节点。
  • 为什么重要:通过这种概率调整,可以数学证明每个节点被选中的概率都是1/n,其中n是链表长度。

3. 随机数生成

  • 是什么:使用随机数生成器来生成随机数,决定是否替换当前选中的节点。
  • 为什么重要:随机数的质量直接影响抽样的公平性,需要确保随机数均匀分布。

四、看图理解流程(通俗理解版本)

假设链表为:1 → 2 → 3

  1. 初始化:当前选中的节点为null,计数器i=0。
  2. 处理节点1
    • i=1,以1/1的概率选择节点1,所以当前选中的节点为1。
  3. 处理节点2
    • i=2,生成一个随机数,如果随机数小于1/2,则选择节点2;否则保持节点1。假设随机数小于1/2,则当前选中的节点变为2。
  4. 处理节点3
    • i=3,生成一个随机数,如果随机数小于1/3,则选择节点3;否则保持当前节点。假设随机数大于1/3,则当前选中的节点仍为2。
  5. 返回结果:返回当前选中的节点值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.奇偶链表--通俗讲解
【算法】【链表】给单链表加一--通俗讲解


关注公众号,获取更多底层机制/ 算法通俗讲解干货!