双向循环链表使用一个例子解释:
例如:链表顺序如下:
1->2->3
双向那么可以表示成:
3->2->1
同时循环的概念理解就是:
1->3
3->1
以上便是双向循环链表。
那么接下来我们从最基础的结点定义->类封装及实现->测试->应用。
这里我们将循环链表中的结点值采用key与val存储。其余的就比较easy了,相信看完非常容易理解。
class Node {
public:
int key;
int val;
Node* prev;
Node* next;
Node() {
this->prev = this->next = nullptr;
}
Node(int key, int val) {
this->key = key;
this->val = val;
this->prev = this->next = nullptr;
}
};
随后,我们定义双向循环链表。
双向循环链表中我们采用head与tail两个结点,初始状态是head与tail互相指,那就是head->next=tail,tail->prev=head。为了方便统计双向循环链表中的size以及指定位置index插入元素,我们在内部定义了一个成员是node_size。
class DoubleCycleLinkList {
public:
DoubleCycleLinkList() {
head = new Node();
tail = new Node();
head->next = tail;
tail->prev = head;
node_size = 0;
}
private:
Node* head,*tail;
int node_size;
};
从最简单的开始,如何获取最后一个结点及获取双向循环链表中的结点个数?
Node* GetLast() {
return tail->prev;
}
int GetSize() {
return node_size;
}
如何遍历双向循环链表呢?只打印实际数据,不打印头尾结点。
跟单链表打印很像,从head的下一个结点,也就是实际结点开始遍历,直到尾部结点。
void TraverseList() {
Node* p = head->next;
while(p!=tail) {
cout << p->key << ":" << p->val << endl;
p = p->next;
}
}
同时,我们根据这个,得到该类的析构函数,在遍历过程中删除所有的实际结点,最后删除掉头与尾结点。
例如:head->1->2,为了简单描述,这里只写了单向。我们只需要让head->next = 2,2->prev=head,删除1号结点,下面的delet操作便是这个原理。
~DoubleCycleLinkList() {
if (head && tail) {
Node* p;
while(head->next!=tail) { // 除去head的所有节点 包括tail
p = head->next;
head->next->next->prev = head;
head->next = head->next->next;
delete p;
}
delete head;
delete tail;
}
}
紧接着,我们得到了通用的删除操作,随便删除任意实际结点?
我们让node的前面结点指向node的后面结点,让node的后面结点指向node的前面结点,这样便将node的前后连接起来。
void Remove(Node* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
还有一个核心的操作,那就是插入操作,我们以尾部插入开始->任意index位置插入->头插入。
尾部插入
例如:1->tail中2插入过程:
2->prev = tail->prev 得到2->1,2->next=tail,
1->next = 2是通过:tail->prev->next = node实现,最后就是tail->prev=2,对应tail->prev=node。
void AddBack(Node* node) {
node->prev = tail->prev;
node->next = tail;
tail->prev->next = node;
tail->prev = node;
node_size++;
}
任意位置插入
特殊点:若index超过了node_size,类似于数组越界,那么就push_back一样操作,直接调用上面的AddBack()函数即可。
其他点:直接循环拿到插入位置的前一个结点,例如:在head->1->3的1后面插入了2,我们想得到head->1->2->3,怎么操作呢?
用户调用的时候,Insert(node,1),便可以得到head->1->2->3。实际实现如下:
循环遍历,直到找到index位置,实际上是前一位置,这样方便插入,上面那个例子便是找到1的位置,p结点指向1,此时在1结点与3结点之间插入元素,事情就变得非常简单了。
2->prev= 1
2->next =3
3->prev = 2
1->next = 2
以上便是全过程,下面是具体实现:
void Insert(Node* node, int pos) {
if (pos != 0 && pos >= node_size) {
AddBack(node);
return;
}
Node* p = head;
int i=0;
while (i != pos) {
i++;
p = p->next;
}
node->prev = p;
node->next = p->next;
p->next->prev = node;
p->next = node;
node_size++;
}
最后,我们想在头部插入,以O(1)时间复杂度插入,只需要调用Insert(node,0)即可,为啥是O(1)呢,因为里面那个循环时不会执行的i=0,pos也等于0,因此O(1)。
void AddFirst(Node* node) {
Insert(node,0);
}
int main() {
DoubleCycleLinkList double_cycle_link_list;
cout << "插入节点" << endl;
Node* p = new Node(5,10);
double_cycle_link_list.AddBack(p);
double_cycle_link_list.AddFirst(new Node(4,11));
double_cycle_link_list.AddFirst(new Node(3,12));
double_cycle_link_list.Insert(new Node(6,11),3);
double_cycle_link_list.Insert(new Node(10,11),3);
double_cycle_link_list.TraverseList();
cout << "删除某个节点" << endl;
double_cycle_link_list.Remove(p);
double_cycle_link_list.TraverseList();
cout << "获取最后一个节点" << endl;
p = double_cycle_link_list.GetLast();
cout << p->key << ":" << p->val << endl;
cout << "删除最后一个节点" << endl;
double_cycle_link_list.Remove(p);
double_cycle_link_list.TraverseList();
}
输出:
插入节点
3:12
4:11
5:10
10:11
6:11
删除某个节点
3:12
4:11
10:11
6:11
获取最后一个节点
6:11
删除最后一个节点
3:12
4:11
10:1
最后,我们使用前面写的双向循环链表AC一下比较经典的LRU。
https://leetcode-cn.com/problems/lru-cache/
题目如下:设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作:获取数据 get
和 写入数据 put
。
通俗理解如下:每get一次,如果那个key存在,就返回数据,否则返回-1。比较重要的时key存在的时候我们需要将该结点提升优先级,例如:手机你开了三个应用,之前打开过qq,微信,qq音乐。这三个时按照时间最近->远排序,如果现在想在访问qq音乐,那便顺序改为:qq音乐->qq->微信,这便是LRU的一个例子。
写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
题目中提到,能否以O(1)时间复杂度实现呢?
答案是肯定的,我们知道删除与访问一个元素时间复杂度为O(1),想到了hash,而头部插入删除某个结点在双向循环链表中时间复杂度也是O(1),因此我们结合哈希表+双向循环链表实现。
实现:
内部成员:哈希表+双向循环链表+容量
get:不存在返回-1,存在更新该数据,直接调用put,并返回数据
put:存在,删除旧结点(链表与哈希表都要删除),并插入新结点(链表与哈希表都插入)。不存在,则需要判断此时容量是否超过,如果已经够了容量,那么需要把低优先级的,也就是尾巴结点删除掉,更新新结点。
class LRUCache {
private:
unordered_map cache_;
DoubleCycleLinkList double_cycle_link_list_;
int cap_;
public:
LRUCache(int capacity) {
cap_ = capacity;
}
int get(int key) {
if (!cache_.count(key)) return -1;
// 拿到这个数据同时,提升优先级
put(key,cache_[key]->val);
return cache_[key]->val;
}
void put(int key,int val) {
if (cache_.count(key)) {
Node* old_node = cache_[key];
cache_.erase(key);
double_cycle_link_list_.Remove(old_node);
} else {
if (cache_.size() == cap_) {
Node* last = double_cycle_link_list_.GetLast();
cache_.erase(last->key);
double_cycle_link_list_.Remove(last);
}
}
Node *new_node = new Node(key,val);
double_cycle_link_list_.AddFirst(new_node);
cache_[key] = new_node;
}
};
最后,测试:
int main() {
LRUCache* lru = new LRUCache(2);
lru->put(2,1);
lru->put(2,2);
cout << lru->get(2) << endl;
lru->put(1,1);
lru->put(4,1);
cout << lru->get(2) << endl;
retutn 0;
}
输出:
2
-1