《Deep In Go》
  • 介绍
  • Go 语言数据结构
    • 程序实体
    • 命令 & 程序入口
    • 库
    • 数组和切片
    • Map
    • 结构体
    • 函数
    • 指针
    • 循环与判断
    • 接口
    • 错误
    • Panic Recover Defer
  • Go 实现通用数据结构
    • 复杂度分析
    • 链表
      • 单链表
      • 双向链表
      • 循环链表
      • 链表的常见操作
      • 链表的应用(进阶)——LRU 缓存
      • 链表的应用(高级)——LFU 缓存
    • 排序
  • Go 并发机理
    • Channel 信道
    • Mutex 锁
    • Cond 条件变量
    • Automic 原子操作
    • WaitGroup 同步
    • Context 上下文
    • Pool 临时对象池
    • Sync Map 并发安全字典
  • Go 并发编程指南
  • Go I/O
    • Bufio
    • 字符编码
    • Strings 操作
    • Bytes 包
  • Go 系统调用
    • OS API
    • Socket & IPC
  • Go 源码分析
    • 切片
  • Go 语言开发规范
  • Go Test
  • Go FAQ
由 GitBook 提供支持
在本页
  1. Go 实现通用数据结构
  2. 链表

链表的应用(进阶)——LRU 缓存

设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

它应该支持以下操作: 获取数据 get 和 写入数据 put 。

  • 获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。

  • 写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

基本的思路是,我们设计一个双向链表,一旦一个节点被访问,我们就把它挪动到链表的表头,也就是说,越往表头,这个节点越经常被访问,如果最少被访问,那么这个节点肯定在表尾。为了提高链表的查找效率,我们可以将链表对象同时用map做存储,克服链表查找效率低下的问题,同时也发挥了链表插入、删除效率高的特性。

我们首先可以设计一个结构体,用于存储缓存节点:

type NodeList struct {
    val int
    key int
    pre *NodeList
    next *NodeList
}

func (c *NodeList) remove() {
    c.next.pre = c.pre
    c.pre.next = c.next
}

这里第一个了一个方法用于删除当前节点。

接着,我们设计 LRU 结构体:

type LRUCache struct {
  // 链表表头
    head *NodeList
  // 链表表尾
    tail *NodeList
  // 容量
    capacity int
  // 缓存map
    cache map[int]*NodeList
}

func(c *LRUCache) setHeader(node *NodeList) {
    c.head.next.pre = node
    node.next = c.head.next
    node.pre = c.head
    c.head.next = node

}

// LRU 创建函数
func Constructor(capacity int) LRUCache {
    lru := LRUCache{head: new(NodeList),
                    tail: new(NodeList),
                    capacity: capacity,
                    cache: make(map[int]*NodeList, capacity)}

    lru.tail.pre = lru.head
    lru.head.next = lru.tail
    return lru
}

这里的 setHeader 方法用于将当前节点放入链表表头。

这里的创建函数需要注意下,我们创建了一个带表头和表尾的双向链表,表头和表尾不存储元素,只是方便操作。

我们继续设计 LRU 的 Get 方法:

func (this *LRUCache) Get(key int) int {
    if n, ok := this.cache[key]; !ok {
        return -1
    } else {
        n.remove()
        this.setHeader(n)
        return n.val
    }
}

如果查找到,就先将n从链表中当前位置移除并挪动到表头。

最后,我们设计 LRU 的 Put 方法:

func (this *LRUCache) Put(key int, value int)  {
    if node, ok := this.cache[key]; ok {
        node.remove()
        delete(this.cache, key)
    } else if len(this.cache) >= this.capacity {
                toRemove := this.tail.pre
                this.tail.pre = nil
        toRemove.next = nil
        delete(this.cache, toRemove.key)
        this.tail = toRemove
    }
    newNode := &NodeList{val:value, key:key}
    this.setHeader(newNode)
    this.cache[key] = newNode
}

这里的做法比较巧妙,如果查找到 key 值在链表中已经存在,我们的处理方式是先将其从表中移除,且从map 中移除(防止出现 key 值重复)。接着如果容量已满,则删除表尾元素,将新节点创建并放入表头。

上一页链表的常见操作下一页链表的应用(高级)——LFU 缓存

最后更新于5年前