c++实现lru lfu页面置换算法_面试必问:请手写淘汰算法LRU与LFU

从Redis看淘汰算法

虽然「Redis」有自己的过期策略来删除过期的数据(惰性删除和抽样删除)。这其中具体的删除原理本章不做详细介绍。但是也会存在Redis删不过来导致内存占满的情况。所以「Redis」使用了一些淘汰算法来处理这些来不及删除的数据。

下面我们来说说「LRU」淘汰算法。

LRU算法

定义

「LRU」算法中,需要有一个链表来存放数据,当某个元素被访问时,这个元素会被移动到表头。当空间满了,会剔除掉链表末尾的元素。

其核心就是保留最近使用的元素。

代码实现

我们来看看代码实现:

public class LRULinkedHashMap extends LinkedHashMap {    private int capacity;    LRULinkedHashMap(int capacity) {        // 初始大小,0.75是装载因子,true是表示按照访问时间排序        super(capacity, 0.75f, true);        //传入指定的缓存最大容量        this.capacity = capacity;    }    /**     * 实现LRU的关键方法,如果map里面的元素个数大于了缓存最大容量,则删除链表的顶端元素     */    @Override    protected boolean removeEldestEntry(Map.Entry eldest) {        return size() > capacity;    }}

我们来写个单元测试测试下:

@org.junit.Testpublic void test() {    LRULinkedHashMap map = new LRULinkedHashMap<>(4);    map.put("A", 1);    map.put("B", 2);    map.put("C", 3);    System.out.println(map);    map.get("B");    System.out.println(map);    map.put("D",4);    map.put("E",5);    System.out.println(map);}

测试结果:

{A=1, B=2, C=3}{A=1, C=3, B=2}{C=3, B=2, D=4, E=5}

利用LinkedHashMap的特性:访问的数据会排到最前面。

我们来图解上面代码:

ff59a5494dde3872720c1566cb84cf14.png

(1)我们创建一个容量为「4」的LinkedHashMap,并put初始值:A ->B -> C

(2)查询值「key」「B」的值,「B」会重新排列到最前面。顺序为:A ->C -> B

(3)put新值「D」,顺序为:A ->C ->B ->D

(4)put新值「E」,最末尾的值「A」被淘汰。顺序为:C ->B ->D ->E

LFU算法

定义

「Redis」 4.0中引入了一个新的淘汰算法LFU,可以说是LRU的进阶版。

LFU算法规定的是按最近访问的频率进行淘汰,与LRU算法相比,LFU更精准的表示了一个「key」被访问的热度。

为什么Redis要引入LFU算法呢?

如果一个「key」长时间没有没访问,只是刚刚被用户偶尔访问了一下。在LRU算法下,这个「key」是不容易被淘汰的。但如果是LFU算法,会追踪最近一段时间的访问频率。就是说在LFU算法下,只是最近偶尔被访问一次是不足以说明这个「key」是热点数据。

算法示意图:

0f722ff3e78bae8c5007195998e6ce60.png

如图,算法将访问次数最高的放在最前面,容量满后会删除末尾的元素。

代码实现

public class LFUCache {    private Map map;    /**     * 访问次数哈希表,使用 ListNode[] 也可以,不过要占用很多空间     */    private Map frequentMap;    /**     * 外部传入的容量大小     */    private Integer capacity;    /**     * 全局最高访问次数,删除最少使用访问次数的结点时会用到     */    private Integer minFrequent = 1;    public LFUCache(int capacity) {        map = new HashMap(capacity);        frequentMap = new HashMap();        this.capacity = capacity;    }    /**     * get 一次操作,访问次数就增加 1;     * 从原来的链表调整到访问次数更高的链表的表头     *     * @param key     * @return     */    public int get(int key) {        // 测试测出来的,capacity 可能传 0        if (capacity == 0) {            return -1;        }        if (map.containsKey(key)) {            // 获得结点类            ListNode listNode = removeListNode(key);            // 挂接到新的访问次数的双向链表的头部            int frequent = listNode.frequent;            addListNode2Head(frequent, listNode);            return listNode.value;        } else {            return -1;        }    }    /**     * @param key     * @param value     */    public void put(int key, int value) {        if (capacity == 0) {            return;        }        // 如果 key 存在,就更新访问次数 + 1,更新值        if (map.containsKey(key)) {            ListNode listNode = removeListNode(key);            // 更新 value            listNode.value = value;            int frequent = listNode.frequent;            addListNode2Head(frequent, listNode);            return;        }        // 如果 key 不存在        // 1、如果满了,先删除访问次数最小的的末尾结点,再删除 map 里对应的 key        if (map.size() == capacity) {            // 1、从双链表里删除结点            DoubleLinkedList doubleLinkedList = frequentMap.get(minFrequent);            ListNode removeNode = doubleLinkedList.removeTail();            // 2、删除 map 里对应的 key            map.remove(removeNode.key);        }        // 2、再创建新结点放在访问次数为 1 的双向链表的前面        ListNode newListNode = new ListNode(key, value);        addListNode2Head(1, newListNode);        map.put(key, newListNode);        // 【注意】因为这个结点是刚刚创建的,最少访问次数一定为 1        this.minFrequent = 1;    }    // 以下部分主要是结点类和双向链表的操作    /**     * 结点类,是双向链表的组成部分     */    private class ListNode {        private int key;        private int value;        private int frequent = 1;        private ListNode pre;        private ListNode next;        public ListNode() {        }        public ListNode(int key, int value) {            this.key = key;            this.value = value;        }    }    /**     * 双向链表     */    private class DoubleLinkedList {        /**         * 虚拟头结点,它无前驱结点         */        private ListNode dummyHead;        /**         * 虚拟尾结点,它无后继结点         */        private ListNode dummyTail;        /**         * 当前双向链表的有效结点数         */        private int count;        public DoubleLinkedList() {            // 虚拟头尾结点赋值多少无所谓            this.dummyHead = new ListNode(-1, -1);            this.dummyTail = new ListNode(-2, -2);            dummyHead.next = dummyTail;            dummyTail.pre = dummyHead;            count = 0;        }        /**         * 把一个结点类添加到双向链表的开头(头部是最新使用数据)         *         * @param addNode         */        public void addNode2Head(ListNode addNode) {            ListNode oldHead = dummyHead.next;            // 两侧结点指向它            dummyHead.next = addNode;            oldHead.pre = addNode;            // 它的前驱和后继指向两侧结点            addNode.pre = dummyHead;            addNode.next = oldHead;            count++;        }        /**         * 把双向链表的末尾结点删除(尾部是最旧的数据,在缓存满的时候淘汰)         *         * @return         */        public ListNode removeTail() {            ListNode oldTail = dummyTail.pre;            ListNode newTail = oldTail.pre;            // 两侧结点建立连接            newTail.next = dummyTail;            dummyTail.pre = newTail;            // 它的两个属性切断连接            oldTail.pre = null;            oldTail.next = null;            // 重要:删除一个结点,当前双向链表的结点个数少 1            count--;            return oldTail;        }    }    /**     * 将原来访问次数的结点,从双向链表里脱离出来     *     * @param key     * @return     */    private ListNode removeListNode(int key) {        // 获得结点类        ListNode deleteNode = map.get(key);        ListNode preNode = deleteNode.pre;        ListNode nextNode = deleteNode.next;        // 两侧结点建立连接        preNode.next = nextNode;        nextNode.pre = preNode;        // 删除去原来两侧结点的连接        deleteNode.pre = null;        deleteNode.next = null;        // 维护双链表结点数        frequentMap.get(deleteNode.frequent).count--;        // 【注意】维护 minFrequent        // 如果当前结点正好在最小访问次数的链表上,并且移除以后结点数为 0,最小访问次数需要加 1        if (deleteNode.frequent == minFrequent && frequentMap.get(deleteNode.frequent).count == 0) {            minFrequent++;        }        // 访问次数加 1        deleteNode.frequent++;        return deleteNode;    }    /**     * 把结点放在对应访问次数的双向链表的头部     *     * @param frequent     * @param addNode     */    private void addListNode2Head(int frequent, ListNode addNode) {        DoubleLinkedList doubleLinkedList;        // 如果不存在,就初始化        if (frequentMap.containsKey(frequent)) {            doubleLinkedList = frequentMap.get(frequent);        } else {            doubleLinkedList = new DoubleLinkedList();        }        // 添加到 DoubleLinkedList 的表头        doubleLinkedList.addNode2Head(addNode);        frequentMap.put(frequent, doubleLinkedList);    }}

测试代码:

@Testpublic void test() {    LFUCache cache = new LFUCache(3);    //tail ->  head    // ①[1,2,3]    cache.put(1, 1);    cache.put(2, 2);    cache.put(3, 3);    // ②[2,3,1]    int i = cache.get(1);    int i1 = cache.get(1);    // ③[3,2,1]    cache.get(2);    cache.put(4,4);    // ④[4,1,2]    System.out.println(cache.map.keySet());}

运行结果:

c848b62121f6d28eeaf475e944047daf.png

我们来分析下:

(1)设容量为「3」,最开始put值,「map」 (取的「key」)为[1,2,3],初始每个元素访问计数为1;

(2)get获取两次「1」「1」的计数为1+2=3次,map为[2,3,1];

(3)get获取「2」一次,「2」的计数为1+1=2次,map为[3,2,1];

(4)put值4,由于map容量达到上限,访问次数最少的「1」被淘汰。由于「4」的计数为1次,「4」排到最末尾。map值为[4,1,2]。

总结

由上面可知。LRU算法和LFU算法有各自的特点,我们应该根据实际业务使用情况去使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值