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)层期望的元素个数是1p

  1. 期望层数

期望层数是logp(1n)

本文作者:Gold_stein

本文链接:https://www.cnblogs.com/smartljy/p/18735263

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Gold_stein  阅读(9)  评论(0编辑  收藏  举报
历史上的今天:
2024-02-25 P8669 [蓝桥杯 2018 省 B] 乘积最大
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
🔑
  1. 1 逃离地面 RAD & 三浦透子
逃离地面 - RAD & 三浦透子
00:00 / 00:00
An audio error has occurred.

作词 : 野田洋次郎

作曲 : 野田洋次郎

空飛ぶ羽根と引き換えに 繋ぎ合う手を選んだ僕ら

それでも空に魅せられて 夢を重ねるのは罪か

夏は秋の背中を見て その顔を思い浮かべる

憧れなのか、恋なのか 叶わぬと知っていながら

重力が眠りにつく 1000年に一度の今日

太陽の死角に立ち 僕らこの星を出よう

彼が眼を覚ました時 連れ戻せない場所へ

「せーの」で大地を蹴って ここではない星へ

行こう

もう少しで運命の向こう もう少しで文明の向こう

もう少しで運命の向こう もう少しで

夢に僕らで帆を張って 来たるべき日のために夜を超え

いざ期待だけ満タンで あとはどうにかなるさと 肩を組んだ

怖くないわけない でも止まんない

ピンチの先回りしたって 僕らじゃしょうがない

僕らの恋が言う 声が言う

「行け」と言う