Análisis de Algoritmos LRU y LFU

Introducción

Usado menos recientemente (LRU), el nombre chino se usa menos recientemente, su principio de diseño se basa en el principio de localidad temporal : si un objeto no se ha usado recientemente, no se usará durante un período de tiempo en el futuro, y viceversa. Específicamente, cuando los recursos de espacio están ocupados, si se agregan nuevos recursos al espacio, los recursos que no se han utilizado recientemente en el espacio se intercambiarán para hacer espacio para los nuevos recursos.

Least Frequently Used (LFU), el nombre chino es el menos usado, su principio de diseño es que si un objeto se usa con menos frecuencia , la probabilidad de que se vuelva a usar también es menor. Por lo tanto, cuando los recursos de espacio están ocupados, si se agregan nuevos recursos al espacio, los recursos que se usan con menos frecuencia en el espacio se intercambiarán para hacer espacio para los nuevos recursos.

Si escucha acerca de estos dos algoritmos por primera vez, los estudiantes pueden asustarse con su nombre, pero después de una comprensión cuidadosa, descubren que LRU solo usa la idea de series de tiempo para la predicción, mientras que LFU usa la idea de probabilidad . para hacer predicciones Solo juicio previo. Este artículo lo llevará a comprender la fuente de estos dos algoritmos y brindará implementaciones específicas de Java.

Antecedentes algorítmicos

Los recursos de espacio de la memoria son muy limitados. Para administrar dichos recursos, el sistema operativo optimizará la asignación de memoria. En términos generales, hay cuatro formas principales de asignar memoria:

  1. distribución continua
  2. Gestión básica de almacenamiento de paginación
  3. Gestión básica de almacenamiento segmentado
  4. Gestión de almacenamiento de páginas segmentadas

Sin embargo, este tipo de método de administración tiene ciertas limitaciones. Cuando se programa un trabajo , todas las páginas deben transferirse a la memoria al mismo tiempo, pero si el espacio de memoria no es suficiente, el trabajo debe esperar a que haya suficiente espacio antes de que pueda realizarse. programado para ejecutarse.

El modo de gestión de paginación de solicitudes puede resolver este problema muy bien. El modo de gestión de paginación de solicitudes admite memoria virtual y tiene la función de reemplazo de página . Al programar trabajos, solo una parte de las páginas se transfieren a la memoria. Parte de la página se llama y la página requerida se carga en la memoria. En general, la gestión de almacenamiento de paginación de solicitudes no se ejecuta de forma independiente, sino un método de gestión de almacenamiento basado en la gestión de paginación.

Memoria virtual ( Enciclopedia de Baidu ): La memoria virtual es una tecnología para la gestión de la memoria del sistema informático. Hace que la aplicación piense que tiene memoria contigua disponible (un espacio de direcciones completo contiguo), cuando en realidad generalmente se divide en múltiples fragmentos de memoria física, y algunos se almacenan temporalmente en un disco externo, cuando es necesario intercambiar datos.

La característica de la gestión de paginación de solicitudes mencionada anteriormente es que tiene la función de reemplazo de página, y existen muchas estrategias para el reemplazo de página. LRU y LFU son dos de ellas, que es el origen de estos dos algoritmos.

Implementación de código LRU

La dificultad de implementar el algoritmo LRU es completar el reemplazo bajo la complejidad temporal de O(1).Este documento adopta la solución de lista doblemente enlazada + tabla hash para resolver este problema.El núcleo de esta solución es:

  1. La clave de cada nodo y el propio nodo se almacenan en la tabla hash en forma de pares clave-valor. Cuando se ha ocupado el espacio y se agrega un nuevo nodo, se elimina el nodo final de la lista enlazada. En este momento , la eliminación de la cola y la inserción de la cabeza son dos. Las operaciones de paso son todas O(1) ;
  2. Al obtener los datos del nodo existente, el valor clave se toma del HashMap en lugar de atravesar la lista enlazada. Dado que el HashMap tiene una pequeña cantidad de datos, la operación de obtención puede considerarse como O(1) , por lo que también se obtiene la página existente. alcanza el estándar O(1).

La implementación específica es la siguiente:

/**
 * LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰
 * LRUCache可以简单修改代码然后cv到LeetCode进行测试 https://leetcode-cn.com/problems/lru-cache-lcci/
 */
public class LRUCache {

    public int capacity;

    public int size;

    static class LRUDoubleLinkedNode {
        String key;
        int val;
        LRUDoubleLinkedNode next;
        LRUDoubleLinkedNode prev;

        LRUDoubleLinkedNode(String key, int val) {
            this.key = key;
            this.val = val;
        }
    }

    public LRUDoubleLinkedNode head;

    public LRUDoubleLinkedNode tail;

    public Map<String, LRUDoubleLinkedNode> cache;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        head = new LRUDoubleLinkedNode(Constant.HEAD_SENTINEL, 0);
        tail = new LRUDoubleLinkedNode(Constant.TAIL_SENTINEL, 0);
        cache = new HashMap<>();
        head.next = tail;
        tail.prev = head;
    }

    /**
     * 插入页面
     */
    public void put(String key, int val) {
        if (capacity == 0) {
            throw new RuntimeException(Constant.CACHE_CAPACITY_ZERO);
        }
        LRUDoubleLinkedNode node = cache.get(key);
        if (node != null) {
            node.val = val;
            moveToHead(node);
        }else {
            LRUDoubleLinkedNode newNode = new LRUDoubleLinkedNode(key, val);
            cache.put(key, newNode);
            addToHead(newNode);
            size++;
            if (size > capacity) {
                String removeKey = removeLast();
                cache.remove(removeKey);
                size--;
            }
        }
    }

    /**
     * 取出页面
     */
    public int get(String key) {
        if (!cache.containsKey(key)) {
            throw new RuntimeException(Constant.PAGE_IS_NOT_EXIST);
        }
        LRUDoubleLinkedNode node = cache.get(key);
        moveToHead(node);
        return node.val;
    }

    /**
     * 移动页面至头部
     */
    private void moveToHead(LRUDoubleLinkedNode node) {
        remove(node);
        addToHead(node);
    }

    /**
     * 头插法添加页面
     */
    private void addToHead(LRUDoubleLinkedNode node) {
        head.next.prev = node;
        node.next = head.next;
        node.prev = head;
        head.next = node;
    }

    /**
     * 删除页面
     */
    private void remove(LRUDoubleLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    /**
     * 删除尾部页面
     */
    private String removeLast() {
        LRUDoubleLinkedNode node = tail.prev;
        remove(node);
        return node.key;
    }

    /**
     * 测试
     * 因为LRU结构较为复杂,单一测试案例完全无法验证算法的正确性,因此可以前往LeetCode界面进行测试
     */
    public static void main(String[] args) {

        LRUCache lruCache = new LRUCache(1);

        System.out.println("---------------原缓存---------------");
        System.out.println(lruCache.cache.toString());
        System.out.println("\n");

        System.out.println("---------------put测试---------------");
        lruCache.put("1", 2);
        System.out.println(lruCache.cache.toString());
        lruCache.put("1",3);
        System.out.println(lruCache.cache.toString());
        lruCache.put("2", 5);
        System.out.println(lruCache.cache.toString());
        System.out.println("\n");

        System.out.println("---------------get测试---------------");
        int page = lruCache.get("2");
        System.out.println(page);
        lruCache.get("1");

    }
}
复制代码

Implementación de código LFU

LFU实现的难点同样在于要在O(1)的时间复杂度下完成置换,但LFU同LRU不同,最近最少使用这一条件使得调出的页面被限定在了空间的尾部,因此查找到这一调出页面的效率是O(1),但LFU需要知道哪一个页面的历史使用次数最少,这个节点是不固定的。为了解决这一问题,本文采取 双HashMap+双向链表 的方案,这一方案的核心在于:

  1. 第一个HashMap的键值对为 key 和双向链表节点,第二个HashMap的键值对为频数和每个频数对应的双向链表(将所有相同频数的节点组成的链表),此外,再维护一个全局最小频数【即操作数】;
  2. 每次插入一个新节点时,如果空间已满,此时查找最小频数对应的链表,并将该链表的尾部节点取出并删除(O(1)操作),同时维护最小频数【查看最小频数对应的链表是否为空】,最后再删除第一个HashMap中对应的节点,最后将新的节点插入;
  3. 每次要获取一个已有节点时,则更新最小频数,并将对应的链表进行调整,由于操作了一次该节点,因此该节点需要移动至【频数+1】对应的链表头部

综合而言,就是在LRU的基础上将链表节点依照频数进行分组,从而解决了节点不固定的问题。具体实现如下:

/**
 * LFU(least frequently used (LFU) page-replacement algorithm),即最不经常使用页置换算法.
 * 定义:
 *      LFU根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
 */
public class LFUCache {

    static class LFUDoubleLinkedNode {
        int val;
        int key;
        int freq;
        LFUDoubleLinkedNode prev;
        LFUDoubleLinkedNode next;

        public LFUDoubleLinkedNode() {}

        public LFUDoubleLinkedNode(int key, int val) {
            this.key = key;
            this.val = val;
            this.freq = 1;
        }
    }

    static class LFUDoubleLinkedList {

        public LFUDoubleLinkedNode head;
        public LFUDoubleLinkedNode tail;

        public LFUDoubleLinkedList() {
            this.head = new LFUDoubleLinkedNode();
            this.tail = new LFUDoubleLinkedNode();
            head.next = tail;
            tail.prev = head;
        }

        /**
         * 插入节点
         */
        private void addToHead(LFUDoubleLinkedNode node) {
            head.next.prev = node;
            node.next = head.next;
            node.prev = head;
            head.next = node;
        }

        /**
         * 删除节点
         */
        private void remove(LFUDoubleLinkedNode node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }

        /**
         * 删除尾部节点
         */
        private LFUDoubleLinkedNode removeLast() {
            LFUDoubleLinkedNode node = tail.prev;
            remove(node);
            return node;
        }

    }

    public int capacity; // 最大容量
    public int size; // 当前容量
    public Map<Integer, LFUDoubleLinkedList> freqMap; // 相同频次链表
    public Map<Integer, LFUDoubleLinkedNode> cache; // 节点存储
    public int minFreq; // 当前最小频次

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.minFreq = 0;
        freqMap = new HashMap<>();
        cache = new HashMap<>();
    }

    /**
     * 获取页面
     */
    public int get(int key) {
        LFUDoubleLinkedNode node = cache.get(key);
        if (node == null) {
            throw new RuntimeException(Constant.PAGE_IS_NOT_EXIST);
        }
        updateFreq(node);
        return node.val;
    }

    /**
     * 插入页面
     */
    public void put(int key, int val) {
        if (capacity == 0) {
            throw new RuntimeException(Constant.CACHE_CAPACITY_ZERO);
        }
        LFUDoubleLinkedNode node = cache.get(key);
        if (node == null) {
            if (size == capacity) {
                LFUDoubleLinkedList minLfuDoubleLinkedList = freqMap.get(minFreq);
                LFUDoubleLinkedNode lfuDoubleLinkedNode = minLfuDoubleLinkedList.removeLast();
                cache.remove(lfuDoubleLinkedNode.key);
                size--;
            }
            node = new LFUDoubleLinkedNode(key, val);
            cache.put(key, node);
            LFUDoubleLinkedList lfuDoubleLinkedList = freqMap.get(node.freq);
            if (lfuDoubleLinkedList == null) {
                lfuDoubleLinkedList = new LFUDoubleLinkedList();
                freqMap.put(node.freq, lfuDoubleLinkedList);
            }
            lfuDoubleLinkedList.addToHead(node);
            size++;
            minFreq = 1;
        }else {
            node.val = val;
            updateFreq(node);
        }
    }

    /**
     * 更新频次
     */
    public void updateFreq(LFUDoubleLinkedNode node) {
        // 删除老频次链表中的节点
        int freq = node.freq;
        LFUDoubleLinkedList lfuDoubleLinkedListOld = freqMap.get(freq);
        lfuDoubleLinkedListOld.remove(node);

        // 更新当前最小频次
        if (freq == minFreq && lfuDoubleLinkedListOld.head.next == lfuDoubleLinkedListOld.tail) {
            minFreq = freq+1;
        }

        // 插入到新频次链表
        node.freq = freq + 1;
        LFUDoubleLinkedList lfuDoubleLinkedListNew = freqMap.get(node.freq);
        if (lfuDoubleLinkedListNew == null) {
            lfuDoubleLinkedListNew = new LFUDoubleLinkedList();
            freqMap.put(node.freq, lfuDoubleLinkedListNew);
        }
        lfuDoubleLinkedListNew.addToHead(node);
    }

    /**
     * 测试,详细测试前往 https://leetcode-cn.com/problems/lfu-cache/submissions/
     */
    public static void main(String[] args) {

        LFUCache lfuCache = new LFUCache(2);
        System.out.println("------------LFU测试------------");
        lfuCache.put(3, 1);
        lfuCache.put(2, 1);
        lfuCache.put(2, 2);
        lfuCache.put(4, 4);
        System.out.println(lfuCache.get(2));

    }
}
复制代码

结语

LRU和LFU仅仅是页面置换算法中的两种,其实现并不重要,重要的是理解 链表+哈希表 这种数据结构组合使用的思想,因为这两个算法说穿了就是数据结构的举一反三。

Supongo que te gusta

Origin juejin.im/post/7082777986231402533
Recomendado
Clasificación