每日一题
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;
,这里b
是a
的一个副本。 - 资源处理 :在拷贝过程中,如果对象内部管理资源(如动态分配的内存、文件句柄等),需要对这些资源进行深拷贝。例如,若
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;
,这里p2
和p3
共享同一个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
所管理的对象的场景。
- 所有权特点 :