刷穿LeetCode——Task12

这篇博客记录刷题第12天的解题过程与学习所得。

146. LRU 缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。

实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例

输入 [“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”,
“get”, “get”] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1],
[3], [4]]
输出 [null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是{1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); 返回 3
lRUCache.get(4); // 返回 4

提示

  • 1 <= capacity <= 3000
  • 0 <= key <= 3000
  • 0 <= value <= 104
  • 最多调用 3 * 104 次 get 和 put

这题非常难,我好不容易才看懂题解,以下分析和代码也都全摘自题解区[1]。
LRU的思路:删除最久未被使用的数据,核心是:快速查找+数据存储的有序性

  • 快速查找 O ( 1 ) O(1) O(1) 完成,那么立即就要想到哈希表;
  • 因为哈希表是无序的,所以为了保证数据的有序性,可以借助链表来实现,插入、删除可以在常数时间 O ( 1 ) O(1) O(1) 内完成;
  • 链表的表尾为最久未被使用数据,头部为最近使用数据。所以插入数据若key不存在则直接插在链表头部,存在则更新val后移到表头,删除数据则直接删除表尾数据。

LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:

HashLinkedList

思想很简单,就是借助哈希表赋予了链表快速查找的特性嘛:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。

  1. get方法访问缓存中的数据,并把当前访问的数据(key, val) 提到表头(最近使用标记);

  2. put方法插入新的元素对;

  • 如果已存在key, 则更新对应的val并移到表头;
  • 若链表未满,则直接插入新数据到表头;
  • 若链表已满,则删除表尾数据(最久未被使用);
struct DLinkedNode {   //双向链表(插入、删除O(1))+ 哈希表(查找O(1))
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};


class LRUCache {
private:              //成员变量
    unordered_map<int, DLinkedNode*> cache;  //哈希表对节点的映射
    DLinkedNode* head;  
    DLinkedNode* tail;
    int size;
    int capacity;
public:
    LRUCache(int _capacity): capacity(_capacity), size(0) {   //类初始化
        // 使用伪头部和伪尾部节点,避免检查相邻的节点是否存在。
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;			//向右是next
        tail->prev = head;			//向左是pre
    }
    
    int get(int key) {
        if (!cache.count(key)) {  //key 不存在
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到双向链表头部
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }
    
    void put(int key, int value) {
        if (cache.count(key)){
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到双向链表头部
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        } else {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode* node = new DLinkedNode(key, value);
            // 添加进哈希表,cache中新建 key 对新节点 node 的映射;
            cache[key] = node;
            // 添加至双向链表的头部
            addToHead(node);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode* removed = removeTail();
                // 删除哈希表中对应的项
                cache.erase(removed->key);
                // 防止内存泄漏
                delete removed;
                --size;
            }
        }
    }

     void addToHead(DLinkedNode* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }
    
    void removeNode(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(DLinkedNode* node) {
        removeNode(node);
        addToHead(node);
    }

    DLinkedNode* removeTail() {
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }
};

  • 为什么必须要用双向链表?
    因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前节点的指针,而双向链表才能支持直接查找前节点,保证操作的时间复杂度 O ( 1 ) O(1) O(1)
  • 哈希表中已经存储key为什么要在链表中仍要同时存储 key 和 val,而不是只存储 val?
    (1) 首先创建节点时需添加进哈希表,map中需创建key对新节点的映射:
    DLinkedNode* node = new DLinkedNode(key, value);
    cache[key] = node;
    (2) 当缓存容量已满,我们不仅仅要删除表尾节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 node 得到。如果 node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。
    DLinkedNode* removed = removeTail();
    cache.erase(removed->key);

148.排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

进阶

  • 你可以在 O(n log n) 时间复杂度和常数级 O ( 1 ) O(1) O(1) 空间复杂度下,对链表进行排序吗?

示例 1:
sortList_1
输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例 2:
sortList_2
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目在范围 [0, 5 * 1 0 4 10^4 104] 内
  • - 1 0 5 10^5 105 <= Node.val <= 1 0 5 10^5 105

直接引用官方题解[2],分析如下:时间复杂度是 O ( n l o g ⁡ n ) O(nlog⁡n) O(nlogn) 的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O ( n 2 ) O(n^2) O(n2),其中最适合链表的排序算法是归并排序。

归并排序基于分治算法。最容易想到的实现方式是自顶向下的递归实现,考虑到递归调用的栈空间,自顶向下归并排序的空间复杂度是 O ( l o g n ) O(log n) O(logn)。如果要达到 O ( 1 ) O(1) O(1) 的空间复杂度,则需要使用自底向上的实现方式。

方法一:自顶向下归并排序

对链表自顶向下归并排序的过程如下。

  1. 找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2 步,慢指针每次移动 1 步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。

  2. 对两个子链表分别排序。

  3. 将两个排序后的子链表合并,得到完整的排序后的链表。可以使用「21. 合并两个有序链表」的做法,将两个有序的子链表进行合并。

上述过程可以通过递归实现。递归的终止条件是链表的节点个数小于或等于 1,即当链表为空或者链表只包含 [1] 个节点时,不需要对链表进行拆分和排序。

class Solution {
public:
    ListNode* sortList(ListNode* head) { 
    return sortList(head, nullptr); 
    }
    ListNode* sortList(ListNode* head, ListNode* tail) {  //拆分排序合并,递归调用
        if (head == nullptr) {
            return head;
        }
        if (head->next == tail) {
            head->next = nullptr;
            return head;
        }
        ListNode* slow = head, *fast = head;  //快慢指针找链表中点
        while (fast != slow) {
            slow = slow -> next;
            fast = fast -> next;
            if (fast != slow)   fast = fast -> next;
            }
        ListNode* mid = slow;
        return merge(sortList(head, mid), sortList(mid, tail))
    }

    ListNode* merge(ListNode* head1, ListNode* head2) {   //合并两有序链表
        ListNode* dummyHead = new ListNode(0);
        ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
        while (temp != nullptr && temp2 != nullptr) {
            if (temp1->val <= temp2->val) {
                temp->next = temp1;
                temp1 = temp1->next;
            } else {
                temp->next = temp2;
                temp2 = temp2->next;
            }
        }
        if (temp1 != nullptr) {
            temp->next = temp1
        } else if (temp2 != nullptr) {
            temp->next = temp2;           
        }
        return dummyHead->next;
    }
};

方法二:自底向上归并排序

使用自底向上的方法实现归并排序,则可以达到 O ( 1 ) O(1) O(1) 的空间复杂度。

首先求得链表的长度 length,然后将链表拆分成子链表进行合并。

具体做法如下。
1. 用 subLength 表示每次需要排序的子链表的长度,初始时 s u b L e n g t h = 1 subLength=1 subLength=1
2. 每次将链表拆分成若干个长度为 subLength 的子链表(最后一个子链表的长度可以小于 subLength ),按照每两个子链表一组进行合并,合并后即可得到若干个长度为 s u b L e n g t h × 2 subLength×2 subLength×2 的有序子链表(最后一个子链表的长度可以小于 s u b L e n g t h × 2 subLength×2 subLength×2 )。合并两个子链表仍然使用「21. 合并两个有序链表」的做法。
3. 将 subLength 的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于 length,整个链表排序完毕。

如何保证每次合并之后得到的子链表都是有序的呢?可以通过数学归纳法证明。
1. 初始时 s u b L e n g t h = 1 subLength=1 subLength=1 ,每个长度为 1 的子链表都是有序的。
2. 如果每个长度为subLength 子链表已经有序,合并两个长度为 subLength 的有序子链表,得到长度为 s u b L e n g t h × 2 subLength×2 subLength×2 的子链表,一定也是有序的。
3. 当最后一个子链表的长度小于 subLength 时,该子链表也是有序的,合并两个有序子链表之后得到的子链表一定也是有序的。

因此可以保证最后得到的链表是有序的。

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (head == nullptr) {
            return head;
        }
        int length = 0;
        ListNode* node = head;
        while (node != nullptr) {
            length++;
            node = node->next;
        }
        ListNode* dummyHead = new ListNode(0, head);
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode* prev = dummyHead, *curr = dummyHead->next;
            while (curr != nullptr) {
                ListNode* head1 = curr;
                for (int i = 1; i < subLength && curr->next != nullptr; i++) {
                    curr = curr->next;
                }
                ListNode* head2 = curr->next;
                curr->next = nullptr;
                curr = head2;
                for (int i = 1; i < subLength && curr != nullptr && curr->next != nullptr; i++) {
                    curr = curr->next;
                }
                ListNode* next = nullptr;
                if (curr != nullptr) {
                    next = curr->next;
                    curr->next = nullptr;
                }
                ListNode* merged = merge(head1, head2);
                prev->next = merged;
                while (prev->next != nullptr) {
                    prev = prev->next;
                }
                curr = next;
            }
        }
        return dummyHead->next;
    }

    ListNode* merge(ListNode* head1, ListNode* head2) {
        ListNode* dummyHead = new ListNode(0);
        ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
        while (temp1 != nullptr && temp2 != nullptr) {
            if (temp1->val <= temp2->val) {
                temp->next = temp1;
                temp1 = temp1->next;
            } else {
                temp->next = temp2;
                temp2 = temp2->next;
            }
            temp = temp->next;
        }
        if (temp1 != nullptr) {
            temp->next = temp1;
        } else if (temp2 != nullptr) {
            temp->next = temp2;
        }
        return dummyHead->next;
    }
};

155. 最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) —— 将元素 x 推入栈中。
  • pop() —— 删除栈顶的元素。
  • top() —— 获取栈顶元素。
  • getMin() —— 检索栈中的最小元素。

示例:

输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.

提示:

  • pop、top 和 getMin 操作总是在 非空栈 上调用。

参考

[1]https://leetcode-cn.com/problems/lru-cache/solution/cpp-unorder_maplist-jie-jue-by-zhangm365-tj57/
[2]https://leetcode-cn.com/problems/sort-list/solution/pai-xu-lian-biao-by-leetcode-solution/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
插件名字叫leetcode editor,可以解决在上班时想leetcode但又不想直接打开leetcode界面太扎眼或者无法debug的问题。你可以在IDEA的plugins中搜索并下载leetcode editor插件。 插件的下载地址是https://plugins.jetbrains.com/plugin/12132-leetcode-editor。 下载并安装插件后,你可以在IDEA的File -> Settings -> Tools -> Leetcode Plugin***com作为网址选项。此外,你还可以选择代码类型,包括Java、Python、C、Python3、C、C#、JavaScript、Ruby、Swift、Go、Scala、Kotlin、Rust、PHP。你需要输入登录名和密码来登录leetcode账号,并可以设置临时文件的存放目录和HTTP Proxy。 如果你想自定义代码模板,可以参考该插件提供的自定义代码模板文档(https://github.com/shuzijun/leetcode-editor/blob/master/doc/CustomCode.md)。通过这个插件,你可以方便地在IDEA中leetcode,并享受更好的调试体验。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [好用的idea插件leetcode editor【详细安装指南】](https://blog.csdn.net/weixin_45988401/article/details/129170239)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [IDEA插件系列(112):LeetCode Editor插件——LeetCode编辑器](https://blog.csdn.net/cnds123321/article/details/119859448)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值