A cache keeps a few recent results close at hand so they are quick to read again. It can only hold a limited number of entries, so when it is full it must throw one away. An LRU (Least Recently Used) cache throws away the entry that was used longest ago. Build a class LRUCache that stores integer keys with integer values.
The class keeps: capacity: usize, a HashMap<i32, i32> called cache (key to value) and a Vec<i32> called order (the keys, least recently used first).
Create it: LRUCache::new(capacity) makes an empty cache that holds at most capacity entries.
Methods and rules:
fn get(&mut self, key: i32) -> i32returns the value stored forkey. A hit counts as a use: the key becomes the most recently used. If the key is not in the cache (never stored, or already evicted), return-1and change nothing.fn put(&mut self, key: i32, value: i32)stores the value forkey, and the key becomes the most recently used.- If the key is already in the cache, replace its value. Nothing is evicted.
- If the key is new and the cache already holds
capacityentries, first evict the least recently used entry (remove it from the map and fromorder), then store the new one.
- Keep
ordersorted by use: the least recently used key at the front, the most recently used at the end. Every use moves the key to the end.
Tools for this language:
- Rust has no constructors:
fn new(capacity: i32) -> LRUCachereturns a new struct (convert withcapacity as usize). The methods go in animpl LRUCacheblock and take&mut self. self.cache.contains_key(&key)tells whether a key is stored;self.cache.insert(key, value)stores or replaces a value;self.cache.remove(&key)deletes an entry;self.cache.len()counts the entries.self.order.retain(|&x| x != key)removes an item by value,self.order.push(key)adds at the end, andself.order[0]withself.order.remove(0)reads and removes the first item.
This version walks over the list of keys for each operation, which is fine for learning. Interview solutions reach O(1) with a hash map plus a doubly linked list.
You start with the code that tests your class (write LRUCache above it):
fn mainFunction() {
let mut c = LRUCache::new(2);
c.put(1, 10);
c.put(2, 20);
println!("Get 1: {}", c.get(1));
c.put(3, 30);
println!("Get 2: {}", c.get(2));
println!("Get 3: {}", c.get(3));
println!("Miss 9: {}", c.get(9));
c.put(1, 11);
println!("Update 1: {}", c.get(1));
c.put(4, 40);
println!("Evicted 3: {}", c.get(3));
println!("Kept 1: {}", c.get(1));
println!("Kept 4: {}", c.get(4));
let mut s = LRUCache::new(1);
s.put(1, 100);
s.put(2, 200);
println!("Small 1: {}", s.get(1));
println!("Small 2: {}", s.get(2));
}
Expected output:
Get 1: 10
Get 2: -1
Get 3: 30
Miss 9: -1
Update 1: 11
Evicted 3: -1
Kept 1: 11
Kept 4: 40
Small 1: -1
Small 2: 200
Check it against the rules: Get 2 is -1 because key 2 was the least recently used when key 3 arrived (key 1 had just been read). Updating key 1 to 11 does not evict anything. With capacity 1, the second put evicts the first key.
Two structures work together: the map gives a fast lookup of the value, and
order remembers which key was used when. Both must always hold the same keys.
self.order.retain(|&x| x != key) then self.order.push(key) moves a key to the end. self.order[0] is the least recently used key.
struct LRUCache {
capacity: usize,
cache: HashMap<i32, i32>,
order: Vec<i32>,
}
impl LRUCache {
fn new(capacity: i32) -> LRUCache {
LRUCache {
capacity: capacity as usize,
cache: HashMap::new(),
order: Vec::new(),
}
}
fn get(&mut self, key: i32) -> i32 {
if !self.cache.contains_key(&key) {
return -1;
}
self.order.retain(|&x| x != key);
self.order.push(key);
*self.cache.get(&key).unwrap()
}
fn put(&mut self, key: i32, value: i32) {
if self.cache.contains_key(&key) {
self.order.retain(|&x| x != key);
} else if self.cache.len() >= self.capacity {
let evict = self.order[0];
self.order.remove(0);
self.cache.remove(&evict);
}
self.cache.insert(key, value);
self.order.push(key);
}
}
Free plan: 30 tutor messages. Premium is unlimited.
