initial version
This commit is contained in:
parent
222aa4da0d
commit
ac292ced9b
100
lru.go
Normal file
100
lru.go
Normal file
@ -0,0 +1,100 @@
|
||||
package lru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
key, value any
|
||||
prev, next *node
|
||||
}
|
||||
|
||||
type LRUCache struct {
|
||||
capacity int
|
||||
cache map[any]*node
|
||||
head *node
|
||||
tail *node
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewLRUCache(capacity int) *LRUCache {
|
||||
head := &node{}
|
||||
tail := &node{}
|
||||
head.next = tail
|
||||
tail.prev = head
|
||||
|
||||
return &LRUCache{
|
||||
capacity: capacity,
|
||||
cache: make(map[any]*node, capacity),
|
||||
head: head,
|
||||
tail: tail,
|
||||
}
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Get(key any) (any, bool) {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
if node, exists := lru.cache[key]; exists {
|
||||
lru.moveToHead(node)
|
||||
return node.value, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Put(key, value any) {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
if node, exists := lru.cache[key]; exists {
|
||||
node.value = value
|
||||
lru.moveToHead(node)
|
||||
return
|
||||
}
|
||||
|
||||
newNode := &node{key: key, value: value}
|
||||
lru.cache[key] = newNode
|
||||
lru.addToHead(newNode)
|
||||
|
||||
if len(lru.cache) > lru.capacity {
|
||||
removed := lru.removeTail()
|
||||
delete(lru.cache, removed.key)
|
||||
}
|
||||
}
|
||||
|
||||
func (lru *LRUCache) moveToHead(node *node) {
|
||||
lru.removeNode(node)
|
||||
lru.addToHead(node)
|
||||
}
|
||||
|
||||
func (lru *LRUCache) removeNode(node *node) {
|
||||
node.prev.next = node.next
|
||||
node.next.prev = node.prev
|
||||
}
|
||||
|
||||
func (lru *LRUCache) addToHead(node *node) {
|
||||
node.prev = lru.head
|
||||
node.next = lru.head.next
|
||||
lru.head.next.prev = node
|
||||
lru.head.next = node
|
||||
}
|
||||
|
||||
func (lru *LRUCache) removeTail() *node {
|
||||
node := lru.tail.prev
|
||||
lru.removeNode(node)
|
||||
return node
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Len() int {
|
||||
lru.mu.RLock()
|
||||
defer lru.mu.RUnlock()
|
||||
return len(lru.cache)
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Clear() {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
lru.cache = make(map[any]*node, lru.capacity)
|
||||
lru.head.next = lru.tail
|
||||
lru.tail.prev = lru.head
|
||||
}
|
286
lru_test.go
Normal file
286
lru_test.go
Normal file
@ -0,0 +1,286 @@
|
||||
package lru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewLRUCache(t *testing.T) {
|
||||
cache := NewLRUCache(5)
|
||||
if cache.capacity != 5 {
|
||||
t.Errorf("expected capacity 5, got %d", cache.capacity)
|
||||
}
|
||||
if cache.Len() != 0 {
|
||||
t.Errorf("expected empty cache, got length %d", cache.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutAndGet(t *testing.T) {
|
||||
cache := NewLRUCache(3)
|
||||
|
||||
// Test Put and Get
|
||||
cache.Put("a", 1)
|
||||
cache.Put("b", 2)
|
||||
cache.Put("c", 3)
|
||||
|
||||
val, exists := cache.Get("a")
|
||||
if !exists || val != 1 {
|
||||
t.Errorf("expected (1, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
|
||||
val, exists = cache.Get("b")
|
||||
if !exists || val != 2 {
|
||||
t.Errorf("expected (2, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
|
||||
// Test miss
|
||||
val, exists = cache.Get("z")
|
||||
if exists {
|
||||
t.Errorf("expected (nil, false), got (%v, %v)", val, exists)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateExistingKey(t *testing.T) {
|
||||
cache := NewLRUCache(2)
|
||||
|
||||
cache.Put("a", 1)
|
||||
cache.Put("a", 10) // Update
|
||||
|
||||
val, exists := cache.Get("a")
|
||||
if !exists || val != 10 {
|
||||
t.Errorf("expected (10, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
|
||||
if cache.Len() != 1 {
|
||||
t.Errorf("expected length 1, got %d", cache.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEviction(t *testing.T) {
|
||||
cache := NewLRUCache(3)
|
||||
|
||||
cache.Put("a", 1)
|
||||
cache.Put("b", 2)
|
||||
cache.Put("c", 3)
|
||||
cache.Put("d", 4) // Should evict "a"
|
||||
|
||||
_, exists := cache.Get("a")
|
||||
if exists {
|
||||
t.Error("expected 'a' to be evicted")
|
||||
}
|
||||
|
||||
val, exists := cache.Get("b")
|
||||
if !exists || val != 2 {
|
||||
t.Errorf("expected (2, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUOrdering(t *testing.T) {
|
||||
cache := NewLRUCache(3)
|
||||
|
||||
cache.Put("a", 1)
|
||||
cache.Put("b", 2)
|
||||
cache.Put("c", 3)
|
||||
|
||||
// Access "a" to make it recently used
|
||||
cache.Get("a")
|
||||
|
||||
// Add "d", should evict "b" (least recently used)
|
||||
cache.Put("d", 4)
|
||||
|
||||
_, exists := cache.Get("b")
|
||||
if exists {
|
||||
t.Error("expected 'b' to be evicted")
|
||||
}
|
||||
|
||||
// Verify others still exist
|
||||
val, exists := cache.Get("a")
|
||||
if !exists || val != 1 {
|
||||
t.Errorf("expected (1, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
|
||||
val, exists = cache.Get("c")
|
||||
if !exists || val != 3 {
|
||||
t.Errorf("expected (3, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
|
||||
val, exists = cache.Get("d")
|
||||
if !exists || val != 4 {
|
||||
t.Errorf("expected (4, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateMakesRecent(t *testing.T) {
|
||||
cache := NewLRUCache(3)
|
||||
|
||||
cache.Put("a", 1)
|
||||
cache.Put("b", 2)
|
||||
cache.Put("c", 3)
|
||||
|
||||
// Update "a" to make it recently used
|
||||
cache.Put("a", 10)
|
||||
|
||||
// Add "d", should evict "b"
|
||||
cache.Put("d", 4)
|
||||
|
||||
_, exists := cache.Get("b")
|
||||
if exists {
|
||||
t.Error("expected 'b' to be evicted")
|
||||
}
|
||||
|
||||
val, exists := cache.Get("a")
|
||||
if !exists || val != 10 {
|
||||
t.Errorf("expected (10, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
cache := NewLRUCache(3)
|
||||
|
||||
cache.Put("a", 1)
|
||||
cache.Put("b", 2)
|
||||
cache.Put("c", 3)
|
||||
|
||||
cache.Clear()
|
||||
|
||||
if cache.Len() != 0 {
|
||||
t.Errorf("expected empty cache after clear, got length %d", cache.Len())
|
||||
}
|
||||
|
||||
_, exists := cache.Get("a")
|
||||
if exists {
|
||||
t.Error("expected cache to be empty after clear")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleCapacity(t *testing.T) {
|
||||
cache := NewLRUCache(1)
|
||||
|
||||
cache.Put("a", 1)
|
||||
cache.Put("b", 2) // Should evict "a"
|
||||
|
||||
_, exists := cache.Get("a")
|
||||
if exists {
|
||||
t.Error("expected 'a' to be evicted")
|
||||
}
|
||||
|
||||
val, exists := cache.Get("b")
|
||||
if !exists || val != 2 {
|
||||
t.Errorf("expected (2, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDifferentTypes(t *testing.T) {
|
||||
cache := NewLRUCache(3)
|
||||
|
||||
// Test with different key/value types
|
||||
cache.Put(1, "one")
|
||||
cache.Put("two", 2)
|
||||
cache.Put(3.14, []int{1, 2, 3})
|
||||
|
||||
val, exists := cache.Get(1)
|
||||
if !exists || val != "one" {
|
||||
t.Errorf("expected ('one', true), got (%v, %v)", val, exists)
|
||||
}
|
||||
|
||||
val, exists = cache.Get("two")
|
||||
if !exists || val != 2 {
|
||||
t.Errorf("expected (2, true), got (%v, %v)", val, exists)
|
||||
}
|
||||
|
||||
val, exists = cache.Get(3.14)
|
||||
if !exists {
|
||||
t.Errorf("expected slice value, got (%v, %v)", val, exists)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentAccess(t *testing.T) {
|
||||
cache := NewLRUCache(100)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrent puts
|
||||
for i := range 50 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
cache.Put(i, i*10)
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent gets
|
||||
for i := range 50 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
cache.Get(i)
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent updates
|
||||
for i := range 25 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
cache.Put(i, i*100)
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent len checks
|
||||
for range 10 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
cache.Len()
|
||||
}()
|
||||
}
|
||||
|
||||
// Concurrent clear
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
cache.Clear()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestRaceCondition(t *testing.T) {
|
||||
cache := NewLRUCache(10)
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
for i := range 1000 {
|
||||
cache.Put(i%10, i)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := range 1000 {
|
||||
cache.Get(i % 10)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
<-done
|
||||
<-done
|
||||
}
|
||||
|
||||
func BenchmarkPut(b *testing.B) {
|
||||
cache := NewLRUCache(1000)
|
||||
|
||||
for i := 0; b.Loop(); i++ {
|
||||
cache.Put(i%1000, i)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGet(b *testing.B) {
|
||||
cache := NewLRUCache(1000)
|
||||
for i := range 1000 {
|
||||
cache.Put(i, i)
|
||||
}
|
||||
|
||||
for i := 0; b.Loop(); i++ {
|
||||
cache.Get(i % 1000)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user