Skip to content

Commit c0ab614

Browse files
committed
chore: implement algo
1 parent 60bf6c1 commit c0ab614

File tree

4 files changed

+245
-0
lines changed

4 files changed

+245
-0
lines changed

7-lfu-cache/cache_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package main
2+
3+
import (
4+
"slices"
5+
"testing"
6+
)
7+
8+
func TestCache(t *testing.T) {
9+
cache, err := NewLFUCache(3, func(key string) (string, error) {
10+
return "", nil
11+
})
12+
if err != nil {
13+
t.Fatal(err)
14+
}
15+
16+
err = cache.Set("vu", "10")
17+
err = cache.Set("nghia", "20")
18+
err = cache.Set("luan", "5")
19+
20+
value, err := cache.Get("vu")
21+
if value != "10" {
22+
t.Errorf("value should be 10, got %s", value)
23+
}
24+
25+
value, err = cache.Get("nghia")
26+
if value != "20" {
27+
t.Errorf("value should be 20, got %s", value)
28+
}
29+
30+
err = cache.Set("xanh", "30")
31+
32+
keys := cache.GetKeys()
33+
if slices.Contains(keys, "luan") {
34+
t.Errorf("keys should not contain luan")
35+
}
36+
}

7-lfu-cache/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module lfu_cache
2+
3+
go 1.24.2

7-lfu-cache/main.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package main
2+
3+
import (
4+
"container/list"
5+
"errors"
6+
"sync"
7+
)
8+
9+
var EMPTY_ERROR = errors.New("EMPTY ERROR")
10+
11+
type Cache interface {
12+
Get(key string) (string, error)
13+
Set(key, value string) error
14+
}
15+
16+
type (
17+
LoaderFunc func(key string) (string, error)
18+
)
19+
20+
type baseCache struct {
21+
mu sync.RWMutex
22+
size int
23+
loaderFunc LoaderFunc
24+
loadGroup LoadGroup
25+
}
26+
27+
type LFUCache struct {
28+
baseCache
29+
cache map[string]*lfuItem
30+
list *list.List // list of lruEntry
31+
}
32+
33+
type lruEntry struct {
34+
freq int
35+
items map[*lfuItem]struct{}
36+
}
37+
38+
type lfuItem struct {
39+
value string
40+
key string
41+
el *list.Element // Reference to lruEntry
42+
}
43+
44+
type LoadGroup struct {
45+
}
46+
47+
func NewLFUCache(size int, loaderFunc LoaderFunc) (*LFUCache, error) {
48+
if size <= 0 {
49+
return nil, errors.New("size must be greater than zero")
50+
}
51+
52+
cache := &LFUCache{
53+
cache: make(map[string]*lfuItem),
54+
list: list.New(),
55+
}
56+
57+
cache.baseCache.size = size
58+
cache.baseCache.loaderFunc = loaderFunc
59+
cache.baseCache.loadGroup = LoadGroup{}
60+
61+
return cache, nil
62+
}
63+
64+
func (cache *LFUCache) Get(key string) (string, error) {
65+
if item, ok := cache.cache[key]; ok {
66+
// Move item to the higher bucket
67+
err := cache.moveToHigherBucket(item)
68+
if err != nil {
69+
return "", err
70+
}
71+
72+
return item.value, nil
73+
}
74+
75+
// Miss, so load value
76+
value, err := cache.loaderFunc(key)
77+
if err != nil {
78+
return "", err
79+
}
80+
81+
err = cache.Set(key, value)
82+
if err != nil {
83+
return "", err
84+
}
85+
86+
return value, nil
87+
}
88+
89+
func (cache *LFUCache) GetKeys() []string {
90+
keys := make([]string, 0)
91+
for k, _ := range cache.cache {
92+
keys = append(keys, k)
93+
}
94+
95+
return keys
96+
}
97+
98+
func (cache *LFUCache) Set(key, value string) error {
99+
if item, ok := cache.cache[key]; ok {
100+
item.value = value
101+
return nil
102+
}
103+
104+
if len(cache.cache) >= cache.size {
105+
err := cache.evict()
106+
if err != nil && !errors.Is(err, EMPTY_ERROR) {
107+
return err
108+
}
109+
}
110+
111+
cache.insert(key, value)
112+
return nil
113+
}
114+
115+
// insert inserts key, value knowing that there is always slot for it
116+
func (cache *LFUCache) insert(key, value string) {
117+
insertedItem := &lfuItem{
118+
value: value,
119+
key: key,
120+
}
121+
122+
cache.cache[key] = insertedItem
123+
124+
var firstEntry *lruEntry
125+
var firstElement *list.Element
126+
if cache.list.Front() == nil || cache.list.Front().Value.(*lruEntry).freq != 0 {
127+
firstEntry = &lruEntry{
128+
freq: 0,
129+
items: make(map[*lfuItem]struct{}),
130+
}
131+
132+
firstElement = cache.list.PushFront(firstEntry)
133+
} else {
134+
firstElement = cache.list.Front()
135+
firstEntry = firstElement.Value.(*lruEntry)
136+
}
137+
138+
firstEntry.items[insertedItem] = struct{}{}
139+
insertedItem.el = firstElement
140+
}
141+
142+
func getItemToEvict(mapp map[*lfuItem]struct{}) (*lfuItem, error) {
143+
for key, _ := range mapp {
144+
return key, nil
145+
}
146+
147+
return nil, EMPTY_ERROR
148+
}
149+
150+
func (cache *LFUCache) evict() error {
151+
zeroBucket := cache.list.Front()
152+
if zeroBucket == nil {
153+
return EMPTY_ERROR
154+
}
155+
156+
items := zeroBucket.Value.(*lruEntry).items
157+
itemToRemove, err := getItemToEvict(items)
158+
if err != nil {
159+
return err
160+
}
161+
162+
delete(items, itemToRemove)
163+
if len(items) == 0 {
164+
cache.list.Remove(zeroBucket)
165+
}
166+
167+
delete(cache.cache, itemToRemove.key)
168+
169+
return nil
170+
}
171+
172+
func (cache *LFUCache) moveToHigherBucket(item *lfuItem) error {
173+
if item == nil {
174+
return errors.New("item is nil")
175+
}
176+
177+
curBucket := item.el
178+
curBucketEntry := curBucket.Value.(*lruEntry)
179+
nextFreq := curBucketEntry.freq + 1
180+
delete(curBucketEntry.items, item)
181+
182+
var nextBucket *list.Element
183+
if item.el.Next() == nil || item.el.Next().Value.(*lruEntry).freq > nextFreq {
184+
nextBucketEntry := &lruEntry{
185+
freq: nextFreq,
186+
items: make(map[*lfuItem]struct{}),
187+
}
188+
189+
nextBucketEntry.items[item] = struct{}{}
190+
nextBucket = cache.list.InsertAfter(nextBucketEntry, item.el)
191+
} else {
192+
nextBucket = item.el.Next()
193+
nextBucketEntry := nextBucket.Value.(*lruEntry)
194+
nextBucketEntry.items[item] = struct{}{}
195+
}
196+
197+
item.el = nextBucket
198+
199+
// Remove last bucket in case it is empty
200+
if len(curBucketEntry.items) == 0 {
201+
cache.list.Remove(curBucket)
202+
}
203+
204+
return nil
205+
}

playground/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package playground

0 commit comments

Comments
 (0)