Skip to content

Commit 2bae556

Browse files
authored
Add Advanced Lru Cache (#139)
1 parent 30782bc commit 2bae556

File tree

12 files changed

+254
-51
lines changed

12 files changed

+254
-51
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ multiple times and be persistent over time.
133133
- [Coins](src/test/kotlin/com/igorwojda/list/coins)
134134
- [Medan Of Sorted Lists](src/test/kotlin/com/igorwojda/list/medianoftwosorted)
135135
- [LRU Cache](src/test/kotlin/com/igorwojda/cache/lru)
136+
- [Advanced Cache](src/test/kotlin/com/igorwojda/cache/advancedlru)
136137

137138
# Useful links
138139

misc/ChallengeGroups.md

+1
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,4 @@ We use sliding window instead of nested loops which decreases complexity from `O
184184
## Cache
185185

186186
- [LRU Cache](../src/test/kotlin/com/igorwojda/cache/lru)
187+
- [Advanced Cache](../src/test/kotlin/com/igorwojda/cache/advancedlru)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.igorwojda.cache.advancedlru
2+
3+
import org.amshove.kluent.shouldBeEqualTo
4+
import org.junit.jupiter.api.Test
5+
import java.util.*
6+
7+
class AdvancedLRUCache(private val capacity: Int) {
8+
fun put(key: String, value: Int, priority: Int, expiryTime: Long) {
9+
TODO("Add your solution here")
10+
}
11+
12+
fun get(key: String): Int? {
13+
TODO("Add your solution here")
14+
}
15+
16+
private fun remove(key: String) {
17+
TODO("Add your solution here")
18+
}
19+
20+
private fun clearCache() {
21+
TODO("Add your solution here")
22+
}
23+
24+
private data class CacheItem(
25+
val key: String,
26+
var value: Int,
27+
var priority: Int,
28+
var expiryTime: Long,
29+
) : Comparable<CacheItem> {
30+
var lastUsed: Long = System.currentTimeMillis()
31+
32+
override fun compareTo(other: CacheItem): Int {
33+
return when {
34+
this.expiryTime != other.expiryTime -> this.expiryTime.compareTo(other.expiryTime)
35+
this.priority != other.priority -> this.priority.compareTo(other.priority)
36+
else -> this.lastUsed.compareTo(other.lastUsed)
37+
}
38+
}
39+
}
40+
41+
// Returns fixed system time in milliseconds
42+
private fun getSystemTimeForExpiry() = 1000
43+
}
44+
45+
private class Test {
46+
@Test
47+
fun `add and get`() {
48+
val cache = AdvancedLRUCache(2)
49+
cache.put("A", 1, 5, 5000)
50+
51+
cache.get("A") shouldBeEqualTo 1
52+
}
53+
54+
@Test
55+
fun `evict by priority`() {
56+
val cache = AdvancedLRUCache(2)
57+
cache.put("A", 1, 1, 3000)
58+
cache.put("B", 2, 3, 4000)
59+
cache.put("C", 3, 4, 5000)
60+
61+
// This should be null because "A" was evicted due to lower priority.
62+
cache.get("A") shouldBeEqualTo null
63+
cache.get("B") shouldBeEqualTo 2
64+
cache.get("C") shouldBeEqualTo 3
65+
}
66+
67+
@Test
68+
fun `evict by expiry`() {
69+
val cache = AdvancedLRUCache(2)
70+
cache.put("A", 1, 1, 500)
71+
cache.put("B", 2, 3, 700)
72+
73+
// This should be null because "A" was evicted due to expiry.
74+
cache.get("A") shouldBeEqualTo null
75+
cache.get("B") shouldBeEqualTo null
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Advanced LRU Cache
2+
3+
## Nice to solve before
4+
5+
- [LRU Cache](../lru/README.md)
6+
7+
## Instructions
8+
9+
Design a data structure that enables the storage and retrieval of items via a key, subject to a specified capacity
10+
limit. In cases where the addition of new items exceeds this capacity, ensure that space is made available through the
11+
following sequence of operations:
12+
13+
- Firstly, discard items that have exceeded their validity period (`expiryTime` > `getSystemTimeForExpiry()`).
14+
- If there are no items past their validity, identify the items with the lowest priority rating and from these, remove
15+
the item that was least recently accessed or used.
16+
17+
To simplify expiry logic testing use provided `getSystemTime()` method (instead of `System.currentTimeMillis()`) that
18+
will return fixed system time in milliseconds.
19+
20+
[Challenge](Challenge.kt) | [Solution](Solution.kt)
21+
22+
## Examples
23+
24+
```kotlin
25+
val cache = AdvancedLRUCache(2)
26+
cache.put("A", 1, 5, 5000)
27+
cache.get("A") // 1
28+
```
29+
30+
```kotlin
31+
val cache = AdvancedLRUCache(2)
32+
cache.put("A", 1, 1, 3000)
33+
cache.put("B", 2, 3, 4000)
34+
cache.put("C", 3, 4, 5000)
35+
36+
37+
cache.get("A") // null - "A" was evicted due to lower priority.
38+
cache.get("B") // 2
39+
cache.get("C") // 3
40+
```
41+
42+
```kotlin
43+
val cache = AdvancedLRUCache(2)
44+
cache.put("A", 1, 1, 500)
45+
cache.put("B", 2, 3, 700)
46+
47+
cache.get("A") // null - "A" was evicted due to expiry.
48+
cache.get("B") // null - "B" was evicted due to expiry.
49+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.igorwojda.cache.advancedlru
2+
3+
import java.util.*
4+
5+
// Implementation is using combination of HashMap and LinkedList.
6+
// Time Complexity: O(1)
7+
private object Solution1 {
8+
class AdvancedLRUCache(private val capacity: Int) {
9+
private val map: MutableMap<String, CacheItem> = mutableMapOf()
10+
private val pq: PriorityQueue<CacheItem> = PriorityQueue()
11+
12+
fun put(key: String, value: Int, priority: Int, expiryTime: Long) {
13+
if (map.containsKey(key)) {
14+
this.remove(key)
15+
}
16+
17+
if (map.size == capacity) {
18+
this.clearCache()
19+
}
20+
21+
val item = CacheItem(key, value, priority, expiryTime)
22+
map[key] = item
23+
pq.add(item)
24+
}
25+
26+
fun get(key: String): Int? {
27+
val item = map[key]
28+
29+
return if (item == null || item.expiryTime < getSystemTimeForExpiry()) {
30+
null
31+
} else {
32+
item.lastUsed = System.currentTimeMillis()
33+
item.value
34+
}
35+
}
36+
37+
private fun remove(key: String) {
38+
val item = map[key]
39+
40+
item?.let {
41+
it.expiryTime = 0L // Mark as expired for next eviction
42+
map.remove(key)
43+
}
44+
}
45+
46+
private fun clearCache() {
47+
while (pq.isNotEmpty() && pq.peek().expiryTime < getSystemTimeForExpiry()) {
48+
val item = pq.poll()
49+
50+
if (map.containsKey(item.key) && map[item.key] == item) {
51+
map.remove(item.key)
52+
}
53+
}
54+
55+
if (pq.isEmpty()) return
56+
val item = pq.poll()
57+
if (map.containsKey(item.key) && map[item.key] == item) {
58+
map.remove(item.key)
59+
}
60+
}
61+
62+
private data class CacheItem(
63+
val key: String,
64+
var value: Int,
65+
var priority: Int,
66+
var expiryTime: Long,
67+
) : Comparable<CacheItem> {
68+
var lastUsed: Long = System.currentTimeMillis()
69+
70+
override fun compareTo(other: CacheItem): Int {
71+
return when {
72+
this.expiryTime != other.expiryTime -> this.expiryTime.compareTo(other.expiryTime)
73+
this.priority != other.priority -> this.priority.compareTo(other.priority)
74+
else -> this.lastUsed.compareTo(other.lastUsed)
75+
}
76+
}
77+
}
78+
79+
// Returns fixed system time in milliseconds
80+
private fun getSystemTimeForExpiry() = 1000
81+
}
82+
}
83+
84+
private object KtLintWillNotComplain

src/test/kotlin/com/igorwojda/cache/lru/Challenge.kt

+19-19
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,35 @@ class LRUCache(private val capacity: Int) {
1818
private class Test {
1919
@Test
2020
fun `lru cache is empty after creation`() {
21-
val lruCache = LRUCache(3)
21+
val cache = LRUCache(3)
2222

23-
lruCache.size shouldBeEqualTo 0
23+
cache.size shouldBeEqualTo 0
2424
}
2525

2626
@Test
2727
fun `oldest value is not removed from cache after capacity is exceeded`() {
28-
val lruCache = LRUCache(2)
28+
val cache = LRUCache(2)
2929

30-
lruCache.put(1, "Person1")
31-
lruCache.put(2, "Person2")
32-
lruCache.put(3, "Person3")
30+
cache.put(1, "Person1")
31+
cache.put(2, "Person2")
32+
cache.put(3, "Person3")
3333

34-
lruCache.size shouldBeEqualTo 2
35-
lruCache.get(1) shouldBeEqualTo null
36-
lruCache.get(2) shouldBeEqualTo "Person2"
37-
lruCache.get(3) shouldBeEqualTo "Person3"
34+
cache.size shouldBeEqualTo 2
35+
cache.get(1) shouldBeEqualTo null
36+
cache.get(2) shouldBeEqualTo "Person2"
37+
cache.get(3) shouldBeEqualTo "Person3"
3838
}
3939

4040
@Test
4141
fun `retrieved element becomes most recently used`() {
42-
val lruCache = LRUCache(2)
43-
lruCache.put(1, "Person1")
44-
lruCache.put(2, "Person2")
45-
lruCache.get(1)
46-
lruCache.put(3, "Person3")
47-
48-
lruCache.get(1) shouldBeEqualTo "Person1"
49-
lruCache.get(2) shouldBeEqualTo null
50-
lruCache.get(3) shouldBeEqualTo "Person3"
42+
val cache = LRUCache(2)
43+
cache.put(1, "Person1")
44+
cache.put(2, "Person2")
45+
cache.get(1)
46+
cache.put(3, "Person3")
47+
48+
cache.get(1) shouldBeEqualTo "Person1"
49+
cache.get(2) shouldBeEqualTo null
50+
cache.get(3) shouldBeEqualTo "Person3"
5151
}
5252
}

src/test/kotlin/com/igorwojda/cache/lru/README.md

+5-12
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,11 @@
22

33
## Instructions
44

5-
Design a data structure that follows the constraints of a
6-
[Least Recently Used (LRU) cache](https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU).
5+
Design a data structure that enables the storage and retrieval of items via a key, subject to a specified capacity
6+
limit. In cases where the addition of new items exceeds this capacity, ensure that space is made available through
7+
removing the item that was least recently accessed or used.
78

8-
Implement the `LRUCache` class:
9-
10-
`LRUCache` (int capacity) Initialize the LRU cache with positive size `capacity` and two methods:
11-
- `get(key: Int)` - return the value of the key if the key exists, otherwise return `null`.
12-
- `put(key: Int, value: String)` - update the value of the key if the key exists, otherwise, add the key-value pair
13-
to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key.
14-
- `size` - currently used cache.
15-
16-
The overall run time complexity of each methods should be `O(1)`.
9+
The overall run time complexity of each method should be `O(1)`.
1710

1811
[Challenge](Challenge.kt) | [Solution](Solution.kt)
1912

@@ -31,7 +24,7 @@ lruCache.put(2, 20)
3124
lruCache.put(3, 30)
3225

3326
lruCache.size shouldBeEqualTo 2
34-
lruCache.get(1) shouldBeEqualTo null
27+
lruCache.get(1) shouldBeEqualTo null // value removed due to capacity limit
3528
lruCache.get(2) shouldBeEqualTo 20
3629
lruCache.get(3) shouldBeEqualTo 30
3730
```

0 commit comments

Comments
 (0)