首页 > 其他分享 >使用 Go 语言实现 LRU 缓存

使用 Go 语言实现 LRU 缓存

时间:2024-11-05 15:47:35浏览次数:5  
标签:Node 缓存 cache 链表 tail LRU Go 节点

文章目录

在日常开发中,缓存是提高系统性能的重要手段。LRU(Least Recently Used)缓存是一种基于“最近最少使用”策略的缓存系统,其目的是在空间受限的情况下保留最新、最常用的数据。当缓存空间不足时,LRU 缓存会优先淘汰最久未被使用的数据,从而保持缓存的时效性。本文将详细讲解如何使用 Go 语言实现一个 LRU 缓存。

LRU 缓存的关键特性

  • 快速访问缓存内容Get 操作的时间复杂度为 (O(1))。
  • 快速插入和更新缓存Put 操作的时间复杂度也为 (O(1))。
  • 淘汰最久未使用的数据:当缓存满时,移除最久未访问的数据。

数据结构选型

为了实现 LRU 缓存的上述特性,常用的数据结构组合为 哈希表双向链表

  • 哈希表:用于快速访问缓存节点。
  • 双向链表:管理节点的访问顺序。每次访问时,将节点移动到链表头部;当缓存满时,移除链表尾部节点(即最久未访问的数据)。

通过这种组合,GetPut 的时间复杂度均为 (O(1))。

LRU 缓存的结构设计

在 LRU 缓存的设计中,我们需要以下两个核心组件:

  1. 双向链表节点 Node

    • 存储缓存的 keyvalue
    • 通过 prevnext 指针指向前后节点。
  2. LRUCache 缓存结构

    • capacity:缓存的容量。
    • cache:使用 map[int]*Node 作为哈希表,存储键值对和链表节点的映射。
    • headtail:虚拟头尾节点,用于链表的边界处理,避免在插入和删除操作时对边界条件进行额外判断。

操作流程图

下面是 LRU 缓存的整体操作流程概览:

Yes No Yes No Start Get Key Key Exists? Move Node to Head End Create New Node Exceeds Capacity? Remove Tail Node Add Node to Head

代码实现

1. 定义节点和缓存结构

我们首先定义双向链表节点 NodeLRUCache 结构:

package main

import "fmt"

// 双向链表的节点
type Node struct {
	key, value int
	prev, next *Node
}

// LRUCache 缓存结构
type LRUCache struct {
	capacity   int
	cache      map[int]*Node // 哈希表,快速定位节点
	head, tail *Node         // 虚拟头尾节点
}

2. 初始化 LRU 缓存

Constructor 方法中,初始化缓存容量 capacity 和哈希表 cache,并创建 headtail 虚拟节点。headtail 之间没有数据节点,它们仅用于简化节点插入和删除的边界处理。此时,链表初始状态如下:

    head <--> tail

初始化代码如下:

// 构造函数
func Constructor(capacity int) LRUCache {
	cache := LRUCache{
		capacity: capacity,
		cache:    make(map[int]*Node),
		head:     &Node{},
		tail:     &Node{},
	}
	cache.head.next = cache.tail
	cache.tail.prev = cache.head
	return cache
}

3. 获取缓存值(Get 方法)

Get 方法根据 key 查找缓存中的数据。如果数据存在,则将该节点移到链表头部,标记为“最近使用”;如果数据不存在,则返回 -1。

// 获取缓存中的值
func (this *LRUCache) Get(key int) int {
	if node, found := this.cache[key]; found {
		this.moveToHead(node) // 访问后移至头部
		return node.value
	}
	return -1 // 如果不存在,返回 -1
}

在调用 moveToHead 方法时,节点被移动到链表头部。假设我们在链表中有节点顺序:head <-> A <-> B <-> tail,访问 B 后,链表状态变为:

    head <--> B <--> A <--> tail

4. 更新或插入值(Put 方法)

Put 方法根据 key 更新值,或在缓存中插入新的键值对。如果缓存超过容量限制,则移除链表尾部的节点。

// 更新或插入值
func (this *LRUCache) Put(key int, value int) {
	if node, found := this.cache[key]; found {
		node.value = value
		this.moveToHead(node) // 已存在的节点移至头部
	} else {
		// 创建新节点并加入头部
		newNode := &Node{key: key, value: value}
		this.cache[key] = newNode
		this.addNode(newNode)

		// 超出容量时,删除尾节点
		if len(this.cache) > this.capacity {
			tail := this.popTail()
			delete(this.cache, tail.key)
		}
	}
}

当缓存满时,popTail 方法删除链表尾部节点。假设链表当前状态为:head <-> B <-> A <-> tail,插入新节点 C 后,链表状态变为:

    head <--> C <--> B <--> tail

节点 A 被淘汰,从而控制了缓存的空间限制。

5. 辅助方法

addNoderemoveNodemoveToHeadpopTail 是缓存核心操作的辅助方法,用于管理链表中节点的插入、删除和移动。

// 添加节点至头部
func (this *LRUCache) addNode(node *Node) {
	node.prev = this.head
	node.next = this.head.next
	this.head.next.prev = node
	this.head.next = node
}

// 删除节点
func (this *LRUCache) removeNode(node *Node) {
	prev := node.prev
	next := node.next
	prev.next = next
	next.prev = prev
}

// 移动节点到头部
func (this *LRUCache) moveToHead(node *Node) {
	this.removeNode(node)
	this.addNode(node)
}

// 弹出尾部节点
func (this *LRUCache) popTail() *Node {
	tail := this.tail.prev
	this.removeNode(tail)
	return tail
}

插入节点到链表头部的图示

addNode 方法的核心步骤如下:假设链表初始状态为 head <-> A <-> B <-> tail,插入新节点 nodehead 后,链表状态变为:

    head <--> node <--> A <--> B <--> tail

6. 单元测试代码

为验证实现正确性,可以使用以下测试:

import "testing"

func TestLRUCache(t *testing.T) {
	cache := Constructor(2)

	cache.Put(1, 1)
	cache.Put(2, 2)
	if cache.Get(1) != 1 {
		t.Errorf("Expected 1, got %d", cache.Get(1))
	}

	cache.Put(3, 3) // 淘汰 key 2
	if cache.Get(2) != -1 {
		t.Errorf("Expected -1, got %d", cache.Get(2))
	}

	cache.Put(4, 4) // 淘汰 key 1
	if cache.Get(1) != -1 {
		t.Errorf("Expected -1, got %d", cache.Get(1))
	}
	if cache.Get(3) != 3 {
		t.Errorf("Expected 3, got %d", cache.Get(3))
	}
	if cache.Get(4) != 4 {
		t.Errorf("Expected 4, got %d", cache.Get(4))
	}
}

总结

通过双向链表和哈希表的结合,实现了一个高效的 LRU 缓存,使 GetPut 操作在 O(1) 的时间内完成。双向链表和虚拟节点的设计简化了链表边界的处理,广泛适用于缓存系统中。


关注我

标签:Node,缓存,cache,链表,tail,LRU,Go,节点
From: https://blog.csdn.net/tatasix/article/details/143403385

相关文章

  • Go 语言变量类型:从入门到精通,一篇搞定所有知识点!
    Go语言变量类型1.基本类型1.1数值类型1.2布尔类型1.3字符串类型2.复合类型2.1数组2.2切片2.3字典(map)2.4结构体2.5接口3.类型转换4.零值5.示例1.基本类型Go语言中的基本类型主要包括数值类型、布尔类型和字符串类型。1.1数值类型整型:int:根据......
  • golang占位符%v、%+v、%#v详解
    目录%v%+v%#v在Go语言中,fmt包提供了格式化字符串的功能,类似于C语言的printf函数。fmt包中的%v、%+v和%#v是用于格式化输出的占位符,它们各自有不同的用途。%v含义:%v表示以默认格式(值)输出变量。对于基本类型如整数、浮点数等,它会直接输出其值;对于结构体,它会输出......
  • Google Play 三季度应用下架报告:下架约 180万款应用,同比增长 80%
    大家好,我是牢鹅!聊到GooglePlay封号下架,相信大伙应该不陌生了吧!由于前些年各种捞偏门的App以及大量马甲包的出现,让谷歌不停的更新它们的审核机制,特别是近年谷歌开始大规模使用大模型对开发者的账号、应用扫描,导致很多做绿色合规应用的开发者被误封与下架,这也大大提高了普通开......
  • 九、Go语言快速入门之map
    文章目录Map:one:使用`Map`:star2:声明和初始化:star2:`map`容量:star2:用切片作为`map`的值:two:测试键值对是否存在及删除元素:three:`For`-`range`:four:`map`类型的切片:five:map的排序:six:将map的健和值对调......
  • Linux基础——服务器Raid阵列卡开启cache缓存
    服务器Raid阵列卡开启cache缓存一、问题描述客户业务环境:本地存储型裸金属服务器做NFS服务器,15台以上的客户端接入服务器,读写大量的小文件,客户读写速录慢的现象;影响读写速率:磁盘性能和磁盘缓存,容易造成大量的IO拥塞;二、问题分析裸金属NFS服务器单盘最大IOPS2200,一台主机可能......
  • MySQL导入sql文件报错:2006 - MySQL server has gone away(转载)
    今天在在MySQL导入sql文件,导入失败,出现如下错误:2006-MySQLserverhasgoneaway,之前也遇到过,又一次遇到,还是记录一下吧!【问题】导入的sql文件大概有15M,导入过程中报错:2006-MySQLserverhasgoneaway  【解决办法】1、找到MySQL安装目录下的my.ini文件,修改max_allo......
  • 基于django框架开发在线书店推荐系统 python实现个性化网上书店/图书购物商城推荐网站
    基于django框架开发在线书店推荐系统python实现个性化网上书店/图书购物商城推荐网站爬虫、兴趣标签、排行榜标签推荐、热点推荐、协同过滤算法推荐大数据深度学习机器学习人工智能WebBookShopRecPy一、项目简介1、开发工具和使用技术Pycharm、Python3及以上版本,D......
  • 基于django框架开发在线美食推荐系统 python实现个性化美食食谱推荐系统 爬虫、排行榜
    基于django框架开发在线美食推荐系统python实现个性化美食食谱推荐系统爬虫、排行榜、可视化数据分析基于流行度热点推荐、基于用户/物品协同过滤算法推荐、平均加权混合推荐大数据深度学习机器学习OnlineFoodRecommendPy一、项目简介1、开发工具和使用技术Pycharm......
  • PbootCMS网站后台管理系统登录界面描述/LOGO/背景图/介绍修改
    1.修改登录界面描述位置:登录页面通常会有一个简短的系统或公司介绍。修改方法:找到登录页面的模板文件,通常位于 /template/admin/login.html。在该文件中找到描述文本的部分,通常是 <p> 标签内的内容。直接修改该段落的内容即可。2.修改LOGO位置:LOGO通常显......
  • Golang channel底层原理
    1原理默认情况下,读写未就绪的channel(读没有数据的channel,或者写缓冲区已满的channel)时,协程会被阻塞。但是当读写channel操作和select搭配使用时,即使channel未就绪,也可以执行其它分支,当前协程不会被阻塞。ch:=make(chanint)select{case<-ch:default:}本文......