手写双向循环链表+LRU练习

手写双向循环链表+LRU练习

1.双向循环链表

双向循环链表使用一个例子解释:

例如:链表顺序如下:

1->2->3

双向那么可以表示成:

3->2->1

同时循环的概念理解就是:

1->3
3->1

以上便是双向循环链表。

那么接下来我们从最基础的结点定义->类封装及实现->测试->应用

2.加工材料

2.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;
    }
};

随后,我们定义双向循环链表。

2.2 双向循环链表定义

双向循环链表中我们采用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;
};

2.3 函数定义

从最简单的开始,如何获取最后一个结点及获取双向循环链表中的结点个数?

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);
}

2.4 测试

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

3.实践

最后,我们使用前面写的双向循环链表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),因此我们结合哈希表+双向循环链表实现。

实现:

  1. 内部成员:哈希表+双向循环链表+容量

  2. get:不存在返回-1,存在更新该数据,直接调用put,并返回数据

  3. 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

你可能感兴趣的:(手写双向循环链表+LRU练习)