Leetcode 1206. 设计跳表
这道题的经验包:
-
跳表的知识点
-
现代C++的随机数操作
随机数知识点
以下是现代 C++(C++11 及以后版本)中随机数生成器的使用指南,包含代码示例和关键概念说明:
一、现代 C++ 随机数库的组成
现代 C++ 通过 <random>
头文件提供随机数库,包含三个核心组件:
- 随机数引擎(生成原始随机数)
- 分布器(将原始数映射到特定范围/分布)
- 适配器(修改引擎行为)
二、基本使用步骤(以骰子为例)
#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";}
}
六、常见错误
-
重复创建引擎
// 错误:每次调用都新建引擎(导致相同序列) int bad_random() {std::mt19937 gen(std::random_device{}());std::uniform_int_distribution<int> dist(1, 100);return dist(gen); }
-
误用
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可能会超过现有的levelstd::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操作是一定会加入实际节点的,那么它的层数就应该至少是1int res = 1;// 分布器传入的是引擎对象while(dis(gen) <= P && res < MAX_LEVEL) res++;return res;}
};