I have implemented an LRU cache, the question is taken from:leetcode: 146. LRU Cache
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and put.
get(key)- Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value)- Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
Tests:
[TestClass]public class LruCacheUnitTest{ [TestMethod] public void LruCacheTest() { LRUCache cache = new LRUCache(2 /* capacity */ ); cache.put(1, 1); cache.put(2, 2); Assert.AreEqual(1, cache.get(1)); // returns 1 cache.put(3, 3); // evicts key 2 Assert.AreEqual(-1, cache.get(2)); // returns -1 (not found) cache.put(4, 4); // evicts key 1 Assert.AreEqual(-1, cache.get(1)); // returns -1 (not found) Assert.AreEqual(3, cache.get(3)); // returns 3 Assert.AreEqual(4, cache.get(4)); // returns 4 }}Implementation:
public class LRUCache{ private int _numOfCells; private Dictionary<int, int> _cache; private List<KeyValuePair<int, int>> _orderList; public LRUCache(int numberOfCacheCells) { this._numOfCells = numberOfCacheCells; _cache = new Dictionary<int, int>(_numOfCells); _orderList = new List<KeyValuePair<int, int>>(_numOfCells); } public void put(int key, int value) { if (_cache.Count == _numOfCells) // the cache is full we need to remove 1 { var toRemove = _orderList[0]; _cache.Remove(toRemove.Key); _orderList.Remove(toRemove); } _orderList.Add(new KeyValuePair<int, int>(key, value)); _cache[key] = value; } public int get(int key) { if (!_cache.ContainsKey(key)) { return -1; } //put the key and value to the back of the ordered list var tempCacheCell = _orderList.FirstOrDefault(x=>x.Key == key); _orderList.Remove(tempCacheCell); _orderList.Add(tempCacheCell); return _cache[key]; }}- \$\begingroup\$Is this really an interview-question? What did they say? Did you pass?\$\endgroup\$t3chb0t– t3chb0t2017-10-13 14:15:15 +00:00CommentedOct 13, 2017 at 14:15
- \$\begingroup\$This is an interview question. Not my interview. I am just practicing.\$\endgroup\$Gilad– Gilad2017-10-13 14:22:32 +00:00CommentedOct 13, 2017 at 14:22
- 3\$\begingroup\$As noted in answers: your solution is not O(1). But you are on the right track. Hint: instead of an array-backed list of pairs, use a doubly-linked list of pairs, and instead of a dictionary of key to value, make a dictionary of key to doubly-linked-list-node. Now do you see how to do it in O(1)?\$\endgroup\$Eric Lippert– Eric Lippert2017-10-13 14:34:17 +00:00CommentedOct 13, 2017 at 14:34
- 4\$\begingroup\$Next challenge: Can you write an efficientimmutable LRU key-value store? That is,
Putreturns anew LRUCache, and the old one stays the same. SimilarlyGetreturns a tuple ofint, LRUCache. What is the maximum asymptotic order efficiency you can obtain using only immutable data structures?\$\endgroup\$Eric Lippert– Eric Lippert2017-10-13 14:37:31 +00:00CommentedOct 13, 2017 at 14:37 - \$\begingroup\$It's unfortunate that the question as written returns -1 from the
getmethod, which prevents intentionally storing -1 in the cache. Instead, it should return anint?.\$\endgroup\$Matthew FitzGerald-Chamberlain– Matthew FitzGerald-Chamberlain2017-10-13 16:50:31 +00:00CommentedOct 13, 2017 at 16:50
3 Answers3
The indentation is inconsistent: the test class has its{} indented the same as the class declaration, but the main class has them indented one level more.
It's good that you've included a unit test, but it's only testing half of the functionality. What about the getter?
put andget as method names don't follow C# conventions, which would bePut andGet -- although it would be even more idiomatic here to make them anindexer. The original task is phrased in terms which are as language-agnostic as possible, but in an interview you should aim to show language knowledge where you have it as well as general knowledge and skill.
Is there any reason for hard-codingint as the type of the key and value rather than making them generic?
private List<KeyValuePair<int, int>> _orderList;
Two questions: firstly, whyKeyValuePair<int, int>? I don't see anything which uses the value. Secondly, why includeList in the name? The type says that already.
_orderList.Remove(toRemove);
var tempCacheCell = _orderList.FirstOrDefault(x=>x.Key == key); _orderList.Remove(tempCacheCell); _orderList.Add(tempCacheCell);
What's the asymptotic complexity of adding or fetching an element? It can certainly be improved. Hint: one approach would be to manually implement a different type of list. Alternative hint: there's some quite powerful collections in .Net Framework (I haven't checked which of them made it into .Net Standard).
- \$\begingroup\$Great feedback thanks. I understand what you mean about storing the same object for linked list and dictionary and creating my own linked list. But what do you mean about collections. There is nothing like linkedlist with dictionary in c#. The closest is orderdictionary. But it doesn't support the functionaltly needed for cache.\$\endgroup\$Gilad– Gilad2017-10-13 10:14:35 +00:00CommentedOct 13, 2017 at 10:14
- \$\begingroup\$I think OrderedDictionary could be wrapped to provide an LRU cache. There's also the cheating option of writing an LRU cache around MemoryCache.\$\endgroup\$Peter Taylor– Peter Taylor2017-10-13 10:23:56 +00:00CommentedOct 13, 2017 at 10:23
- \$\begingroup\$The indentation is inconsistent: the test class has its {} indented the same as the class declaration, but the main class has them indented one level more. well, this is a copy/paste error, I don't think it's intentionally indented... so I've fixed it... as I always do :-]\$\endgroup\$t3chb0t– t3chb0t2017-10-13 14:13:05 +00:00CommentedOct 13, 2017 at 14:13
- \$\begingroup\$Can a
Dictionary<int, (int data, int lastUsed)>be used here?\$\endgroup\$the default.– the default.2020-04-11 01:18:04 +00:00CommentedApr 11, 2020 at 1:18
This is a simple implementation but its performance wouldn't scale well for largenumberOfCacheCells values.
In particular,_orderList.Remove(toRemove); looks like it's O(n) rather than O(1).
TheDictionary<int, int> _cache operations are probably O(1) because of hashing; it's theList<KeyValuePair<int, int>> _orderList operations that are problematic.
There are never "holes" in the LRU list because you only ever remove the oldest item (the class doesn't support removing arbitrary items). So a "circular list buffer" or double-ended queue is possibly the container that you want to use instead ofList. I don't think there is such a container built-in to .NET, though you could implement one yourself. An adequate alternative isLinkedList, adequate because its operations are O(1).
I wouldn't normally fuss, except that "cache" sounds like it might be performance-sensitive; and, yes, reading the question you linked to, it does say,
Follow up:
Could you do both operations inO(1) time complexity?
Also yourget implementation could be slightly faster using_cache.TryGetValue so that it only has to access_cache once.
List is internally backed by an array, so item-removal won't be efficient. UseLinkedList instead which is a doubly linked list and allows efficient removal.
[TestClass]public class LruCacheUnitTest{ [TestMethod] public void LruCacheTest() { var cache = new LruCache<int, string>(2); cache.Put(1, "One"); cache.Put(2, "Two"); Assert.AreEqual("One", cache.Get(1)); cache.Put(3, "Three"); Assert.IsNull(cache.Get(2)); cache.Put(4, "Four"); Assert.IsNull(cache.Get(1)); Assert.AreEqual("Three", cache.Get(3)); Assert.AreEqual("Four", cache.Get(4)); }}public class LruCache<TKey, TValue>{ private int capacity; private Dictionary<TKey, TValue> valueCache; private Dictionary<TKey, LinkedListNode<TKey>> nodeCache; private LinkedList<TKey> orderList; public LruCache(int capacity) { this.capacity = capacity; this.valueCache = new Dictionary<TKey, TValue>(capacity); this.nodeCache = new Dictionary<TKey, LinkedListNode<TKey>>(capacity); this.orderList = new LinkedList<TKey>(); } public void Put(TKey key, TValue value) { if (this.valueCache.ContainsKey(key)) // Key already exists. { this.Promote(key); this.valueCache[key] = value; return; } if (this.valueCache.Count == capacity) // Cache full. { this.RemoveLast(); } this.AddFirst(key, value); } public TValue Get(TKey key) { if (!this.valueCache.ContainsKey(key)) { return default; } this.Promote(key); return this.valueCache[key]; } private void AddFirst(TKey key, TValue value) { var node = new LinkedListNode<TKey>(key); this.valueCache[key] = value; this.nodeCache[key] = node; this.orderList.AddFirst(node); } private void Promote(TKey key) { LinkedListNode<TKey> node = this.nodeCache[key]; this.orderList.Remove(node); this.orderList.AddFirst(node); } private void RemoveLast() { LinkedListNode<TKey> lastNode = this.orderList.Last; this.valueCache.Remove(lastNode.Value); this.nodeCache.Remove(lastNode.Value); this.orderList.RemoveLast(); }}You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.

