【模拟】BM100 设计LRU缓存结构

一、题目

牛客题目链接设计LRU缓存结构_牛客题霸_牛客网

题目描述:

设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设大小为 capacity ,操作次数是 n ,并有如下功能:
1. Solution(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
2. get(key):如果关键字 key 存在于缓存中,则返回key对应的value值,否则返回 -1 。
3. set(key, value):将记录(key, value)插入该结构,如果关键字 key 已经存在,则变更其数据值 value,如果不存在,则向缓存中插入该组 key-value ,如果key-value的数量超过capacity,弹出最久未使用的key-value

提示:
1.某个key的set或get操作一旦发生,则认为这个key的记录成了最常使用的,然后都会刷新缓存。
2.当缓存的大小超过capacity时,移除最不经常使用的记录。
3.返回的value都以字符串形式表达,如果是set,则会输出"null"来表示(不需要用户返回,系统会自动输出),方便观察。
4.函数set和get必须以O(1)的方式运行
5.为了方便区分缓存里key与value,下面说明的缓存里key用""号包裹

数据范围:

1≤capacity<=10^{5}
0≤key,val≤2× 10^{9}
1≤n≤10^{5}
 

示例1

输入:["set","set","get","set","get","set","get","get","get"],[[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]],2

返回值:["null","null","1","null","-1","null","-1","3","4"]

说明:我们将缓存看成一个队列,最后一个参数为2代表capacity,所以 Solution s = new Solution(2);

s.set(1,1); //将(1,1)插入缓存,缓存是{"1"=1},set操作返回"null"

s.set(2,2); //将(2,2)插入缓存,缓存是{"2"=2,"1"=1},set操作返回"null"

output=s.get(1);// 因为get(1)操作,缓存更新,缓存是{"1"=1,"2"=2},get操作返回"1"

s.set(3,3); //将(3,3)插入缓存,缓存容量是2,故去掉某尾的key-value,缓存是{"3"=3,"1"=1},set操作返回"null"

output=s.get(2);// 因为get(2)操作,不存在对应的key,故get操作返回"-1"

s.set(4,4); //将(4,4)插入缓存,缓存容量是2,故去掉某尾的key-value,缓存是{"4"=4,"3"=3},set操作返回"null"

output=s.get(1);// 因为get(1)操作,不存在对应的key,故get操作返回"-1"

output=s.get(3);//因为get(3)操作,缓存更新,缓存是{"3"=3,"4"=4},get操作返回"3"

output=s.get(4);//因为get(4)操作,缓存更新,缓存是{"4"=4,"3"=3},get操作返回"4"

二、解题思路&代码实现

方案一:哈希表+双向链表(推荐使用)

知识点1:哈希表

哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。

知识点2:双向链表

双向链表是一种特殊的链表,它除了链表具有的每个节点指向后一个节点的指针外,还拥有一个每个节点指向前一个节点的指针,因此它可以任意向前或者向后访问,每次更改节点连接状态的时候,需要变动两个指针。

解题思路:

插入与访问值都是O(1) ,没有任何一种数据结构可以直接做到。可以用哈希表+双向链表组合实现。

具体实现:

  • step 1:构建一个双向链表的类,记录key值与val值,同时一前一后两个指针。用哈希表存储key值和链表节点,这样我们可以根据key值在哈希表中直接锁定链表节点,从而实现在链表中直接访问,能够做到O(1) 时间访问链表任意节点。
  • step 2:设置全局变量,记录双向链表的头、尾及LRU剩余的大小,并全部初始化,首尾相互连接好。
  • step 3:遍历函数的操作数组,检查第一个元素判断是属于set操作还是get操作。
  • step 4:如果是set操作,即将key值与val值加入链表,我们先检查链表中是否有这个key值,可以通过哈希表检查出,如果有直接通过哈希表访问链表的相应节点,修改val值,并将访问过的节点移到表头;如果没有则需要新建节点加到表头,同时哈希表中增加相应key值(当然,还需要检查链表长度还有无剩余,若是没有剩余则需要删去链表尾)。
  • step 5:不管是新节点,还是访问过的节点都需要加到表头,若是访问过的,需要断开原来的连接,再插入表头head的后面。
  • step 6:删除链表尾需要断掉尾节点前的连接,同时哈希表中去掉这个key值。
  • step 7:如果是get操作,检验哈希表中有无这个key值,如果没有说明链表中也没有,返回-1,否则可以根据哈希表直接锁定链表中的位置进行访问,然后重复step 5,访问过的节点加入表头。

 

复杂度分析:

  • 时间复杂度:O(n) ,其中nn为操作数组大小,map为哈希表,每次插入的复杂度都是O(1) 
  • 空间复杂度:O(k) ,链表和哈希表都是O(k)的辅助空间

代码实现:

go:

package main

type Solution struct {
	// write code here
	Table    map[int]*LinkNode
	Capacity int
	Len      int
	Head     *LinkNode
	Tail     *LinkNode
}

type LinkNode struct {
	Key  int
	Val  int
	Pre  *LinkNode
	Next *LinkNode
}

func Constructor(capacity int) Solution {
	obj := Solution{
		Table:    make(map[int]*LinkNode, capacity),
		Capacity: capacity,
		Len:      0,
		Head:     nil,
		Tail:     nil,
	}
	return obj
}

func (this *Solution) get(key int) int {
	// write code here
	if node, ok := this.Table[key]; ok {
		if node != this.Head { //将该节点移动到队首
			if node.Next != nil { //将该节点移出
				node.Next.Pre, node.Pre.Next = node.Pre, node.Next
			} else {
				this.Tail = node.Pre
				this.Tail.Next = nil
			}

			this.Head.Pre = node
			node.Next = this.Head
			this.Head = node
			this.Head.Pre = nil
		} else { //本来在队首就无需处理移位
			//do nothing
		}
		return node.Val
	}
	return -1
}

func (this *Solution) set(key int, value int) {
	// write code here
	if node, ok := this.Table[key]; ok {
		if node != this.Head { //将该节点移动到队首
			if node.Next != nil { //将该节点移出
				node.Next.Pre, node.Pre.Next = node.Pre, node.Next
			} else {
				this.Tail = node.Pre
				this.Tail.Next = nil
			}

			this.Head.Pre = node
			node.Next = this.Head
			this.Head = node
			this.Head.Pre = nil
		} else { //本来在队首就无需处理移位
			//do nothing
		}
		node.Val = value
		node.Key = key
	} else {
		if this.Len == this.Capacity { //删除尾部的节点
			removeNode := this.Tail

			if this.Tail == this.Head {
				this.Head = nil
				this.Tail = nil
			} else {
				this.Tail = this.Tail.Pre
				this.Tail.Next = nil
			}

			delete(this.Table, removeNode.Key)
			this.Len--
		}

		node := &LinkNode{
			Key:  key,
			Val:  value,
			Pre:  nil,
			Next: this.Head,
		}
		if this.Head != nil {
			this.Head.Pre = node
		}
		this.Head = node
		this.Table[key] = node
		this.Len++

		if this.Tail == nil {
			this.Tail = node
		}
	}
}

/**
 * Your Solution object will be instantiated and called as such:
 * solution := Constructor(capacity);
 * output := solution.get(key);
 * solution.set(key,value);
 */

java:

import java.util.*;

//双向链表
class Node {
    int key ;//键
    int val ;//值
    Node pre ;
    Node nxt ;
    Node(int key, int val) {
        this.key = key ;
        this.val = val ;
    }
}
public class Solution {
    HashMap<Integer, Node> map ; //存储数据+查询
    int maxSize ;//最大容量
    int size ;//当前容量
    //双向链表保存数据的访问时间情况,链头为最近使用,链尾为最久未使用
    Node head_pre
    ;//最近使用的结点 的上一个结点(辅助结点,免去考虑在链头、链尾操作的特殊情况)
    Node tail_nxt ;//最久未使用的结点的 上一个结点(辅助结点)
    public Solution(int capacity) {
        map = new HashMap<>() ;
        this.size = 0 ;
        this.maxSize = capacity ;
        head_pre = new Node(-1, -1) ;
        tail_nxt = new Node(-1, -1) ;
        head_pre.nxt = tail_nxt ;//构建双向链表
        tail_nxt.pre = head_pre ;
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1 ;
        } else {
            Node find = map.get(key) ;
            moveToHead(find)
            ;//将访问的节点移动到最近使用的位置(即,链表头)
            return find.val ;
        }
    }

    public void set(int key, int value) {
        if (map.containsKey(key)) {
            Node find = map.get(key) ;
            find.val = value ;
            moveToHead(find) ;
        } else {
            Node newNode = new Node(key, value) ;
            if (this.size == this.maxSize) {
                Node last = removeTail() ;//移除最久未使用的
                map.remove(last.key) ;
                map.put(key, newNode) ;
                insertHead(newNode) ;
            } else {
                map.put(key, newNode) ;
                insertHead(newNode) ;
                this.size ++ ;
            }
        }
    }

    //将节点移动到最近使用的位置(即,链表头)
    public void moveToHead(Node node) {
        if (node.pre == head_pre) {
            return ;
        } else {
            node.pre.nxt = node.nxt ;
            node.nxt.pre = node.pre ;
            node.nxt = null ;
            node.pre = null ;
            insertHead(node) ;
        }
    }
    //在链表头插入
    public void insertHead(Node node) {
        node.nxt = head_pre.nxt ;
        head_pre.nxt = node ;
        node.nxt.pre = node ;
        node.pre = head_pre ;
    }
    //移除最后一个结点(最久未使用的),然后返回这个结点
    public Node removeTail() {
        if (head_pre.nxt == tail_nxt) return null ;
        Node target = tail_nxt.pre ;
        target.pre.nxt = target.nxt ;
        target.nxt.pre = target.pre ;
        target.pre = null ;
        target.nxt = null ;
        return target ;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution solution = new Solution(capacity);
 * int output = solution.get(key);
 * solution.set(key,value);
 */

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值