mirror of
https://github.moeyy.xyz/https://github.com/trekhleb/javascript-algorithms.git
synced 2024-12-26 07:01:18 +08:00
Add an example of the LRU Cache based on the Map.
This commit is contained in:
parent
69c3a16f75
commit
4b4d77071c
@ -24,7 +24,7 @@ class LinkedListNode {
|
|||||||
* Implementation of the LRU (Least Recently Used) Cache
|
* Implementation of the LRU (Least Recently Used) Cache
|
||||||
* based on the HashMap and Doubly Linked List data-structures.
|
* based on the HashMap and Doubly Linked List data-structures.
|
||||||
*
|
*
|
||||||
* Current implementation allows to have fast (O(1)) read and write operations.
|
* Current implementation allows to have fast O(1) (in average) read and write operations.
|
||||||
*
|
*
|
||||||
* At any moment in time the LRU Cache holds not more that "capacity" number of items in it.
|
* At any moment in time the LRU Cache holds not more that "capacity" number of items in it.
|
||||||
*/
|
*/
|
||||||
@ -43,7 +43,7 @@ class LRUCache {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cached value by its key.
|
* Returns the cached value by its key.
|
||||||
* Time complexity: O(1).
|
* Time complexity: O(1) in average.
|
||||||
* @param {string} key
|
* @param {string} key
|
||||||
* @returns {any}
|
* @returns {any}
|
||||||
*/
|
*/
|
||||||
@ -56,7 +56,7 @@ class LRUCache {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the value to cache by its key.
|
* Sets the value to cache by its key.
|
||||||
* Time complexity: O(1).
|
* Time complexity: O(1) in average.
|
||||||
* @param {string} key
|
* @param {string} key
|
||||||
* @param {any} val
|
* @param {any} val
|
||||||
*/
|
*/
|
||||||
|
53
src/data-structures/lru-cache/LRUCacheOnMap.js
Normal file
53
src/data-structures/lru-cache/LRUCacheOnMap.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/* eslint-disable no-restricted-syntax, no-unreachable-loop */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the LRU (Least Recently Used) Cache
|
||||||
|
* based on the (ordered) Map data-structure.
|
||||||
|
*
|
||||||
|
* Current implementation allows to have fast O(1) (in average) read and write operations.
|
||||||
|
*
|
||||||
|
* At any moment in time the LRU Cache holds not more that "capacity" number of items in it.
|
||||||
|
*/
|
||||||
|
class LRUCacheOnMap {
|
||||||
|
/**
|
||||||
|
* Creates a cache instance of a specific capacity.
|
||||||
|
* @param {number} capacity
|
||||||
|
*/
|
||||||
|
constructor(capacity) {
|
||||||
|
this.capacity = capacity; // How many items to store in cache at max.
|
||||||
|
this.items = new Map(); // The ordered hash map of all cached items.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cached value by its key.
|
||||||
|
* Time complexity: O(1) in average.
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
get(key) {
|
||||||
|
if (!this.items.has(key)) return undefined;
|
||||||
|
const val = this.items.get(key);
|
||||||
|
this.items.delete(key);
|
||||||
|
this.items.set(key, val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value to cache by its key.
|
||||||
|
* Time complexity: O(1).
|
||||||
|
* @param {string} key
|
||||||
|
* @param {any} val
|
||||||
|
*/
|
||||||
|
set(key, val) {
|
||||||
|
this.items.delete(key);
|
||||||
|
this.items.set(key, val);
|
||||||
|
if (this.items.size > this.capacity) {
|
||||||
|
for (const headKey of this.items.keys()) {
|
||||||
|
this.items.delete(headKey);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LRUCacheOnMap;
|
@ -16,6 +16,8 @@ The functions `get()` and `set()` must each run in `O(1)` average time complexit
|
|||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
|
### Version 1: Doubly Linked List + Hash Map
|
||||||
|
|
||||||
See the `LRUCache` implementation example in [LRUCache.js](./LRUCache.js). The solution uses a `HashMap` for fast `O(1)` (in average) cache items access, and a `DoublyLinkedList` for fast `O(1)` (in average) cache items promotions and eviction (to keep the maximum allowed cache capacity).
|
See the `LRUCache` implementation example in [LRUCache.js](./LRUCache.js). The solution uses a `HashMap` for fast `O(1)` (in average) cache items access, and a `DoublyLinkedList` for fast `O(1)` (in average) cache items promotions and eviction (to keep the maximum allowed cache capacity).
|
||||||
|
|
||||||
![Linked List](./images/lru-cache.jpg)
|
![Linked List](./images/lru-cache.jpg)
|
||||||
@ -24,6 +26,16 @@ See the `LRUCache` implementation example in [LRUCache.js](./LRUCache.js). The s
|
|||||||
|
|
||||||
You may also find more test-case examples of how the LRU Cache works in [LRUCache.test.js](./__test__/LRUCache.test.js) file.
|
You may also find more test-case examples of how the LRU Cache works in [LRUCache.test.js](./__test__/LRUCache.test.js) file.
|
||||||
|
|
||||||
|
### Version 2: Ordered Map
|
||||||
|
|
||||||
|
The first implementation that uses doubly linked list is good for learning purposes and for better understanding of how the average `O(1)` time complexity is achievable while doing `set()` and `get()`.
|
||||||
|
|
||||||
|
However, the simpler approach might be to use a JavaScript [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object. The `Map` object holds key-value pairs and **remembers the original insertion order** of the keys. We can use this fact in order to keep the recently-used items in the "end" of the map by removing and re-adding items. The item at the beginning of the `Map` is the first one to be evicted if cache capacity overflows. The order of the items may checked by using the `IterableIterator` like `map.keys()`.
|
||||||
|
|
||||||
|
See the `LRUCacheOnMap` implementation example in [LRUCacheOnMap.js](./LRUCacheOnMap.js).
|
||||||
|
|
||||||
|
You may also find more test-case examples of how the LRU Cache works in [LRUCacheOnMap.test.js](./__test__/LRUCacheOnMap.test.js) file.
|
||||||
|
|
||||||
## Complexities
|
## Complexities
|
||||||
|
|
||||||
| | Average |
|
| | Average |
|
||||||
|
148
src/data-structures/lru-cache/__test__/LRUCacheOnMap.test.js
Normal file
148
src/data-structures/lru-cache/__test__/LRUCacheOnMap.test.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import LRUCache from '../LRUCacheOnMap';
|
||||||
|
|
||||||
|
describe('LRUCacheOnMap', () => {
|
||||||
|
it('should set and get values to and from the cache', () => {
|
||||||
|
const cache = new LRUCache(100);
|
||||||
|
expect(cache.get('key-1')).toBeUndefined();
|
||||||
|
|
||||||
|
cache.set('key-1', 15);
|
||||||
|
cache.set('key-2', 16);
|
||||||
|
cache.set('key-3', 17);
|
||||||
|
expect(cache.get('key-1')).toBe(15);
|
||||||
|
expect(cache.get('key-2')).toBe(16);
|
||||||
|
expect(cache.get('key-3')).toBe(17);
|
||||||
|
expect(cache.get('key-3')).toBe(17);
|
||||||
|
expect(cache.get('key-2')).toBe(16);
|
||||||
|
expect(cache.get('key-1')).toBe(15);
|
||||||
|
|
||||||
|
cache.set('key-1', 5);
|
||||||
|
cache.set('key-2', 6);
|
||||||
|
cache.set('key-3', 7);
|
||||||
|
expect(cache.get('key-1')).toBe(5);
|
||||||
|
expect(cache.get('key-2')).toBe(6);
|
||||||
|
expect(cache.get('key-3')).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evict least recently used items from cache with cache size of 1', () => {
|
||||||
|
const cache = new LRUCache(1);
|
||||||
|
expect(cache.get('key-1')).toBeUndefined();
|
||||||
|
|
||||||
|
cache.set('key-1', 15);
|
||||||
|
expect(cache.get('key-1')).toBe(15);
|
||||||
|
|
||||||
|
cache.set('key-2', 16);
|
||||||
|
expect(cache.get('key-1')).toBeUndefined();
|
||||||
|
expect(cache.get('key-2')).toBe(16);
|
||||||
|
|
||||||
|
cache.set('key-2', 17);
|
||||||
|
expect(cache.get('key-2')).toBe(17);
|
||||||
|
|
||||||
|
cache.set('key-3', 18);
|
||||||
|
cache.set('key-4', 19);
|
||||||
|
expect(cache.get('key-2')).toBeUndefined();
|
||||||
|
expect(cache.get('key-3')).toBeUndefined();
|
||||||
|
expect(cache.get('key-4')).toBe(19);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evict least recently used items from cache with cache size of 2', () => {
|
||||||
|
const cache = new LRUCache(2);
|
||||||
|
expect(cache.get('key-21')).toBeUndefined();
|
||||||
|
|
||||||
|
cache.set('key-21', 15);
|
||||||
|
expect(cache.get('key-21')).toBe(15);
|
||||||
|
|
||||||
|
cache.set('key-22', 16);
|
||||||
|
expect(cache.get('key-21')).toBe(15);
|
||||||
|
expect(cache.get('key-22')).toBe(16);
|
||||||
|
|
||||||
|
cache.set('key-22', 17);
|
||||||
|
expect(cache.get('key-22')).toBe(17);
|
||||||
|
|
||||||
|
cache.set('key-23', 18);
|
||||||
|
expect(cache.get('key-21')).toBeUndefined();
|
||||||
|
expect(cache.get('key-22')).toBe(17);
|
||||||
|
expect(cache.get('key-23')).toBe(18);
|
||||||
|
|
||||||
|
cache.set('key-24', 19);
|
||||||
|
expect(cache.get('key-21')).toBeUndefined();
|
||||||
|
expect(cache.get('key-22')).toBeUndefined();
|
||||||
|
expect(cache.get('key-23')).toBe(18);
|
||||||
|
expect(cache.get('key-24')).toBe(19);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evict least recently used items from cache with cache size of 3', () => {
|
||||||
|
const cache = new LRUCache(3);
|
||||||
|
|
||||||
|
cache.set('key-1', 1);
|
||||||
|
cache.set('key-2', 2);
|
||||||
|
cache.set('key-3', 3);
|
||||||
|
expect(cache.get('key-1')).toBe(1);
|
||||||
|
expect(cache.get('key-2')).toBe(2);
|
||||||
|
expect(cache.get('key-3')).toBe(3);
|
||||||
|
|
||||||
|
cache.set('key-3', 4);
|
||||||
|
expect(cache.get('key-1')).toBe(1);
|
||||||
|
expect(cache.get('key-2')).toBe(2);
|
||||||
|
expect(cache.get('key-3')).toBe(4);
|
||||||
|
|
||||||
|
cache.set('key-4', 5);
|
||||||
|
expect(cache.get('key-1')).toBeUndefined();
|
||||||
|
expect(cache.get('key-2')).toBe(2);
|
||||||
|
expect(cache.get('key-3')).toBe(4);
|
||||||
|
expect(cache.get('key-4')).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should promote the node while calling set() method', () => {
|
||||||
|
const cache = new LRUCache(2);
|
||||||
|
|
||||||
|
cache.set('2', 1);
|
||||||
|
cache.set('1', 1);
|
||||||
|
cache.set('2', 3);
|
||||||
|
cache.set('4', 1);
|
||||||
|
expect(cache.get('1')).toBeUndefined();
|
||||||
|
expect(cache.get('2')).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should promote the recently accessed item with cache size of 3', () => {
|
||||||
|
const cache = new LRUCache(3);
|
||||||
|
|
||||||
|
cache.set('key-1', 1);
|
||||||
|
cache.set('key-2', 2);
|
||||||
|
cache.set('key-3', 3);
|
||||||
|
expect(cache.get('key-1')).toBe(1);
|
||||||
|
|
||||||
|
cache.set('key-4', 4);
|
||||||
|
expect(cache.get('key-1')).toBe(1);
|
||||||
|
expect(cache.get('key-3')).toBe(3);
|
||||||
|
expect(cache.get('key-4')).toBe(4);
|
||||||
|
expect(cache.get('key-2')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should promote the recently accessed item with cache size of 4', () => {
|
||||||
|
const cache = new LRUCache(4);
|
||||||
|
|
||||||
|
cache.set('key-1', 1);
|
||||||
|
cache.set('key-2', 2);
|
||||||
|
cache.set('key-3', 3);
|
||||||
|
cache.set('key-4', 4);
|
||||||
|
expect(cache.get('key-4')).toBe(4);
|
||||||
|
expect(cache.get('key-3')).toBe(3);
|
||||||
|
expect(cache.get('key-2')).toBe(2);
|
||||||
|
expect(cache.get('key-1')).toBe(1);
|
||||||
|
|
||||||
|
cache.set('key-5', 5);
|
||||||
|
expect(cache.get('key-1')).toBe(1);
|
||||||
|
expect(cache.get('key-2')).toBe(2);
|
||||||
|
expect(cache.get('key-3')).toBe(3);
|
||||||
|
expect(cache.get('key-4')).toBeUndefined();
|
||||||
|
expect(cache.get('key-5')).toBe(5);
|
||||||
|
|
||||||
|
cache.set('key-6', 6);
|
||||||
|
expect(cache.get('key-1')).toBeUndefined();
|
||||||
|
expect(cache.get('key-2')).toBe(2);
|
||||||
|
expect(cache.get('key-3')).toBe(3);
|
||||||
|
expect(cache.get('key-4')).toBeUndefined();
|
||||||
|
expect(cache.get('key-5')).toBe(5);
|
||||||
|
expect(cache.get('key-6')).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user