Skiplist 和LRU Cache
由于本人最近在看redis, 其实很多DB都使用了跳表和LRU, 跳表目的是为了Logn时间内查找,添加删除节点,相对于红黑树,AVL,哈希等,跳表最大的优势就是实现简单,代码短,你只需要花费白天时间就可以自己写出简单的跳表。
跳表的图像是这样的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DGKKFnmC-1677074997650)(C:\Users\41988\Desktop\image-20230222214432980.png)]
每个节点都有一个next数组,next[i]表示在当前节点在i的next是谁,这句话仔细看几遍,因为我第一次完全不知道在说啥。
redis里面的层数是32层,日常生活一般8层10层就够了。这个层数是非常关键的,我们知道0层,也就是最下面的层数,是所有的节点,也就是n个,但是上面一层,严格保证了期望值达到 n / 2个节点,符合正态分布,具体的证明在网上是一篇英文的论文,我就不提了,比较理论。
复杂度为什么是logN级别是因为我们每一层的期望跳步是O1级, 因为顶层的节点非常少,一次就可以跳一半,下一层又是一半,其实和二分查找是类似的,只不过我们用链表实现了二分查找。
力扣这道题目比较简单,和现实的跳表距离很大,但是逻辑框架相差无二,我们设计了一个辅助函数find,为了找到对应的前驱节点数组,方便我们查找,插入和删除。
class Skiplist {
public:
static const int level = 8;
struct Node {
int val;
vector<Node*> next;
Node(int value)
{
val = value;
next.resize(level, nullptr);
}
}*head;
Skiplist() {
head = new Node(-1);
}
~Skiplist() {
delete head;
}
void find(int target, vector<Node*> &pre) {
Node *p = head;
for (int i = level - 1; i >= 0; i--) {
while (p->next[i] && p->next[i]->val < target)
p = p->next[i];
pre[i] = p;
}
}
bool search(int target) {
vector<Node*> pre(level);
find(target, pre);
auto p = pre[0]->next[0];
return p && p->val == target;
}
void add(int num) {
vector<Node*> pre(level);
find(num, pre);
auto newnode = new Node(num);
for (int i = 0; i < level; i ++) {
newnode->next[i] = pre[i]->next[i]; //这里其实和单链表的插入相似
pre[i]->next[i] = newnode;
if (rand() % 2) break; //这一步其实严格应该调用正态分布函数, 但是为了简化
}
}
bool erase(int num) {
vector<Node*> pre(level);
find(num, pre);
auto p = pre[0]->next[0];
if (!p || p->val != num) return false;
for (int i = 0; i < level && pre[i]->next[i] == p; i ++) {
pre[i]->next[i] = p->next[i]; //单链表的删除,但是在每层都删除
}
delete p;
return true;
}
};
LRU
LRU(least recently used)是一种缓存置换算法, 其意义在于为我们设置了一种键淘汰策略,这里的键你不需要知道是啥,如果你感兴趣请读redis相关的书籍。我简要说明一下,LRU的目的是有新的数据进来后,我们由于有限的内存,需要淘汰一些缓存在内存上的数据,怎么选择数据是他的设计目的,我们只要搞懂他是怎么淘汰旧数据的即可。 LRU的做法是删除最少用的一个数据,很符合直觉的行为。
比如下面这个图片,我们放了2进来,假设此时内存满,我们应该淘汰1.
这里面有几个问题需要考虑 :: 我们怎么知道谁是最古老的数据?? 答案是我们用一个双链表,不停的头插,这样满了只要尾删除就可。 第二个问题是, 假设我们要查找某数据,如何做到快并且可以移动到头部? LRU的查找较为特殊, 每次查找会把找到的数据放置于链表头,这样叫做更新时间戳。 答案是我们需要一个哈希, 在O1时间找到对应的数据。为了方便,我们设计了两个辅助函数,删除和插入。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vQsyniYv-1677074997651)(C:\Users\41988\Desktop\image-20230222215656145.png)]
class LRUCache {
public:
struct Node {
int key, val;
Node *left, *right;
Node(int _key, int _val): key(_key), val(_val), left(NULL), right(NULL) {}
}*L, *R;
unordered_map<int, Node*> hash;
int n;
void remove(Node* p) {
p->right->left = p->left;
p->left->right = p->right;
}
void insert(Node* p) {
p->right = L->right;
p->left = L;
L->right->left = p;
L->right = p;
}
LRUCache(int capacity) {
n = capacity;
L = new Node(-1, -1), R = new Node(-1, -1);
L->right = R, R->left = L;
}
int get(int key) {
if (hash.count(key) == 0) return -1;
auto p = hash[key];
remove(p);
insert(p);
return p->val;
}
void put(int key, int value) {
if (hash.count(key)) {
auto p = hash[key];
p->val = value;
remove(p);
insert(p);
} else {
if (hash.size() == n) {
auto p = R->left;
remove(p);
hash.erase(p->key);
delete p;
}
auto p = new Node(key, value);
hash[key] = p;
insert(p);
}
}
};
rase(p->key);
delete p;
}
auto p = new Node(key, value);
hash[key] = p;
insert§;
}
}
};