实现一个简便的 LRUCache 和 LFUCache

   本文主要是介绍了 LruCache 和 LFUCache,以及对应的简单实现,阅读时长大约 8 分钟

   文章分为2个部分:

目录

  1、LRUCache

概述

简易实现

2、LFUCache

  概述

  简易实现:  


 


  1、LRUCache

  • 概述

           在 Android 或者 java 开发中, LRUCache 的使用很广泛,Android 中的在 android.util.LruCache 这个类,使用什么的就不展开了,进入里面看可以看到借助了 LinkedHashMap 来实现了 LruCache,类的内容并不多,通过注释可以很快理解,这里不做详细展开。

          在 Linux 内核中,页框回收算法( page frame reclaiming algorithm, 简称 PFRA) 采取从用户态进程和内核告诉缓存“窃取”页框的办法补充伙伴系统的空闲块列表,PFRA必须处理多种属于用户态进程、次盘高速缓存和内存高速缓存的页,它的执行有三种基本情形:

  • 内存紧缺回收
  • 睡眠回收
  • 周期回收

         实际上,在用完所有空闲内存之前,就必须执行 PFRA,否则内核很可能陷入一种内存请求的僵局中,严重会导致系统崩溃,而 PFRA 的目标之一就是保存最少的空闲页框池以便内核可以安全地从“内存紧缺”的情形中恢复过来。 对于 PFRA 来说,PFRA 选取的页框肯定不是空闲的(think think ~~),尽管很容易确定回收内存的候选页,但是选择合适的目标页也是需要重点关注的问题。这个时候就引出了 LRU 算法 : LRU 算法的主要思想就是用一个计数器来存放RAM中每一页的页年龄,即上一次访问该页到现在已经过的时间,这个计数器可使PFRA只回收任何进程的最旧页,但是一些处理器并不支持这样的功能,因此,Linux 使用每页表项中的访问标志位,即 (最近最少使用)LRU 链表。

        属于进程用户态地址空间或页高速缓存的所有页被分成两组:活动链表与非活动链表。它们被统称为 LRU 链表。前面一个链表用于存放最近被访问过的页,后面的则存放有一段时间没有被访问过的页,显然,页必须从非活动链表中窃取。页的活动链表和非活动链表是 PFRA 的核心数据结构,这两个双向链表(敲黑板,双向链表~~)的头分别存放在每个 zone 描述符的 active_list 和 inactive_list 字段,看名字就可以看出来了,而该描述符的 nr_activie 和 nr_inactive 字段表示存放在两个链表中的页数。最后,有一个字段,叫 lru_lock ,这是一个自旋锁   (自旋锁可以看看jvm的),主要是保护两个链表免受并发访问。那么,如果页属于LRU链表,则设置页描述符中的 PG_lru 标志,此外,如果页属于活动链表,则设置 PG_active 标志,如果属于非活动链表,则清PG_active 标志。页描述符的 lru 字段存放指向 LRU 链表中下一个元素和前一个元素的指针。

       因此,PFRA 要做的还有就是在 LRU 链表之间移动页,如下:                        

      如上,当内核必须把一个页标记为访问过时,就调用 mark_page_accessed() 函数,PFRA扫描一页调用一次page_referenced() 函数,而 refill_inactivie_zone() 函数由 shrink_zone()  调用,而shrink_zone()函数对页高速缓存和用户态地址空间进行页回收。

      可见,LRUCache 在 Android 以及 操作系统中都有着很广泛的用途。

  • 简易实现

      上面做了一个简单的概述,那么,要实现一个 LRUCache,包含 get 和 set 方法,并且要求这两个方法的时间复杂度都是 O(1),可以怎么做呢?

实现的方式有很多种,下面的代码主要是使用了双向链表:

首先,脑海里先浮现一副结构图:

图中包含了头节点 head 和 尾节点 tail,中间是有限数量的节点,对于这些节点可以做 get 和 set 操作,接着,根据这幅结构图,先思考几种常规操作以及边界条件:

a、get 和 set 操作需要怎么做 ?

b、get 和 set 有没有共同的操作 ?

c、如果超出了容量要怎么操作?

 

好了,接下来就是具体的实现:

首先定义一个节点的头文件,如下:

  Node.h

#ifndef NODE_H
#define NODE_H

// 定义一个节点 Node

struct Node {
public:
    int key;
    int val;
    Node *pre;
    Node *next;

    Node(int key, int val) : key(key), val(val), pre(nullptr), next(nullptr) {

    }
};

#endif

 接着定义一个 LruCache 的头文件,如下 :

  LruCache.h

#ifndef LRUCACHE_H
#define LRUCACHE_H

#include <unordered_map>
#include "Node.h"

class LruCache {
private:
    int capacity; // 定义容量
protected:
    std::unordered_map<int, Node *> map; // 用于存储 put 传入 的 key 和 对应生成的 Node 节点
    Node *head, *tail;
public:
    LruCache(int capacity);

    int get(int key);

    void put(int key, int val);

    void appendHead(Node *node);

    void removeTail();

    void deleteNode(Node *node);
};

#endif

 最后实现LruCache头文件里面的逻辑,相应注释写在代码里面,如下:

 LruCache.cpp

#include "LruCache.h"

// 初始化构造方法
LruCache::LruCache(int capacity) {
    this->capacity = capacity;    // 初始化容量
    head = new Node(-1, -1);      // 初始化头节点
    tail = new Node(-1, -1);      // 初始化尾节点
    head->next = tail;            // 头节点的 next 指向尾节点
    tail->pre = head;             // 尾节点的 next 指向头节点
}

int LruCache::get(int key) {
    if (map.find(key) == map.end())   // 如果找不到对应的key则返回 -1
        return -1;
    Node *temp = map.find(key)->second;
    deleteNode(temp);                 // 删除访问到的节点的位置,因为要重新将它插入到头节点后面
    appendHead(temp);                 // 访问的节点插入到头节点后面
    return temp->val;
}

void LruCache::put(int key, int val) {
    if (map.find(key) != map.end()) {  // 如果已经有对应节点,更新key的值,并将节点插入到头节点后面
        Node *temp = map.find(key)->second;
        temp->val = val;
        map[key] = temp;
        deleteNode(temp);
        appendHead(temp);
        return;
    }
    Node *temp = new Node(key, val);
    if (map.size() == capacity) {     // 超出 capacity 删除尾节点
        map.erase(map.find(tail->pre->key));
        removeTail();
    }
    map.insert(std::make_pair(key, temp));
    appendHead(temp);
}

void LruCache::appendHead(Node *node) {  // 将指定节点插入到头节点后面
    node->next = head->next;
    head->next->pre = node;
    head->next = node;
    node->pre = head;
}

void LruCache::removeTail() {      // 删除尾部节点
    tail->pre->pre->next = tail;
    tail->pre = tail->pre->pre;
}

void LruCache::deleteNode(Node *node) {    // 删除指定节点
    node->next->pre = node->pre;
    node->pre->next = node->next;
}

 最后来运行下下面代码的效果:

int main() {
    LruCache *lruCache = new LruCache(2);
    lruCache->put(1, 1);
    lruCache->put(2, 2);
    cout << lruCache->get(2) << endl;
    lruCache->put(3, 3);
    lruCache->put(3, 2);
    cout << lruCache->get(3) << endl;
    cout << lruCache->get(1) << endl;

    return 0;
}

/**
 * 结果:
 * 2
 * 2
 * -1
 * */

上面并没有添加哨兵节点,如果需要也可以增加。

总体来说,LruCache 很简单,思路和实现都不难,主要是需要考虑到每次 get 和 put 操作的时候都需要将对应节点插入到头节点后面即可。上面使用 C++ 实现了一遍,没有涉及到什么语法糖,用 java 或者 kotlin 都是可以很快写出来的。简单的整体设计好了,后面各种花样变化就可以慢慢添加。

贴一个java的实现:

public class LRUCache {

    private int capacity;
    private Map<Integer, Node> map = new HashMap<>();
    private Node head, tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.next = tail;
        tail.pre = head;
    }

    public int get(int key) {
        if (!map.containsKey(key))
            return -1;
        Node temp = map.get(key);
        temp.next.pre = temp.pre;
        temp.pre.next = temp.next;
        appendHead(temp);
        return temp.val;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            Node temp = map.get(key);
            temp.val = value;
            map.put(key, temp);
            temp.next.pre = temp.pre;
            temp.pre.next = temp.next;
            appendHead(temp);
            return;
        }
        Node newNode = new Node(key, value);
        if (map.size() == capacity) {
            map.remove(tail.pre.key);
            removeTail();
        }
        map.put(key, newNode);
        appendHead(newNode);
    }


    private void removeTail() {
        tail.pre.pre.next = tail;
        tail.pre = tail.pre.pre;
    }

    private void appendHead(Node node) {
        head.next.pre = node;
        node.next = head.next;
        head.next = node;
        node.pre = head;
    }

    class Node {
        int key;
        int val;
        Node pre;
        Node next;

        Node(int key, int val) {
            this.key = key;
            this.val = val;
            pre = null;
            next = null;
        }
    }

}

2、LFUCache

  •   概述

        相对 LRUCache,LruCache 是基于访问时间,而LFU(Least Frequently Used) 是基于访问频率,即超过容量之后最先淘汰使用次数最少的,当然,LFU是一种称为最不常用的缓存的缓存逐出算法,不过,对于两者来说,LRU 以及 LFU都属于堆栈型替换算法 (堆栈型替换算法的特点:随着分配给程序的主存页面数增加,主存的命中率也提高,至少不下降)可以看看这篇论文 ,内容挺多的:https://www2.eecs.berkeley.edu/Pubs/TechRpts/1987/CSD-87-358.pdf

  •   简易实现:  

         那么,如何实现一个简易的LFUCache呢,由于LFUCache是基于次数的,所以需要考虑用次数来作为key,首先,同样需要在脑海里有一幅图:

 

当然,依然需要考虑这几个问题:

a、get 和 set 操作需要怎么做 ?

b、get 和 set 有没有共同的操作 ?

c、如果超出了容量要怎么操作?

最后就是代码了,注释写在代码里面了,同样,实现LFUCache有很多种方法,这里提供了一种

public class LFUCache {

    private static final int DEFAULT_CAPACITY = 16; // 默认容量
    private Map<Integer, Node> nodeMap = new HashMap<>();
    private Map<Integer, LinkedList<Node>> freq_list_map = new HashMap<>();
    private int mCapacity;
    private int minFreq; // 最少访问次数

    public static void main(String[] args) {
        LFUCache lfuCache = new LFUCache(0);
        lfuCache.put(0, 0);
        lfuCache.get(0);
       // do get or set action
    }

    // 创建内部类 Node
    class Node {
        int key;
        int value;
        int freq;

        Node(int key, int value, int freq) {
            this.key = key;
            this.value = value;
            this.freq = freq;
        }
    }

    // 赋值默认容量
    public LFUCache(){
        this.mCapacity = DEFAULT_CAPACITY;
    }
    
    // 外部传递容量
    public LFUCache(int capacity) {
        this.mCapacity = capacity;
    }


    public int get(int key) {
         // 边界条件
        if (!nodeMap.containsKey(key) || mCapacity <= 0)
            return -1;
        Node result = nodeMap.get(key);
        int fre = result.freq; // 获取访问次数
        freq_list_map.get(fre).remove(result); // 删除掉访问节点,因为后面还要再加上
        if (freq_list_map.get(fre).isEmpty()) {  // 如果以 frep为key的链表已经空了,则可以删掉了
            freq_list_map.remove(fre); // 做删除操作
            if (fre == minFreq) { // 更新最小的 freq,即访问次数
                minFreq++;
            }
        }
        // 如果不包含以 fre + 1 开头的链表,新建一个,并存储进去
        if (!freq_list_map.containsKey(fre + 1)) {
            freq_list_map.put(fre + 1, new LinkedList<Node>());
        }

        freq_list_map.get(fre + 1).add(result);
        result.freq += 1;
        return result.value;
    }

    public void put(int key, int value) {
        if (mCapacity <= 0)
            return;
        if (nodeMap.containsKey(key)) {
            nodeMap.get(key).value = value;
            get(key);
            return;
        }

        // 到达容量上限制,删除最少次数的表头元素
        if (nodeMap.size() == mCapacity) {
            Node n = freq_list_map.get(minFreq).pollFirst();
            if (freq_list_map.get(minFreq).isEmpty()) {
                freq_list_map.remove(minFreq);
            }
            nodeMap.remove(n.key);
        }
        
        // 插入新元素,第一次插入,次数是 1
        minFreq= 1;
        if (!freq_list_map.containsKey(minFreq)) {
            freq_list_map.put(minFreq, new LinkedList<>());
        }
        Node nn = new Node(key, value, 1);
        freq_list_map.get(minFreq).add(nn);
        nodeMap.put(key, nn);
    }
}

       上面就是LFUCache的简易实现,可以稍微思考下,为什么Android里面一些缓存都是LRUCache而不是LFUCache呢,很简单,因为我们时常用的数据都是最新的,比如从网络或者数据库拉取的数据,对于次数并没有什么要求,当然,这两者孰优孰劣,实际要看具体场景,比如堆栈型替换算法,两者的命中率可能不同场景各有优势。

  好了,本文到此告一段落,希望对各位有一点点帮助,文章如有不对,欢迎批评指正,Thanks ~~

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的例子,演示如何在Android Studio中使用OkHttp库实现HTTPS请求。 首先,在build.gradle文件中添加OkHttp库的依赖,如下所示: ``` dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' } ``` 接着,在需要发起HTTPS请求的代码中,我们可以使用OkHttp的"OkHttpClient"类来创建一个可以处理HTTPS请求的客户端,如下所示: ``` public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 创建OkHttpClient实例 OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build(); // 创建Request对象 Request request = new Request.Builder() .url("https://www.example.com/api/data") .build(); // 发起请求 client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "onFailure: " + e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { String responseData = response.body().string(); Log.d(TAG, "onResponse: " + responseData); } }); } } ``` 在上述代码中,我们首先创建了一个OkHttpClient实例,并设置了连接超时、读取超时和写入超时时间。然后,我们创建了一个Request对象,并将HTTPS请求地址作为参数传递进去。最后,我们通过调用"newCall"方法来发起请求,并实现了回调函数,以处理请求响应数据。 需要注意的是,由于HTTPS请求需要验证服务器证书,我们需要在"OkHttpClient"的构造函数中添加一个"SSLSocketFactory"来验证证书。这个过程比较复杂,具体实现方法可以参考OkHttp官方文档。 这就是一个简单的OkHttp实现HTTPS请求的例子。通过使用OkHttp,我们可以快速、简便实现HTTPS请求,并处理请求响应数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值