Commencez par créer des roues pour bien comprendre le mécanisme de cache LRU

Leetcode146. Mécanisme de mise en cache LRU

Faire des roues

Dans une interview, les intervieweurs s'attendent généralement à ce que les lecteurs mettent en œuvre eux-mêmes une simple liste à double chaînage, au lieu d'utiliser la structure de données encapsulée de la langue. Par conséquent, il est nécessaire de fabriquer des roues.

L'algorithme d'élimination du cache LRU est une stratégie courante. Le nom complet de LRU est le moins récemment utilisé, ce qui se traduit par le moins récemment utilisé.

Leikou Question 146 "LRU Caching Mechanism" est à nous de concevoir une telle structure de données:

Recevoir un premier capacityparamètre comme capacité maximale du cache, puis implémenter l'API deux, est une put(key, val)méthode de paires clé-valeur stockée, l'autre get(key)méthode d'acquisition keydu correspondant val, s'il keyn'est pas présent ou -1.

La signature de la fonction est la suivante:

class LRUCache {
    
    
    public LRUCache(int capacity) {
    
    }
    public int get(int key) {
    
    }
    public void put(int key, int value) {
    
    }
}

Remarque: getet les putméthodes doivent à la O(1)fois être complexes dans le temps, donnons un exemple concret pour voir comment fonctionne l'algorithme LRU.

LRUCache cache = new LRUCache(2);  // 缓存容量为 2
// 可以把 cache 理解成一个队列
// 假设左边是队头,右边是队尾
// 那么最近使用的排在队头,最久未使用的排在队尾
// 圆括号表示键值对 (key, val)

cache.put(1, 1);  // cache = [(1, 1)]

cache.put(2, 2);  // 新来的添加到队头(左边),此时 cache = [(2, 2), (1, 1)] 

cache.get(1);     // 访问一次 key=1,返回 1。
// 因为(1,1)被访问,被提到队头,此时 cache = [(1, 1), (2, 2)] 

cache.put(3, 3);  // 新添加一个元素
// 但是此时已经满了,需要将队尾(最久未使用)元素删除,然后将新元素添加到队头
// 此时 cache = [(3, 3), (1, 1)] 

cache.get(2);     // 返回 -1 (未找到)

cache.put(1, 4);  // key=1 已存在,把value覆盖为 4,同时将(1,4)提到队头
// 此时cache = [(1, 4), (3, 3)] 

L'analyse de l'opération ci-dessus, de la fabrication putet de la getcomplexité temporelle de la méthode est O (1), nous pouvons conclure que cacheles conditions nécessaires de cette structure de données:

1, il est évident que cachel'élément temporel doit faire la distinction entre les données récemment utilisées et celles récemment utilisées, lorsque la capacité totale de cet élément doit être supprimée pour faire de la place pour les moins récemment utilisées.

2, nous voulons en cachetrouver rapidement un dans keyexiste déjà et a été correspondant val;

3, chaque visite cacheest un keybesoin d'utiliser cet élément devient récemment, cette opération peut être divisée en trois étapes: ① trouver rapidement cet élément (c'est-à-dire au-dessus de 2); ② les éléments qui ont été supprimés; ③ l'élément est réajouté à le chef de file. Cela permet cachede prendre en charge la suppression rapide et l'insertion d'éléments dans n'importe quelle position.

Alors, quelle structure de données remplit les conditions ci-dessus en même temps? La recherche de table de hachage est rapide, mais les données n'ont pas d'ordre fixe; la liste liée a un ordre, l'insertion et la suppression sont rapides, mais la recherche est lente. Alors combinez-le pour former une nouvelle structure de données: une liste chaînée de hachage LinkedHashMap.

La structure de données de base de l'algorithme de cache LRU est une combinaison de liste chaînée de hachage, de liste doublement liée et de table de hachage. Cette structure de données ressemble à ceci:

Deux problèmes peuvent survenir à la lecture de ceci:

  1. Pourquoi une liste à double lien, une liste à lien unique?
  2. La table de hachage a été enregistrée key, pourquoi la liste devrait-elle exister keyet valquoi?

Ces deux questions ne sont pas faciles à expliquer à partir de rien, puis regardez en bas pour trouver les réponses dans le code.

Conception d'architecture :

Notre objectif ultime est d'implémenter LRUCacheune telle structure de données. Avant cela, dessinons d'abord le diagramme d'architecture:

Nous devons mettre en œuvre l' 底层、抽象层、实现层:.architecture à

Notre couche inférieure a besoin de trois catégories: NodeDoubleListHashMap

La structure générale est la suivante:

Code**:

class Node{
    
    
  	public int key, val;
  	public Node next, prev;
  	public Node(int k, int v){
    
    
      	this.key = k;
      	this.val = v;
    }
}

Ensuite, comptez sur notre Nodetype pour construire une liste doublement chaînée, plusieurs algorithmes LRU doivent réaliser une API (rien de plus qu'un certain nombre d'ajouts et de suppressions à la méthode):

class DoubleList {
    
      
    // 头尾虚节点
    private Node head, tail;  
    // 链表元素数
    private int size;

    public DoubleList() {
    
    
        // 初始化双向链表的数据
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }

    // 在链表尾部添加节点 x,时间 O(1)
    public void addLast(Node x) {
    
    
        x.prev = tail.prev;
        x.next = tail;
        tail.prev.next = x;
        tail.prev = x;
        size++;
    }

    // 删除链表中的 x 节点(x 一定存在)
    // 由于是双链表且给的是目标 Node 节点,时间 O(1)
    public void remove(Node node) {
    
    
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size--;
    }

    // 删除链表中第一个节点,并返回该节点,时间 O(1)
    public Node removeFirst() {
    
    
        if (head.next == tail)
            return null;
        Node first = head.next;
        remove(first);
        return first;
    }

    // 返回链表长度,时间 O(1)
    public int size() {
    
     return size; }
}

À ce stade, nous pouvons répondre à la question "Pourquoi devons-nous utiliser une liste à double chaînage" tout à l'heure, car nous devons supprimer. La suppression d'un nœud nécessite non seulement le pointeur du nœud lui-même, mais également le pointeur de son nœud prédécesseur. La liste doublement liée peut prendre en charge la recherche directe du prédécesseur et la complexité temporelle de l'opération est O (1).

Parmi eux: addLast(Node x)、remove(Node node)、removeFirst()le schéma de principe des trois méthodes est le suivant:

Notez que l'API de liste à double lien que nous avons implémentée ne peut être insérée qu'à partir de la queue, ce qui signifie que les données sur la queue sont les plus récemment utilisées, et les données sur la tête sont les plus anciennes.

Avec la réalisation de la liste doublement chaînée, il suffit de la combiner avec la table de hachage dans l'algorithme LRU, et d'abord construire le framework de code:

class LRUCache {
    
    
    // key -> Node(key, val)
    private HashMap<Integer, Node> map;
    // Node(k1, v1) <-> Node(k2, v2)...
    private DoubleList cache;
    // 最大容量
    private int cap;

    public LRUCache(int capacity) {
    
    
        this.cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }
  	
  	/* 将某个 key 提升为最近使用的 */
    private void makeRecently(int key) {
    
    
        Node x = map.get(key);
        // 先从链表中删除这个节点
        cache.remove(x);
        // 重新插到队尾
        cache.addLast(x);
    }

    /* 添加最近使用的元素 */
    private void addRecently(int key, int val) {
    
    
        Node x = new Node(key, val);
        // 链表尾部就是最近使用的元素
        cache.addLast(x);
        // 别忘了在 map 中添加 key 的映射
        map.put(key, x);
    }

    /* 删除某一个 key */
    private void deleteKey(int key) {
    
    
        Node x = map.get(key);
        // 从链表中删除
        cache.remove(x);
        // 从 map 中删除
        map.remove(key);
    }

    /* 删除最久未使用的元素 */
    private void removeLeastRecently() {
    
    
        // 链表头部的第一个元素就是最久未使用的
        Node deletedNode = cache.removeFirst();
        // 同时别忘了从 map 中删除它的 key
        int deletedKey = deletedNode.key;
        map.remove(deletedKey);
    }
}

Ici, avant de pouvoir répondre aux questions du quiz, "Pourquoi devrait stocker à la fois la clé et la valeur dans la liste, plutôt que de simplement stocker la valeur", faites attention à la removeLeastRecentlyfonction, nous devons deletedNodeobtenir deletedKey.

Autrement dit, lorsque la mémoire tampon est pleine, nous devons non seulement supprimer le dernier Nodenœud, mais également mapmapper vers le nœud keysupprimé, et cela keyne peut être rendu Nodedisponible. Si la Nodestructure est uniquement stockée val, nous ne pouvons pas savoir de keyquoi il s'agit, vous ne pouvez pas supprimer maples clés, ce qui entraîne des erreurs.

La méthode ci-dessus est un package d'opération simple, pour éviter d'appeler ces fonctions manipuler directement les cachelistes liées et maples tables de hachage, maintenant je commence à réaliser la getméthode de l' algorithme LRU :

public int get(int key) {
    
    
    if (!map.containsKey(key)) {
    
    
        return -1;
    }
    // 将该数据提升为最近使用的
    makeRecently(key);
    return map.get(key).val;
}

put La méthode est un peu plus compliquée, dessinons une image pour comprendre sa logique:

Nous pouvons donc facilement écrire le putcode de la méthode:

public void put(int key, int val) {
    
    
    if (map.containsKey(key)) {
    
    
        // 删除旧的数据
        deleteKey(key);
        // 新插入的数据为最近使用的数据
        addRecently(key, val);
        return;
    }
    if (cap == cache.size()) {
    
    
        // 删除最久未使用的元素
        removeLeastRecently();
    }
    // 添加为最近使用的元素
    addRecently(key, val);
}

À ce stade, vous devriez avoir pleinement saisi le principe et l'implémentation de l'algorithme LRU.

Résolution de problème

Enfin, nous utilisons les types intégrés de Java LinkedHashMappour implémenter l'algorithme LRU, et exactement la même logique avant:

class LRUCache {
    
    

    int capacity;
    LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();

    public LRUCache(int capacity) {
    
    
        this.capacity = capacity;
    }
    
    public int get(int key) {
    
    
        if(!cache.containsKey(key)){
    
    
            return -1;
        }
        makeRecently(key);
        return cache.get(key);
    }
    
    public void put(int key, int value) {
    
    
        if(cache.containsKey(key)){
    
    
            cache.put(key, value);  // 修改 key 的值
            makeRecently(key);
            return;
        }
        if(cache.size() >= this.capacity){
    
    
          	// 链表头部就是最久未使用的 key
            int oldestKey = cache.keySet().iterator().next();
            cache.remove(oldestKey);
        }
      	// 将新的 key 添加链表尾部
        cache.put(key, value);
    }

    private void makeRecently(int key){
    
    
        int val = cache.get(key);
      	// 删除 key,重新插入到队尾
        cache.remove(key);
        cache.put(key, val);
    }
}
  • Complexité temporelle: à la fois pour mettre et obtenir O(1)
  • Complexité spatiale: O(capacity)parce que les tables de hachage et les listes doublement liées stockent capacity+1jusqu'à des éléments.

Article de référence

Je suppose que tu aimes

Origine blog.csdn.net/weixin_44471490/article/details/109720273
conseillé
Classement