每日一题&智能指针

每日一题

146. LRU 缓存

cpp 复制代码
/*
双向链表节点
*/
class Node
{
public:
    int key;//记录键值 当删除最后一个节点时需要在哈希表中删除
    int val;//记录节点值 方便修改
    Node *pre;//记录前驱节点
    Node *nxt;//记录后继节点
    // 构造函数 初始化节点
    Node(int k = 0, int v = 0) : key(k), val(v), pre(nullptr), nxt(nullptr){};
};
/*
双向链表 + 哈希表 
为什么使用双向链表 因为需要在头部和尾部插入和删除节点 可以插入一个哨兵节点 可以控制头和尾
且在删除节点时可以直接删除 不需要遍历链表 用哈希表记录键值对应节点的地址 查询和删除时间复杂度为O(1) 
1. 哈希表 查找时间复杂度为O(1)
2. 双向链表 插入和删除时间复杂度为O(1)
满足题目要求的时间复杂度和空间复杂度
查找:哈希表的second记录节点的地址 方便get函数返回节点值 使用后将节点插入到链表头部
插入:先在哈希表中查询一下是否存在 如果存在就更新节点值 然后直接退出 否则新建节点直接插入
      然后判断当前链表是否已满 如果已满就删除最后一个节点 然后在哈希表中删除最后一个节点
*/
class LRUCache
{
public:
    int capacity;
    Node *sentry;
    unordered_map<int, Node *> hash;
    // 从双向链表中移除指定节点
    // 该函数将节点的前驱节点的后继指针指向节点的后继节点
    // 同时将节点的后继节点的前驱指针指向节点的前驱节点
    // 这样就将节点从链表中移除了
    void remove(Node *node)
    {
        node->pre->nxt = node->nxt;
        node->nxt->pre = node->pre;
    }
    // 将指定节点添加到双向链表的头部
    // 首先将节点的后继指针指向哨兵节点的后继节点
    // 然后将节点的前驱指针指向哨兵节点
    // 接着将哨兵节点后继节点的前驱指针指向该节点
    // 最后将哨兵节点的后继指针指向该节点
    void push_front(Node *node)
    {
        node->nxt = sentry->nxt;
        node->pre = sentry;
        sentry->nxt->pre = node;
        sentry->nxt = node;
    }
    Node *get_Node(int key)
    {
        // 在哈希表中查找键对应的节点
        auto it = hash.find(key);
        if (it == hash.end())
        {
            return nullptr;
        }
        Node *node = it->second;
        // 从链表中移除该节点
        remove(node);
        // 将该节点添加到链表头部
        push_front(node);
        return node;
    }

    LRUCache(int capacity)
    {
        this->capacity = capacity;
        sentry = new Node();
        // 初始化哨兵节点的前驱和后继指针都指向自身 因为要成循环双向链表
        sentry->pre = sentry;
        sentry->nxt = sentry;
    }

    int get(int key)
    {
        Node* node = get_Node(key);
        // 如果节点存在,返回节点的值,否则返回 -1
        return node? node->val : -1;
    }

    void put(int key, int value)
    {
        Node *node = get_Node(key);
        if(node)
        {
            // 如果节点存在,更新节点的值
            node->val = value;
            return;
        }
        // 若节点不存在,创建新节点并添加到哈希表
        hash[key] = node = new Node(key, value);
        // 将新节点添加到链表头部
        push_front(node);
        if(hash.size() > capacity)
        {
            // 若超出容量,移除链表尾部节点
            Node* last_node = sentry->pre;
            remove(last_node);
            // 从哈希表中删除该节点
            hash.erase(last_node->key);
            delete last_node;
        }
    }
};

主要难度在于手搓双向链表 逻辑不难

移动语义和拷贝语义有什么区别?

  • 拷贝语义
    • 定义 :拷贝语义是指在对象赋值或初始化时,创建一个新的对象,该新对象是源对象的副本。这意味着新对象和源对象在内存中有各自独立的存储空间,并且它们包含相同的值。例如,对于一个简单的类MyClass,如果有MyClass a; MyClass b = a;,这里ba的一个副本。
    • 资源处理 :在拷贝过程中,如果对象内部管理资源(如动态分配的内存、文件句柄等),需要对这些资源进行深拷贝。例如,若MyClass内部有一个指向动态分配数组的指针,在拷贝构造函数和赋值运算符重载函数中,需要为新对象分配新的内存,并将源对象中的数据复制到新内存中,以保证两个对象互不干扰。
    • 性能开销:拷贝语义可能会产生较大的性能开销,尤其是在处理包含大量资源的对象时。每次拷贝都需要分配新的资源并进行数据复制,这可能会消耗大量的时间和内存。
  • 移动语义
    • 定义 :移动语义允许将一个对象的资源 "窃取" 并转移到另一个对象中,而不是进行复制。这种语义主要用于避免不必要的资源拷贝,特别是对于那些包含动态分配资源(如堆内存)且生命周期即将结束的对象。例如,std::vector的移动构造函数可以将一个临时vector对象中的内存指针直接转移给另一个vector对象,而不是重新分配内存并复制元素。
    • 资源处理 :在移动操作中,源对象的资源所有权被转移到目标对象,源对象通常会被置于一种 "有效但未指定" 的状态。例如,在移动一个包含动态分配内存的对象后,源对象的指针可能被设置为nullptr,以表明它不再拥有该内存块的所有权。
    • 性能开销:移动语义在性能上有很大优势,尤其是在处理临时对象或者即将被销毁的对象时。它避免了资源的复制,只涉及简单的指针赋值等操作,从而可以显著提高程序的性能。

什么是 C++ 中的智能指针?有哪些类型的智能指针?

  • 定义:智能指针是一种类模板,用于管理动态分配的对象。它的主要目的是自动管理对象的生命周期,避免内存泄漏和悬空指针等问题。智能指针在对象不再被使用时,会自动释放其所指向的对象占用的内存。
  • 类型
    • std::unique_ptr
      • 所有权特点std::unique_ptr是独占式智能指针。一个对象只能被一个std::unique_ptr所拥有。当std::unique_ptr被销毁时(例如离开作用域),它所指向的对象也会被自动销毁。它不能被复制,但可以被移动,这保证了对象的独占性。例如,std::unique_ptr<int> p1 = std::make_unique<int>(5);,这里p1独占了一个动态分配的int对象。
      • 使用场景:适用于那些具有明确所有权且不需要共享的对象,如在工厂函数中返回一个新创建的对象的唯一所有权,或者在一个对象内部管理独占资源。
    • std::shared_ptr
      • 所有权特点std::shared_ptr是共享式智能指针。多个std::shared_ptr可以共同指向同一个对象,对象的生命周期由所有指向它的std::shared_ptr共同管理。每一个std::shared_ptr都包含一个引用计数,当一个新的std::shared_ptr指向该对象时,引用计数加 1;当一个std::shared_ptr被销毁或者不再指向该对象时,引用计数减 1。当引用计数为 0 时,对象会被自动销毁。例如,std::shared_ptr<int> p2 = std::make_shared<int>(10); std::shared_ptr<int> p3 = p2;,这里p2p3共享同一个int对象,引用计数为 2。
      • 使用场景:用于需要共享对象所有权的情况,如在多个对象之间共享数据,或者在函数返回值中返回一个对象的共享所有权。
    • std::weak_ptr
      • 所有权特点std::weak_ptr是一种辅助std::shared_ptr的智能指针。它不控制对象的生命周期,而是提供了一种可以观察std::shared_ptr所管理对象的方式。std::weak_ptr指向一个由std::shared_ptr管理的对象,但不会增加引用计数。可以通过std::weak_ptr来检查所指向的对象是否仍然存在。例如,在解决循环引用问题时很有用,当两个对象通过std::shared_ptr相互引用时,可能会导致内存泄漏,使用std::weak_ptr可以打破这种循环。
      • 使用场景 :主要用于解决std::shared_ptr的循环引用问题,或者在需要临时性地访问一个可能已经被销毁的std::shared_ptr所管理的对象的场景。
相关推荐
小庞在加油7 分钟前
《dlib库中的聚类》算法详解:从原理到实践
c++·算法·机器学习·数据挖掘·聚类
ComputerInBook10 分钟前
C++ 标准模板库算法之 transform 用法
开发语言·c++·算法·transform算法
hn小菜鸡6 小时前
LeetCode 377.组合总和IV
数据结构·算法·leetcode
Deepoch7 小时前
Deepoc 大模型:无人机行业的智能变革引擎
人工智能·科技·算法·ai·动态规划·无人机
heimeiyingwang9 天前
【深度学习加速探秘】Winograd 卷积算法:让计算效率 “飞” 起来
人工智能·深度学习·算法
时空自由民.9 天前
C++ 不同线程之间传值
开发语言·c++·算法
ai小鬼头9 天前
AIStarter开发者熊哥分享|低成本部署AI项目的实战经验
后端·算法·架构
小白菜3336669 天前
DAY 37 早停策略和模型权重的保存
人工智能·深度学习·算法
zeroporn9 天前
以玄幻小说方式打开深度学习词嵌入算法!! 使用Skip-gram来完成 Word2Vec 词嵌入(Embedding)
人工智能·深度学习·算法·自然语言处理·embedding·word2vec·skip-gram
亮亮爱刷题9 天前
飞往大厂梦之算法提升-7
数据结构·算法·leetcode·动态规划