【问题标题】:C++ LRU cache - need suggestions on how to improve speedC++ LRU 缓存 - 需要有关如何提高速度的建议
【发布时间】:2020-04-27 04:48:21
【问题描述】:

任务是实现一个 O(1) 最近最少使用的缓存

这是关于leetcode的问题
https://leetcode.com/problems/lru-cache/

这是我的解决方案,虽然它是 O(1),但它不是最快的实现
您能否提供一些反馈,或者关于如何优化它的想法?谢谢!


#include<unordered_map>
#include<list>

class LRUCache {

    // umap<key,<value,listiterator>>
    // store the key,value, position in list(iterator) where push_back occurred
private:
    unordered_map<int,pair<int,list<int>::iterator>> umap;
    list<int> klist;  
    int cap = -1;

public:
    LRUCache(int capacity):cap(capacity){

    }

    int get(int key) {
        // if the key exists in the unordered map
        if(umap.count(key)){
            // remove it from the old position 
            klist.erase(umap[key].second);
            klist.push_back(key);
            list<int>::iterator key_loc = klist.end();
            umap[key].second = --key_loc;
            return umap[key].first;
        }
        return -1;
    }

    void put(int key, int value) {

        // if key already exists delete it from the the umap and klist
        if(umap.count(key)){
            klist.erase(umap[key].second);
            umap.erase(key);
        }
        // if the unordered map is at max capacity
        if(umap.size() == cap){
            umap.erase(klist.front());
            klist.pop_front();
        }
        // finally update klist and umap
        klist.push_back(key);
        list<int>::iterator key_loc = klist.end();
        umap[key].first = value;
        umap[key].second = --key_loc;
        return;
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

【问题讨论】:

  • 从技术上讲,地图大小不应超过容量,否则这是一个错误的设计
  • 从实际操作系统的角度来看是有意义的。

标签: c++ algorithm lru


【解决方案1】:

以下一些优化可能会有所帮助:

get函数中获取这段代码:

    if(umap.count(key)){
        // remove it from the old position 
        klist.erase(umap[key].second);

上面会在地图中查找key两次。一次用于count 方法,看看它是否存在。另一个调用[] 运算符来获取它的值。这样做可以节省几个周期:

auto itor = umap.find(key);
if (itor != umap.end()) {
        // remove it from the old position 
        klist.erase(itor->second);

put 函数中,您可以这样做:

    if(umap.count(key)){
        klist.erase(umap[key].second);
        umap.erase(key);
    }

get,可以通过umap避免冗余搜索。此外,没有理由调用 umap.erase 只是为了在几行之后将相同的键添加回映射中。

另外,这也是低效的

    umap[key].first = value;
    umap[key].second = --key_loc;

与上面类似,在地图中重复查找 key 两次。在第一个赋值语句中,key 不在映射中,因此它默认构造一个新的值对事物。第二个任务是在地图中进行另一次查找。

让我们重组你的put函数如下:

void put(int key, int value) {

    auto itor = umap.find(key);
    bool reinsert = (itor != umap.end());

    // if key already exists delete it from the klist only
    if (reinsert) {
        klist.erase(umap[key].second);
    }
    else {
        // if the unordered map is at max capacity
        if (umap.size() == cap) {
            umap.erase(klist.front());
            klist.pop_front();
        }
    }

    // finally update klist and umap
    klist.push_back(key);
    list<int>::iterator key_loc = klist.end();
    auto endOfList = --key_loc;

    if (reinsert) {
        itor->second.first = value;
        itor->second.second = endOfList;
    }
    else  {
        const pair<int, list<int>::iterator> itempair = { value, endOfList };
        umap.emplace(key, itempair);
    }
}

这可能是您使用std::list 所能达到的程度。 list 类型的缺点是,如果不先将现有节点删除然后再将其添加回来,就无法将现有节点从中间移动到前面(或后面)。这是更新列表的几个不需要的内存分配。可能的替代方法是您只需使用自己的双链表类型并自己手动修复 prev/next 指针。

【讨论】:

  • 您可以使用std::list::splice 将同一列表中的节点移动到前面或后面。
  • @selbie 谢谢!!我尝试了您的建议,尽管代码改进了它现在运行时间为 172 毫秒而不是 184 毫秒,而且 umap.emplace(key,itempair) 与 umap[key] = itempair 有何不同。据我了解 unordered_map 插入和查找是 O(1) 。
  • @Blastfurnace 尝试使用 klist.splice(klist.end(),klist,it->second.second);给我 [ AddressSanitizer : alloc-dealloc-mismatch。 ] 这是将中间节点移到列表末尾的正确方法吗?
  • @auxeon 看起来是正确的。除了this is the code 我在 LeetCode 上使用并被接受之外,我不在一个可以测试任何东西的地方。请注意,我正在拼接到前面,但拼接到后面也应该可以。
【解决方案2】:

这是我的解决方案,虽然它是 O(1),但它不是最快的实现 你能提供一些反馈,也许我该如何优化这个想法?谢谢!

我要在这里讨论 selbie 的观点:
if(umap.count(key)) 的每个实例都将搜索键,使用 umap[key] 是搜索的等效项。您可以通过单个 std::unordered_map::find() 操作分配一个指向键的迭代器来避免重复搜索。

selbie 已经给出了int get() 的搜索代码,这是void put() 的搜索代码:

auto it = umap.find(key);
if (it != umap.end()) 
{
    klist.erase(it ->second);
    umap.erase(key);
}

边箱:

由于缺少输入和输出工作,目前不适用于您的代码,但如果您使用 std::cinstd::cout,您可以禁用 C 和 C++ 流之间的同步,并将 cout 中的 cin 解绑为优化:(它们默认绑定在一起)

// If your using cin/cout or I/O
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);

【讨论】:

  • cin.tiecout.tie 调用如何提高这个完全没有 I/O 的特定代码的性能?
  • @selbie 对! (我很快查了一下代码并假设了一个 cin/cout 的实例)已编辑
猜你喜欢
  • 2023-04-04
  • 1970-01-01
  • 2016-09-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-03
  • 2019-09-27
  • 1970-01-01
  • 2012-06-17
相关资源
最近更新 更多