Leetcode 1206. 设计跳表
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可能会超过现有的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;
}
};
关于跳表的几个常见结论
- 复杂度
期望的空间复杂度是O(n)
,期望的时间复杂度是O(logN)
- 最高层期望元素个数
L(n)层期望的元素个数是
- 期望层数
期望层数是
本文作者:Gold_stein
本文链接:https://www.cnblogs.com/smartljy/p/18735263
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2024-02-25 P8669 [蓝桥杯 2018 省 B] 乘积最大