如何手写一个 LRU ?(不使用Map Set 完成 O(1) 效率 )

手写 LRU

题目

用 JS 实现一个 LRU 缓存

LRU 使用

Least Recently Used 最近最少使用

即淘汰掉最近最少使用的数据,只保留最近经常使用的资源。它是一个固定容量的缓存容器

const lruCache = new LRUCache(2); // 最大缓存长度 2
lruCache.set(1, 1); // 缓存是 {1=1}
lruCache.set(2, 2); // 缓存是 {1=1, 2=2}
lruCache.get(1);    // 返回 1
lruCache.set(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lruCache.get(2);    // 返回 null
lruCache.set(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lruCache.get(1);    // 返回 null
lruCache.get(3);    // 返回 3
lruCache.get(4);    // 返回 4

分析

  • 哈希表,即 { k1: v1, k2: v2, ... } 形式。可以 O(1) 事件复杂度存取 key value
  • 有序。可以根据最近使用情况清理缓存

JS 内置的数据结构类型 Object Array Set Map ,恰好 Map 符合这两条要求

Map 是有序的

Map 有序,Object 无序

实现

class LRUCache {
  length = 0
  data = new Map()
  constructor(length) {
    if (length < 1) throw new Error('invalid length')
    this.length = length
  }

  set(key, value) {
    const data = this.data
    if (data.has(key)) {
      data.delete(key)
    }
    data.set(key, value)

    if (data.size > this.length) {
      const delKay = data.keys().next().value // 最前面的元素
      data.delete(delKay)
    }
  }

  get(key) {
    const data = this.data

    if (!data.has(key)) return null

    const value = data.get(key)

    // 更新到最前面
    data.delete(key)
    data.set(key, value)

    return value
  }
}

注意,get set 时都要把操作数据移动到 Map 最新的位置。

扩展

实际项目中可以使用第三方 lib

  • https://www.npmjs.com/package/quick-lru
  • https://www.npmjs.com/package/lru-cache
  • https://www.npmjs.com/package/tiny-lru
  • https://www.npmjs.com/package/mnemonist

连环问:不用 Map 如何实现 LRU cache ?

LRU cache 是很早就有的算法,而 Map 仅仅是这几年才加入的 ES 语法。

使用 Object 和 Array

根据上文的分析,两个条件

  • 哈希表,可以用 Object 实现
  • 有序,可以用 Array 实现
// 执行 lru.set('a', 1) lru.set('b', 2) lru.set('c', 3) 后的数据

const obj1 = { value: 1, key: 'a' }
const obj2 = { value: 2, key: 'b' }
const obj3 = { value: 3, key: 'c' }

const data = [obj1, obj2, obj3]
const map = { 'a': obj1, 'b': obj2, 'c': obj3 }

模拟 get set 操作,会发现几个问题,都来自于数组

  • 超出 cache 容量时,要移除最早的元素,数组 shift 效率低
  • 每次 get set 时都要把当前元素移动到最新的位置,数组 splice 效率低

Array 改为双向链表

数组有问题,就需要使用新的数据结构 双向链表

Interface INode {
    value: any
    next?: INode
    prev?: INode
}

双向链表可以快速移动元素。末尾新增元素 D 很简单,开头删除 A 元素也很简单。

要把中间的元素 B 移动到最后(如 LRU set get 时移动数据位置),只需要修改前后的指针即可,效率很高。

实现

interface IListNode {
  value: any
  key: string
  prev?: IListNode
  next?: IListNode
}

class LRUCache {
  private length: number
  private data: { [key: string]: IListNode } = {}
  private dataLength: number = 0
  private listHead: IListNode | null = null
  private listTail: IListNode | null = null

  constructor(length: number) {
    if (length < 1) throw new Error('invalid length')
    this.length = length
  }

  // 移动到末尾(最新)
  private moveToTail(curNode: IListNode) {
    const tail = this.listTail
    if (tail === curNode) return

    // 1、让 preNode 和 nextNode 建立关系
    const preNode = curNode.prev
    const nextNode = curNode.next
    if (preNode) {
      if (nextNode) {
        preNode.next = nextNode
      } else {
        delete preNode.next
      }
    }
    if (nextNode) {
      if (preNode) {
        nextNode.prev = preNode
      } else {
        delete nextNode.prev
      }

      // 头指针更新
      if (this.listHead === curNode) this.listHead = nextNode
    }
    // 2、让 curNode 断绝与 preNode 和 nextNode 的关系
    delete curNode.prev
    delete curNode.next
    // 3、在 list 末尾建立与 curNode 的新关系
    if (tail) {
      // 先后再前
      tail.next = curNode
      curNode.prev = tail
    }
    this.listTail = curNode
  }

  private tryClean() {
    while (this.dataLength > this.length) {
      const head = this.listHead
      if (head == null) throw new Error('head is null')
      const headNext = head.next
      if (headNext == null) throw new Error('headNext is null')

      // 1、断绝 head 和 next 的关系
      delete headNext?.prev
      delete head.next

      // 2、重新赋值 listHead
      this.listHead = headNext

      // 3、清理data, 减小长度
      delete this.data[head.key]
      this.dataLength--
    }
  }

  get(key: string): any {
    const data = this.data
    const curNode = data[key]

    if (curNode == null) return null

    if (this.listTail === curNode) {
      // 在末尾直接返回
      return curNode.value
    }

    // 不在末尾则需要移动到末尾
    this.moveToTail(curNode)

    return curNode.value
  }

  set(key: string, value: any) {
    const data = this.data
    const curNode = data[key]

    if (curNode == null) {
      // 新增数据
      const newNode: IListNode = { key, value }
      // 移动到末尾
      this.moveToTail(newNode)

      data[key] = newNode
      this.dataLength++

      // 首个元素的处理
      if (this.dataLength === 1) this.listHead = newNode
    } else {
      // 修改现有数据
      curNode.value = value
      this.moveToTail(curNode)
    }

    // 尝试清理长度
    this.tryClean()
  }
}

注意事项

  • 数据结构如何定义,data 和链表分别存储什么
  • 双向链表的操作(非常繁琐,写代码很容易出错,逻辑一定要清晰!!!)
  • 链表 node 中要存储 data.key ,否则删除 data 需要遍历、效率低
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值