每日一题&智能指针

每日一题

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所管理的对象的场景。
相关推荐
苏言の狗26 分钟前
CCF认证202406-01 | 矩阵重塑(其一)
c语言·数据结构·c++·算法·矩阵
@大嘴巴子1 小时前
从入门到精通数据结构----四大排序(上)
数据结构·算法·排序算法
_GR1 小时前
每日OJ_牛客_游游的字母串_枚举_C++_Java
java·数据结构·c++·算法·哈希算法
蹉跎x2 小时前
力扣104. 二叉树的最大深度
算法·leetcode·职场和发展
gaogao_jack2 小时前
[Leetcode小记] 3233. 统计不是特殊数字的数字数量
java·算法·leetcode
zzzhpzhpzzz2 小时前
设计模式——解释器模式
算法·设计模式·解释器模式
一只鸡某2 小时前
实习冲刺第二十九天
数据结构·c++·算法·leetcode
鲨鱼吃橘子3 小时前
【C++融会贯通】哈希表的使用
c语言·开发语言·数据结构·c++·链表·哈希算法·散列表
ZZZ_O^O3 小时前
【贪心算法入门第一题——860.柠檬水找零】
学习·算法·leetcode·贪心算法