分析:

题解:哈希表 + 双向链表

LRU 缓存机制可以通过哈希表辅助双向链表实现,用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  • 双向链表按照被使用的顺序存储这些 key-value ,靠近链表头部的键值对是最近使用的(最近使用包括:查询、添加),而靠近尾部的键值对是很久没有使用的。这些键值对加上后继节点和前驱节点就组成了缓存数据 cache ,以节点的形式存在 双向链表中;
  • 除了双向链表,哈希表存储的就是普通的哈希映射,key 对应的 value 是双向链表的节点,即 cache 缓存数据节点。

通过 哈希表 与 双向链表 的存储结构,首先通过哈希表进行查询对应的节点信息,找出缓存数据在双向链表中的节点,随后将该节点移动到双向链表的头部(依据 LRU 规则),完成 get 或者 put 的操作,查询哈希表时间复杂度为 O(1),移动到双向链表头部时间复杂度也为 O(1)

详细策略如下:

对于 get 操作,首先判断 key 是否存在:

  • 如果 key 不存在,直接返回 -1;
  • 如果 key 存在,通过哈希表找到对应的缓存数据节点 cache,并将其移动到双向链表的头部(涉及删除节点+添加头节点两个操作),最后返回该节点的值。

对于 put 操作,首先判断 key 是否存在:

  • 如果 key 不存在,使用传入的 key 和 value 创建新的缓存节点以 key-缓存节点 的键值对形式存入哈希表中,然后将这个节点缓存数据移动到双向链表的头部,节点数 + 1。然后判断节点数 size 是否超过初始化的容量 capacity ,超过了就删除双向链表的尾部节点,并删除哈希表对应的映射关系,节点数 – 1;否则什么都不做。
  • 如果 key 存在,则更新节点缓存的 value 值,并且将其移动到双向链表的头部(涉及删除节点+添加头节点两个操作)。

经上述分析,查询哈希表和双向链表添加头节点、删除尾节点时间复杂度都是 O(1)。特别注意:将一个节点移动到双向链表的头部涉及两个操作,删除节点 + 添加头节点。

在双向链表中,使用 虚拟头节点 dummyHead虚拟尾节点 dummyTail 来辅助构建链表。在调用构造函数时,哈希表中初始化的 虚拟头节点 和 虚拟尾节点 如下图所示:

代码:

class LRUCache {

    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int key, int value){
            this.key = key;
            this.value = value;
        }

    }

    //使用 哈希表 + 链表 的形式 作为缓存的数据结构
    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private int capacity; //用于记录缓存初始化时的大小
    private int size;//用于记录增加或减少时缓存中数据的个数
    private DLinkedNode dummyHead, dummyTail;//虚拟头节点,虚拟尾节点

    public LRUCache(int capacity) {
        this.capacity = capacity;
        dummyHead = new DLinkedNode();
        dummyTail = new DLinkedNode();
        dummyHead.next = dummyTail;
        dummyTail.prev = dummyHead;
    }
    
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 没有缓存,直接返回 -1
            return -1;
        } else{
            // 有缓存,移动到链表头部
            removeNode(node);
            addNode(node);
            return node.value;
        }
    }
    
    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if(node == null) {
            // 没有缓存,要存储,且移动到链表头部
            DLinkedNode newNode = new DLinkedNode(key, value);
            cache.put(key, newNode);
            addNode(newNode);
            size++;
            // 判断容量
            if (size > capacity) {
                // 删除最后一个节点
                DLinkedNode tempNode = dummyTail.prev;
                cache.remove(tempNode.key);
                removeNode(tempNode);
                size--;
            }
        } else {
            // 有缓存,更新 value 值,且移动到链表头部
            node.value = value;
            removeNode(node);
            addNode(node);
        }
        
    }

    // 移除节点方法
    public void removeNode(DLinkedNode node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    // 添加到链表首位
    public void addNode(DLinkedNode node){
        node.next = dummyHead.next;
        node.prev = dummyHead;
        dummyHead.next.prev = node;
        dummyHead.next = node;
    }

}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

抽离出两个公用方法,移除节点方法 removeNode() 、添加到链表首位方法 addNode()。

GoLang 解法

代码:

type LRUCache struct {
    // LRU缓存结构体包括容量、节点、虚拟头结点和虚拟尾结点
    size int // 当前的双向链表节点数
    capacity int // 容量
    cache map[int]*DLinkedNode
    dummyHead, dummyTail *DLinkedNode
}

// DLinkedNode 双向链表结构体
type DLinkedNode struct {
    // 包括key和value,以及前驱节点和后继结点
    key, value int
    pre, next *DLinkedNode
}

func initDLinkedNode(key, value int) *DLinkedNode {
    return &DLinkedNode{
        key: key,
        value: value,
    }
}

// 初始化
func Constructor(capacity int) LRUCache {
    l := LRUCache{
        size : 0,
        capacity: capacity,
        cache: make(map[int]*DLinkedNode, 0),
        dummyHead: initDLinkedNode(0, 0),
        dummyTail: initDLinkedNode(0, 0),
    }
    l.dummyHead.next = l.dummyTail
    l.dummyTail.pre = l.dummyHead
    return l
}


func (this *LRUCache) Get(key int) int {
    if _, ok := this.cache[key]; !ok {
        return -1
    }
    // 把查询到的节点放入双向链表首位
    node := this.cache[key]
    this.moveToHead(node)
    return node.value
}


func (this *LRUCache) Put(key int, value int)  {
    // 缓存中没有这个值
    if _, ok := this.cache[key]; !ok {
        node := initDLinkedNode(key, value)
        this.cache[key] = node
        // 添加节点到首位
        this.addToHead(node)
        // 判断size是否超过 capacity
        this.size++
        if this.size > this.capacity {
            // 移除最后一个节点(即虚拟尾结点的前一个节点)
            removed := this.removeTail()
            // 从Map中也一并移除,go的map删除还真是方便
            delete(this.cache, removed.key)
            this.size--
        }
        // 缓存中有这个值
    } else {
        // 移动原有节点到首位
        node := this.cache[key]
        node.value = value
        this.moveToHead(node)
    }
}

// moveToHead 移动原有节点到首位
func (this *LRUCache) moveToHead(node *DLinkedNode) {
    // 移除节点
    this.removeNode(node)
    // 添加节点
    this.addToHead(node)
}

// removeNode 移除节点
func (this *LRUCache) removeNode(node *DLinkedNode) {
    node.pre.next = node.next
    node.next.pre = node.pre
}

// addToHead 将节点添加到链表首位
func (this *LRUCache) addToHead(node *DLinkedNode) {
    node.pre = this.dummyHead
    node.next = this.dummyHead.next
    this.dummyHead.next.pre = node
    this.dummyHead.next = node
}

// removeTail 移除最后一个节点
func (this *LRUCache) removeTail() *DLinkedNode {
    node := this.dummyTail.pre
    this.removeNode(node)
    return node
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * obj := Constructor(capacity);
 * param_1 := obj.Get(key);
 * obj.Put(key,value);
 */

注:在 Go 语言中,删除 map 中的元素可以使用内置的 delete() 函数。这个函数接受两个参数:一个是 map 本身,另一个是要删除的键。如果指定的键在 map 中存在,它将被删除;如果不存在,则不执行任何操作。

scene := make(map[string]int)
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960

delete(scene, "brazil") // 删除键为"brazil"的元素

for k, v := range scene {
fmt.Println(k, v) // 输出剩余的元素
}

Categories:

Tags:

No responses yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注