标签:ast github 方法 一起 字段 总结 功能 决定 delete
把设计思路梳理一遍+实现思路梳理1遍+调试方法梳理1遍
设计
1.1 数据结构怎么选择的。
1.2 数据结构的每个字段都是怎么来的。
实现
调试方法
一种是leetcode题目, 直接让你实现put,get
一种是自己设计并实现LRUCache。第一种题是第二种题的一部分。
主要功能
存放缓存,然后满的时候需要淘汰一些缓存,淘汰策略是LRU。
设计第一步: Scenario Testing(场景测试) / 使用测试
模拟一个LRU的使用过程明确一下我们要实现什么东西。// 这个步骤是做很多软件设计的重要步骤,比如接口设计和框架API设计等。
假如缓存的容量是2。
put1-1, put2-2, put3-3(淘汰1)
put1-1, put2-2, get1-1/put1-11, put3-3(淘汰2)
由此梳理出该数据结构的2个设计要点
1, 是一个具有put,get操作的集合。其实还有delete操作删除指定key(比如put1-11,我们既可以移动旧节点到头节点,也可以删除旧节点然后创建新节点插入头)。
2, 这个集合需要有某种顺序,使得我们在淘汰时能够确定要淘汰的元素,这里就是把要淘汰的元素放在末尾了,淘汰是直接淘汰即可。
3, 缓存淘汰策略最近最少使用和缓存提升策略是最近使用。
从场景测试可以看出,在做put/get操作,有很多插入集合头部,删除集合尾部的操作。我们的存储结构无非就是数组,链表,树,
数组的插入头O(n)删除O(n),链表的插入头O(1)删除O(n)+O(1),树好像没有头尾节点一说,而且我们一般从易到难进行选取,这里链表似乎就满足需求了。
链表又分单链表,双链表,循环单链表,循环双链表,跳表。
此时产生第一版设计:先选择单链表。
链表是先O(n)查找再O(1)删除。可以通过引入map查找表来优化为O(1)删除。
此时产生第二版设计:map{缓存key->链表节点指针}+单链表{val,next}。
使用第二版设计进行删除操作,我们是需要知道被删除节点的pre节点的,所以需要把pre节点保存到某个地方。
此时产生第三版设计:map{缓存key->链表节点指针}+双链表{pre, val, next}
考虑这样一种情况: 当缓存满了,我们需要在链表中删除节点,也需要在map中删除对应的key。大概步骤如下:
1. 定位到尾节点,删除链表尾节点。
因为需要"定位到尾节点", 所以该数据结构需要引入1个tail来记录尾节点的地址。
第1步O(1)。
2. 定位尾节点在map中的键值对并删除该键值对。
"定位尾节点对应的键值", 因为链表节点没有保存key, 所以我们哪不到key, 也就不能直接delete(map,key), 而是需要遍历map来查找尾节点(v==tail的节点)然后删除。
第2步是O(n)。
所以此时的设计删除操作是O(n)的,因为需要遍历map,我们可以把key存放到链表节点中消除这个遍历过程。直接通过delete(map,tail.key)可以直接删除,而不必遍历map。
这样第1,2步就都是O(1)的。
此时产生第四版设计:[tail + map{缓存key->链表节点指针}] + 双链表{pre, key, val, next}。
之前我们都在考虑删除节点的情况,现在来考虑头插法插入头节点的情况。
很明显我们需要1个head指针来快速定位头节点。
此时产生第五版设计:[head + tail + map{缓存key->链表节点指针}] + 双链表{pre, key, val, next}。
我们知道单链表的删除引入dummyhead就是为了统一头节点和中间节点的删除逻辑。这里引入dummyhead也是同样的道理。
至于为什么还要引入dummytail,考虑[head<->1<->2]和[head<->1<->nil], 可以看到如果没有tail的话,在删除尾节点时就有head<-nil的异常, 那么删除中间节点和删除尾节点也是2套逻辑,所以也是引入dummytail来统一为中间节点的删除逻辑[head<->tail]。
此时产生第六版设计:[dummyhead + dummytail + map{缓存key->链表节点指针}] + 双向循环链表
{pre, key, val, next}。
双向循环链表初始化为[head<->tail],之后不管我们对链表进行怎样的插入和删除,整个链表是不存在nil节点,这样我们就不用担心访问空节点而导致异常的情况了。
此时产生第七版设计:[dummyhead + dummytail + map{缓存key->链表节点指针} + cap] + 双链表{pre, key, val, next}。
很多点没考虑到,写不走,上述7个版本的设计都是事后归因来总结的。
这里需要关注,我们是如何提取出remove和add这样共性的基础操作。
在另外1个思路里面,还提取了moveToHead这样的共性操作。
通过打印关键信息来调试,比debug调试效率更高。
打印代码编写。
leetcode刷题:3点
1.1 map+双向循环链表。初始化[head<->tail]
1.2 put, get, remove, add, 把2个操作拆分为4个操作
1.3 put操作根据exist, full 分4种情况讨论,然后看情况合并一些情况即可。
面试中: 这个手写有点费时间,一般不会考,主要说设计的思路,应该不会有7版这么多,但是我们需要答到:// 这个我也拿不准,需要讨论。
为啥引入map,不选数组选链表,选双向链表,双向循环链表和dummy节点。
2个节点的字段都是为什么添加进去的,各自的功能是什么?
标签:ast github 方法 一起 字段 总结 功能 决定 delete
原文地址:https://www.cnblogs.com/yudidi/p/12622296.html