每日一题&智能指针

每日一题

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所管理的对象的场景。
相关推荐
无限码力4 分钟前
[矩阵扩散]
数据结构·算法·华为od·笔试真题·华为od e卷真题
gentle_ice4 分钟前
leetcode——矩阵置零(java)
java·算法·leetcode·矩阵
查理零世6 分钟前
保姆级讲解 python之zip()方法实现矩阵行列转置
python·算法·矩阵
zhbi9826 分钟前
测量校准原理
算法
时间很奇妙!42 分钟前
decison tree 决策树
算法·决策树·机器学习
sysu631 小时前
95.不同的二叉搜索树Ⅱ python
开发语言·数据结构·python·算法·leetcode·面试·深度优先
红鲤鱼遇绿鲤鱼1 小时前
uva 1354 Mobile Computing
算法
‘’林花谢了春红‘’2 小时前
Leetcode::3432. 统计元素和差值为偶数的分区方案
算法·leetcode·职场和发展
三月七(爱看动漫的程序员)2 小时前
Genetic Prompt Search via Exploiting Language Model Probabilities
大数据·人工智能·算法·语言模型·自然语言处理·prompt
SsummerC2 小时前
【leetcode100】从前序与中序遍历序列构造二叉树
python·算法·leetcode