The Algorithms logo
The Algorithms
AboutDonate

Least Frequently Used (LFU) Cache

using System;
using System.Collections.Generic;

namespace DataStructures.Cache
{
    /// <summary>
    /// Least Frequently Used (LFU) cache implementation.
    /// </summary>
    /// <typeparam name="TKey">The type of the key (must be not null).</typeparam>
    /// <typeparam name="TValue">The type of the value.</typeparam>
    /// <remarks>
    /// Cache keeps up to <c>capacity</c> items. When new item is added and cache is full,
    /// one of the least frequently used item is removed (e.g. it keeps N items that were the most
    /// frequently requested using <c>Get()</c> or <c>Put()</c> methods).
    /// When there are multiple items with the same frequency, the least recently used item is removed.
    ///
    /// Cache is built on top of two data structures:
    /// - <c>Dictionary</c>. Allows items to be looked up by key in O(1) time. Another dictionary
    ///   is used to store the frequency of each key.
    /// - <c>LinkedList</c> - Allows items with the same frequency to be ordered by the last
    ///   usage in O(1) time.
    ///
    /// Useful links:
    /// https://en.wikipedia.org/wiki/Cache_replacement_policies
    /// https://www.enjoyalgorithms.com/blog/least-frequently-used-cache
    /// https://www.educative.io/answers/what-is-least-frequently-used-cache-replace-policy
    /// https://leetcode.com/problems/lfu-cache/ .
    /// </remarks>
    public class LfuCache<TKey, TValue> where TKey : notnull
    {
        private class CachedItem
        {
            public TKey Key { get; set; } = default!;

            public TValue? Value { get; set; }

            public int Frequency { get; set; }
        }

        private const int DefaultCapacity = 100;

        private readonly int capacity;

        // Note that <c>Dictionary</c> stores <c>LinkedListNode</c> as it allows
        // removing the node from the <c>LinkedList</c> in O(1) time.
        private readonly Dictionary<TKey, LinkedListNode<CachedItem>> cache = new();

        // Map frequency (number of times the item was requested or updated)
        // to the LRU linked list.
        private readonly Dictionary<int, LinkedList<CachedItem>> frequencies = new();

        // Track the minimum frequency with non-empty linked list in <c>frequencies</c>.
        // When the last item with the minFrequency is promoted (after being requested or updated),
        // the <c>minFrequency</c> is increased.
        // When a new item is added, the <c>minFrequency</c> is set to 1.
        private int minFrequency = -1;

        /// <summary>
        /// Initializes a new instance of the <see cref="LfuCache{TKey, TValue}"/> class.
        /// </summary>
        /// <param name="capacity">The max number of items the cache can store.</param>
        public LfuCache(int capacity = DefaultCapacity)
        {
            this.capacity = capacity;
        }

        public bool Contains(TKey key) => cache.ContainsKey(key);

        /// <summary>
        /// Gets the cached item by key.
        /// </summary>
        /// <param name="key">The key of cached item.</param>
        /// <returns>The cached item or <c>default</c> if item is not found.</returns>
        /// <remarks> Time complexity: O(1). </remarks>
        public TValue? Get(TKey key)
        {
            if (!cache.ContainsKey(key))
            {
                return default;
            }

            var node = cache[key];
            UpdateFrequency(node, isNew: false);
            return node.Value.Value;
        }

        /// <summary>
        /// Adds or updates the value in the cache.
        /// </summary>
        /// <param name="key">The key of item to cache.</param>
        /// <param name="value">The value to cache.</param>
        /// <remarks>
        /// Time complexity: O(1).
        /// If the value is already cached, it is updated and the item is moved
        /// to the end of the LRU list.
        /// If the cache is full, one of the least frequently used items is removed.
        /// </remarks>
        public void Put(TKey key, TValue value)
        {
            if (cache.ContainsKey(key))
            {
                var existingNode = cache[key];
                existingNode.Value.Value = value;
                UpdateFrequency(existingNode, isNew: false);
                return;
            }

            if (cache.Count >= capacity)
            {
                EvictOneItem();
            }

            var item = new CachedItem { Key = key, Value = value };
            var newNode = new LinkedListNode<CachedItem>(item);
            UpdateFrequency(newNode, isNew: true);
            cache.Add(key, newNode);
        }

        private void UpdateFrequency(LinkedListNode<CachedItem> node, bool isNew)
        {
            var item = node.Value;

            if (isNew)
            {
                item.Frequency = 1;
                minFrequency = 1;
            }
            else
            {
                // Remove the existing node from the LRU list with its previous frequency.
                var lruList = frequencies[item.Frequency];
                lruList.Remove(node);
                if (lruList.Count == 0 && minFrequency == item.Frequency)
                {
                    minFrequency++;
                }

                item.Frequency++;
            }

            // Insert item to the end of the LRU list that corresponds to its new frequency.
            if (!frequencies.ContainsKey(item.Frequency))
            {
                frequencies[item.Frequency] = new LinkedList<CachedItem>();
            }

            frequencies[item.Frequency].AddLast(node);
        }

        private void EvictOneItem()
        {
            var lruList = frequencies[minFrequency];
            var itemToRemove = lruList.First!.Value;
            lruList.RemoveFirst();
            cache.Remove(itemToRemove.Key);
        }
    }
}