数据结构与算法(Dart)之LFU算法(十)

432 阅读4分钟

LFU(Least Frequently Used)根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

我们定义两个哈希表,第一个 freq_table 以频率 freq 为索引,每个索引存放一个双向链表,这个链表里存放所有使用频率为 freq 的缓存,缓存里存放三个信息,分别为键 key,值 value,以及使用频率 freq。第二个 key_table 以键值 key 为索引,每个索引存放对应缓存在 freq_table 中链表里的内存地址,这样我们就能利用两个哈希表来使得两个操作的时间复杂度均为 O(1)。同时需要记录一个当前缓存最少使用的频率 minFreq,这是为了删除操作服务的。

对于 get(key) 操作,我们能通过索引 key 在 key_table 中找到缓存在 freq_table 中的链表的内存地址,如果不存在直接返回 -1,否则我们能获取到对应缓存的相关信息,这样我们就能知道缓存的键值还有使用频率,直接返回 key 对应的值即可。

但是我们注意到 get 操作后这个缓存的使用频率加一了,所以我们需要更新缓存在哈希表 freq_table 中的位置。已知这个缓存的键 key,值 value,以及使用频率 freq,那么该缓存应该存放到 freq_tablefreq + 1 索引下的链表中。所以我们在当前链表中 O(1) 删除该缓存对应的节点,根据情况更新 minFreq 值,然后将其O(1) 插入到 freq + 1 索引下的链表头完成更新。这其中的操作复杂度均为 O(1)。你可能会疑惑更新的时候为什么是插入到链表头,这其实是为了保证缓存在当前链表中从链表头到链表尾的插入时间是有序的,为下面的删除操作服务。

对于 put(key, value) 操作,我们先通过索引 keykey_table 中查看是否有对应的缓存,如果有的话,其实操作等价于 get(key) 操作,唯一的区别就是我们需要将当前的缓存里的值更新为 value。如果没有的话,相当于是新加入的缓存,如果缓存已经到达容量,需要先删除最近最少使用的缓存,再进行插入。

先考虑插入,由于是新插入的,所以缓存的使用频率一定是 1,所以我们将缓存的信息插入到 freq_table 中 1 索引下的列表头即可,同时更新 key_table[key] 的信息,以及更新 minFreq = 1

那么剩下的就是删除操作了,由于我们实时维护了 minFreq,所以我们能够知道 freq_table 里目前最少使用频率的索引,同时因为我们保证了链表中从链表头到链表尾的插入时间是有序的,所以 freq_table[minFreq] 的链表中链表尾的节点即为使用频率最小且插入时间最早的节点,我们删除它同时根据情况更新 minFreq ,整个时间复杂度均为 O(1)

/*
  实现LFU算法, 需要用到两个哈希表和N个双向链表
  代码示例 (执行用时:35ms)
  节点
*/
import 'dart:core';

class Node {
  String key;
  var value;

  /// 节点使用频率
  int freq;

  /// 前一个节点
  Node? prev;

  /// 后一个节点
  Node? next;

  Node(
      {this.next,
      this.prev,
      required this.key,
      required this.value,
      this.freq = 0});
}

/// 双向链表
class DoublyLinkedList {
  /// 头部节点
  Node? _head;

  /// 尾部节点
  Node? _tail;

  /// 链表长度
  int _size = 0;

  DoublyLinkedList() {
    _head = Node(key: '', value: '', freq: 0);
    _tail = Node(key: '', value: '', freq: 0);
    _head?.next = _tail;
    _tail?.prev = _head;
  }

  /// 添加节点到头部节点后的第一个位置
  /// head->node->...->tail
  void addToHead(Node node) {
    /// node节点右侧
    node.next = _head?.next;
    _head?.next?.prev = node;

    /// node节点左侧
    node.prev = _head;
    _head?.next = node;
    _size++;
    print(
        "---添加节点后, 双向链表长度,size: $_size, node.key: ${node.key}, node.freq: ${node.freq}---");
  }

  /// 移除节点
  remove(Node node) {
    /// 当前节点的左节点的右节点指向当前节点的右节点
    node.prev?.next = node.next;

    /// 当前节点的右节点的左节点指向当前节点的左节点
    node.next?.prev = node.prev;
    _size--;
    print(
        "---移除节点后, 双向链表长度,size: $_size, node.key: ${node.key}, node.freq: ${node.freq}---");
  }

  /// 遍历链表
  travel() {
    Node? cur = _head;
    while (cur != null) {
      print(
          'cur.key: ${cur.key}, cur.value: ${cur.value}, cur.freq: ${cur.freq}');
      cur = cur.next;
    }
  }

  /// 获取头节点后的一个节点
  Node? getHeadNext() {
    return _head?.next;
  }

  /// 获取尾节点的前一个节点
  Node? getTailPrev() {
    return _tail?.prev;
  }

  /// 链表是否为空
  bool isEmpty() {
    return _size == 0;
  }
}

class LFUCache {
  /// key-节点的哈希表
  final Map<String, Node> _keyToNode = <String, Node>{};

  /// 频率-双向链表的哈希表
  final Map<int, DoublyLinkedList> _freqToList = <int, DoublyLinkedList>{};

  /// LFU缓存的容量
  int _capacity = 0;

  /// 最小使用频率, 默认为1
  int _minFreq = 1;

  LFUCache(int capacity) {
    _capacity = capacity;
  }

  /// 读取元素
  get(String key) {
    if (isEmpty()) return null;

    if (!_keyToNode.containsKey(key)) {
      return null;
    }

    var value = _keyToNode[key]!.value;
    _updateNode(key, value);
    return value;
  }

  /// 添加元素
  put(String key, value) {
    if (isEmpty()) return null;

    if (_keyToNode.containsKey(key)) {
      _updateNode(key, value);
    } else {
      _insertNode(key, value);
    }
  }

  /// 更新节点
  _updateNode(String key, value) {
    /// 获取旧节点。旧节点的使用频率+1后, 赋值给新节点的使用频率freq
    Node oldNode = _keyToNode[key]!;
    Node newNode = Node(key: key, value: value, freq: oldNode.freq + 1);

    /// 添加新节点到(key-节点的哈希表)中
    _keyToNode[key] = newNode;

    /// 从(频率-双向链表的哈希表)中读取旧双向链表, 移除双向链表中的旧节点,
    DoublyLinkedList? list = _freqToList[oldNode.freq];
    if (list != null) {
      list.remove(oldNode);

      /// 双向链表为空, 移除(频率-双向链表的哈希表)中key对应的元素, 减少内存占用
      if (list.isEmpty()) {
        _freqToList.remove(oldNode.freq);

        /// 更新最小使频率
        if (_minFreq == oldNode.freq) {
          _minFreq += 1;
        }
      }
    }

    /// 添加新节点到双向链表头部节点的后一个位置=]
    _addToFreqList(newNode);
  }

  /// 添加节点
  _insertNode(String key, value) {
    /// 新建节点, 保存key、value, 使用频率为1
    Node newNode = Node(key: key, value: value, freq: 1);

    /// 添加到key-节点的哈希表中
    _keyToNode[key] = newNode;

    /// 添加到频率-双向链表的哈希表中
    _addToFreqList(newNode);

    /// 如果(key-节点的哈希表)的长度 等于 (缓存总量+1)
    /// 移除(key-节点的哈希表)中的key: list.tail.prev.key的元素
    /// 移除(频率-双向链表的哈希表)中的key: list.tail.prev的元素
    if (_keyToNode.length == _capacity + 1) {
      DoublyLinkedList? list = _freqToList[_minFreq];
      if (list != null && (list.getTailPrev() != null)) {
        _keyToNode.remove(list.getTailPrev()!.key);
        list.remove(list.getTailPrev()!);
      }
    }

    /// 每次添加新节点后, 最小使用频率都是1
    _minFreq = 1;
  }

  /// 添加到频率-双向链表的哈希表
  _addToFreqList(Node node) {
    /// (频率-双向链表的哈希表)中没有当前节点, 就以(key:节点使用频率, value: 新建链表), 添加到(频率-双向链表的哈希表)
    if (!_freqToList.containsKey(node.freq)) {
      _freqToList[node.freq] = DoublyLinkedList();
    }

    /// 从(频率-双向链表的哈希表)中读取到双向链表, 并将当前节点添加到(双向链表的头部节点的后一个位置)
    _freqToList[node.freq]?.addToHead(node);
  }

  /// 判断链表是否为空
  bool isEmpty() {
    return _capacity == 0;
  }

  /// 打印
  display() {
    _keyToNode.forEach((key, value) {
      print(
          'LFUCache, _keyToNode, key: $key, value: ${value.value}, freq: ${value.freq}');
    });
    _freqToList.forEach((key, value) {
      print('LFUCache, key: $key, value: ${value.travel()}');
    });
  }
}

class LfuOverFlowException implements Exception {
  const LfuOverFlowException();
  String toString() => 'LruOverFlowException';
}

class LfuEmptyException implements Exception {
  const LfuEmptyException();
  String toString() => 'LruEmptyException';
}

void main() {
  final LFUCache _cache = LFUCache(10);

  /// 添加数据
  _cache.put('1', "哈哈哈");
  _cache.put('2', "😄😄");
  _cache.put('3', "YOYO");
  _cache.put('4', "\(^o^)/~");
  _cache.put('5', "iOS");
  _cache.put('6', "flutter");
  _cache.put('7', "android");
  _cache.put('8', "mac");
  _cache.put('9', "mini");
  _cache.put('10', "apple");

  var val = _cache.get('1');
  val = _cache.get('1');

  val = _cache.get('2');
  val = _cache.get('2');

  val = _cache.get('4');

  val = _cache.get('5');
  val = _cache.get('5');
  val = _cache.get('5');
  val = _cache.get('5');
  val = _cache.get('5');
  val = _cache.get('5');

  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');
  val = _cache.get('7');

  val = _cache.get('10');
  val = _cache.get('10');

  val = _cache.get('3');
  val = _cache.get('3');

  val = _cache.get('8');
  val = _cache.get('8');
  val = _cache.get('8');
  val = _cache.get('8');
  val = _cache.get('8');

  val = _cache.get('9');
  val = _cache.get('9');

  val = _cache.get('6');
  val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');
  // val = _cache.get('6');

  print('LFUCache, val: ${val} \n');

  _cache.display();
  print(' \n ');

  _cache.put('11', "apple");
  val = _cache.get('11');
  val = _cache.get('11');
  val = _cache.get('11');
  val = _cache.get('11');
  _cache.display();
  print(' \n ');

  _cache.put('12', "appleh");
  _cache.display();
  print(' \n ');

  // _cache.put(13, "哟呵");
  // _cache.display();
  // print(' \n ');
}

// /*
// https://leetcode.cn/problems/lfu-cache/solutions/186348/lfuhuan-cun-by-leetcode-solution/
// */

参考资料

力扣官方题解

算法练习-LRU、LFU缓存机制

源码浅析-iOS缓存NSCache