算法分享系列No.2---- LRU缓存结构

目录

一、该题基础信息列表

二、【LRU缓存结构】编程练习目标

三、题目分析及解析思路

3-1、设计LRU缓存结构

3-2、解题思路 与 具体实现(JAVA篇)

(1) 结构体: 双向链表

(2)全局变量的设置和初始值

(3) LRU插入操作具体实现

(4) LRU查询操作具体实现

(5) 上面步骤中使用到的方法具体实现【难点】

(5-1) 移动到头节点moveToHead

(5-2) 节点插入到头节点后 insertToHead

(5-3) 删除尾节点removeLastTail()

四、代码完整实现(JAVA篇)


一、该题基础信息列表

  1. 题目难易分类【easy】【middle】【hard】
  2. 题目频率(牛客有考察次数)【高频】(10<=考察次数) 【中频】(2 <=考察次数 < 10) 【普通】
设计LRU缓存结构 LeetCode 牛客【题库--算法篇--面试高频榜单】
题序号

146. LRU 缓存

NC93 设计LRU缓存结构
难度 & 频次 【middle】 【hard】【高频】


二、【LRU缓存结构】编程练习目标

这题据身边大厂朋友反馈,如字节,小米等公司,笔试频率非常高。非常经典,且有一定代码量,即能考察程序员的基础也能有一定的拔高,请务必熟练掌握

本题考点:【链表】【哈希】

三、题目分析及解析思路

3-1、设计LRU缓存结构

算法分享系列No.2---- LRU缓存结构_第1张图片

3-2、解题思路 与 具体实现(JAVA篇)

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

知识点1:哈希表

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

知识点2:双向链表

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

思路:

题目要求,插入和查询必须要求 O(1),是本题的难点!因为任何一个数据结构都不能直接做到O(1),  因此只能组合使用。

插入O(1)的数据结构有很多,但在目标点插入,且超出长度要O(1)之内删除,可用链表。key值节点加入链表头,同时删掉链表尾,选择双向链表,便于删除与移动。

(1) 结构体: 双向链表

//双向链表结构体
class Node{
  int key;
  int val;
  Node pre;
  Node next;
  //进行初始化
  Node(int key, int val){
     this.key = key;
     this.val = val;
     this.pre = null;
     this.next = null;
  }
}

(2)全局变量的设置和初始值

记录双向链表的头、尾及LRU剩余的大小,并全部初始化,首尾相互连接好。

//全局变量

//哈希表用于O(1)查询
Map map = new HashMap<>();
//双向链表,head,tail指针
Node head = new Node(-1,-1);
Node tail = new Node(-1,-1);
//缓存剩余空间数
int k = 0;


//对全局变量进行初始赋值
public LRUCache(int capacity) {
    head.next = tail;
    tail.pre = head;
    k = capacity;
}

(3) LRU插入操作具体实现

存在两种可能性:

(3-1)已经存在(通过哈希表的map判断),此时获取已经存在的节点,重新赋val值, 并把该节点移动到链表头head

(3-2)之前不存在,需要插入,先判断此时缓存剩余空间知否足够,不充足则需要删除链表尾节点tail,插入到链表头head

public void put(int key, int value){
     //存在该节点
     if(map.containsKey(key)){
         map.get(key).val = value;
         //该节点移动到链表头
         moveToHead(map.get(key));
     }else{
         //不存在该节点,则创建,并写入哈希表中
         Node node = new Node(key,value);
         map.put(key,node);

         //如果没有缓存空间,则删除最后一个元素
         if(k <= 0){
            removeLastTail();
         }
         
         //减少缓存剩余空间,并向头插入节点
         k--;
         insertToHead(node);                   
     }
}

(4) LRU查询操作具体实现

如果当前哈希表map中存在key,  则返回对应的value值,并把该节点移动到链表头head.

如果哈希表map不存在key, 则直接返回-1

public int get(int key) {
    int res = -1;
    //哈希表存在,则返回述职,并移动到链表头head
    if(map.containsKey(key)){
       Node node = map.get(key);
       res = node.val;
       moveToHead(node);
    }
    return res;
}

(5) 上面步骤中使用到的方法具体实现【难点】

 我们分析一下,上面步骤中使用到哪个方法,并逐一代码实现

(5-1) 移动到头节点moveToHead

不论是查询已有节点,还是写入新节点,都需要移动到链表头head

void moveToHead(Node node){
   //已经到表头,直接返回
   if(head.next == node)
     return;
   //删除原节点
   node.pre.next = node.next;
   node.next.pre = node.pre;
   //节点插入到头节点后
   insertToHead(node);
}

(5-2) 节点插入到头节点后 insertToHead

不存是写入新节点(3)的set方法,还是(5-1)的移动到头节点moveToHead, 都需要把节点插入到头节点

void insertToHead(Node node){
    node.next = head.next;
    node.pre = head;
    head.next.pre = node;
    head.next =node;
}

(5-3) 删除尾节点removeLastTail()

在缓存空间满的时候,如果需要新插入数据(3)的set方法中k<=0(缓存空间满), 删除缓存尾节点

void removeLastTail(){
    //哈希表需要删除这个key
    map.remove(tail.pre.key);
    //断连该节点
    tail.pre.pre.next = tail;
    tail.pre = tail.pre.pre;
}

四、代码完整实现(JAVA篇)

import java.util.*;

class Node{
    int key;
    int val;
    Node pre;
    Node next;
    //初始化
    Node(int key, int val){
        this.key = key;
        this.val = val;
        this.pre = null;
        this.next = null;
    }
}

class LRUCache {
    //全局变量,哈希表,头尾指针,剩余空间
    Map map = new HashMap<>();
    Node head = new Node(-1,-1);
    Node tail = new Node(-1,-1);
    int k = 0;

    //给全局变量进行赋值
    public LRUCache(int capacity) {
        head.next = tail;
        tail.pre = head;
        k = capacity;
    }
    
    public int get(int key) {
        int res = -1;
        if(map.containsKey(key)){
            Node node = map.get(key);
            res = node.val;
            moveToHead(node);
        }
        return res;
    }
    
    public void put(int key, int value) {
         if(map.containsKey(key)){
             //给这个已有节点赋新value
            map.get(key).val = value;
            moveToHead(map.get(key));
         }else{
            //插入新节点, 哈希表写入新节点
            Node node = new Node(key,value);
            map.put(key,node);
            if(k <= 0){
               //缓存空间已满,删除尾节点
               removeLastTail();  
            }
            //插入到头节点, 空间容量减少
            k--;
            insertToHead(node);
         }
    }

    void moveToHead(Node node){
        //此时node已经是头节点了
        if(head.next == node)
            return;
        node.pre.next = node.next;
        node.next.pre = node.pre;
        insertToHead(node);
    }

    void insertToHead(Node node){
        node.pre = head;
        node.next = head.next;
        head.next.pre = node;
        head.next = node;
    }

    void removeLastTail(){
        //哈希表删除尾节点
        map.remove(tail.pre.key);
        //链表删除尾
        tail.pre.pre.next = tail;
        tail.pre = tail.pre.pre;
    }
}

你可能感兴趣的:(程序员刷题分析成长之路,算法,缓存,链表,面试,java)