Leetcode 1206. 设计跳表

Leetcode 1206. 设计跳表

这道题的经验包:

  1. 跳表的知识点

  2. 现代C++的随机数操作

随机数知识点

以下是现代 C++(C++11 及以后版本)中随机数生成器的使用指南,包含代码示例和关键概念说明:


一、现代 C++ 随机数库的组成

现代 C++ 通过 <random> 头文件提供随机数库,包含三个核心组件:

  1. 随机数引擎(生成原始随机数)
  2. 分布器(将原始数映射到特定范围/分布)
  3. 适配器(修改引擎行为)

二、基本使用步骤(以骰子为例)

#include <iostream>
#include <random>

int main() {
    // 1. 创建随机数引擎(使用硬件熵初始化)
    std::random_device rd;  
    std::mt19937 gen(rd());  // 使用 Mersenne Twister 算法引擎

    // 2. 定义分布器(1~6 的均匀整数分布)
    std::uniform_int_distribution<int> dist(1, 6);

    // 3. 生成随机数
    for (int i = 0; i < 5; ++i) {
        std::cout << dist(gen) << " ";  // 输出类似:3 5 2 6 4
    }
}

三、核心组件详解

1. 随机数引擎(Engines)

引擎类型 特性 典型用途
std::mt19937 梅森旋转算法,高质量随机数 通用场景
std::mt19937_64 64 位版本 需要更大范围
std::minstd_rand 线性同余算法,速度更快 性能敏感场景
std::random_device 可能使用硬件熵源(非确定性) 种子生成

2. 分布器(Distributions)

分布类型 描述 示例代码
uniform_int_distribution 均匀整数分布 dist(1, 100) → 1~100 整数
uniform_real_distribution 均匀浮点数分布 dist(0.0, 1.0) → [0.0, 1.0)
normal_distribution 正态(高斯)分布 dist(均值, 标准差)
bernoulli_distribution 布尔分布(真/假) dist(0.7) → 70% 概率为 true
discrete_distribution 自定义离散概率分布 dist{1,2,3} → 概率权重 1:2:3

四、最佳实践

1. 种子初始化

// 推荐方式:混合种子(更安全)
std::random_device rd;
std::seed_seq seeds{rd(), static_cast<uint64_t>(std::time(nullptr))};
std::mt19937_64 gen(seeds);

2. 线程安全

// 每个线程创建独立引擎实例(避免竞争)
void thread_task() {
    thread_local std::mt19937 gen(std::random_device{}());
    std::uniform_int_distribution<int> dist(1, 6);
    // 使用 gen 和 dist...
}

3. 性能优化

// 重用引擎和分布器(避免重复构造)
std::mt19937& get_engine() {
    static std::mt19937 gen(std::random_device{}());
    return gen;
}

int get_random() {
    static std::uniform_int_distribution<int> dist(1, 100);
    return dist(get_engine());
}

五、完整代码示例(正态分布)

#include <iostream>
#include <random>
#include <vector>
#include <algorithm>
#include <iomanip>

int main() {
    // 1. 初始化引擎
    std::random_device rd;
    std::mt19937 gen(rd());

    // 2. 定义正态分布(均值=0,标准差=1)
    std::normal_distribution<double> dist(0.0, 1.0);

    // 3. 生成 1000 个样本
    std::vector<double> data;
    std::generate_n(std::back_inserter(data), 1000, [&] { return dist(gen); });

    // 4. 统计分布情况
    auto [min, max] = std::minmax_element(data.begin(), data.end());
    std::cout << "范围: [" << *min << ", " << *max << "]\n";

    // 5. 输出直方图
    std::map<int, int> hist;
    for (double x : data) {
        ++hist[std::round(x)];
    }
    for (auto [value, count] : hist) {
        std::cout << std::setw(2) << value << " " << std::string(count/5, '*') << "\n";
    }
}

六、常见错误

  1. 重复创建引擎

    // 错误:每次调用都新建引擎(导致相同序列)
    int bad_random() {
        std::mt19937 gen(std::random_device{}());
        std::uniform_int_distribution<int> dist(1, 100);
        return dist(gen);
    }
    
  2. 误用 std::random_device

    // 某些平台可能回退到伪随机(需检查熵)
    if (std::random_device{}.entropy() == 0) {
        std::cerr << "Warning: 当前平台未提供真随机源\n";
    }
    

总结

现代 C++ 的 <random> 库提供了:

  • ✅ 更高质量的随机数生成
  • ✅ 灵活的分布控制
  • ✅ 更好的线程安全性
  • ✅ 可预测的随机序列(通过固定种子)

建议优先使用此库替代传统的 rand()srand()

正解代码(附详细注释)

constexpr double P = 0.5;
constexpr int MAX_LEVEL = 16;
struct SkipNode {
    int val;
    vector<SkipNode*> list;
    SkipNode(int _val, int _max_level = MAX_LEVEL)
        : val(_val), list(_max_level, nullptr) {}
};

class Skiplist {
   public:
    Skiplist() : head(new SkipNode(-1)), level(0) {}

    bool search(int target) {
        SkipNode *cur = this->head;
        // 从上往下找
        for(int i = level - 1; i >= 0; i--) {
            while(cur->list[i] && cur->list[i]->val < target)
                cur = cur->list[i];
        }
        cur = cur->list[0];
        // 要小心访问到空地址
        return cur && cur->val == target;
    }

    void add(int num) {
        // 这里要开MAX_LEVEL个,因为后面的新level可能会超过现有的level
        std::vector<SkipNode*> update(MAX_LEVEL, head);
        SkipNode *cur = this->head;
        for(int i = level - 1; i >= 0; i--) {
            while(cur->list[i] && cur->list[i]->val < num)
                cur = cur->list[i];
            // 用update记录每层最后一个被访问的节点
            update[i] = cur;
        }
        SkipNode *_insert = new SkipNode(num);
        int lv = gen_lv();
        // 更新层数
        level = std::max(level, lv);
        // 新节点插入在update[i]的后面
        for(int i = 0; i < lv; i++) {
            _insert->list[i] = update[i]->list[i];
            update[i]->list[i] = _insert;
        }
    }

    bool erase(int num) {
        // 这里只要开level个,因为在这个函数level只会变小不会变大
        std::vector<SkipNode*> update(level, head);
        // 跟erase一模一样
        SkipNode *cur = head;
        for(int i = level - 1; i >= 0; i--) {
            while(cur->list[i] && cur->list[i]->val < num)
                cur = cur->list[i];
            // 用update记录每层最后一个被访问的节点
            update[i] = cur;
        }
        // 我们没必要记录cur的prev,因为update已经发挥了这个作用
        cur = cur->list[0];
        if (!cur || cur->val != num)
            return false;
        // 要先完成连接的修改再delete掉cur,不然会空悬指针
        for(int i = 0; i < level; i++) {
            // 如果cur在这一层没有出现,那么再往上它都不会出现了
            if (update[i]->list[i] != cur)
                break;
            update[i]->list[i] = cur->list[i];
        }
        // 可以安全地删除cur了
        delete cur;
        // 更新层数
        while(level > 1 && head->list[level - 1] == nullptr)
            level--;
        return true;
    }
    
   private:
    SkipNode* head;
    int level;
    // 注意,生成引擎不是用device对象来初始化的,使用device对象的调用结果来初始化的
    std::mt19937 gen{std::random_device{}()};
    std::uniform_real_distribution<double> dis{0.0, 1.0};

    int gen_lv() {
        // level初始化为0是因为一开始整个跳表是空的,所以我们的层数相当于0,但是add操作是一定会加入实际节点的,那么它的层数就应该至少是1
        int res = 1;
        // 分布器传入的是引擎对象
        // 一开始写错了,在循环外面就定义了随机数,导致随机数一直都是同一个值,执行速度特别慢
        while(dis(gen) <= P && res < MAX_LEVEL) 
            res++;
        return res;
    }
};

关于跳表的几个常见结论

  1. 复杂度

期望的空间复杂度是O(n),期望的时间复杂度是O(logN)

  1. 最高层期望元素个数

L(n)层期望的元素个数是$ \frac{1}{p} $

  1. 期望层数

期望层数是$ \log_{p}(\frac{1}{n}) $

posted @ 2025-02-25 01:20  Gold_stein  阅读(50)  评论(0)    收藏  举报