关于我的仓库
- 这篇文章是我为面试准备的学习总结中的一篇
- 我将准备面试中找到的所有学习资料,写的Demo,写的博客都放在了这个仓库里iOS-Engineer-Interview
- 欢迎star??
- 其中的博客在简书,优快云都有发布
- 博客中提到的相关的代码Demo可以在仓库里相应的文件夹里找到
前言
- 该系列为学习《数据结构与算法之美》的系列学习笔记
- 总结规律为一周一更,内容包括其中的重要知识带你,以及课后题的解答
- 算法的学习学与刷题并进,希望能真正养成解算法题的思维
- LeetCode刷题仓库:LeetCode-All-In
- 多说无益,你应该开始打代码了
06讲链表(上):如何实现LRU缓存淘汰算法
- 常见缓存淘汰策略:先进先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frequently Used)、最近最少使用策略LRU(Least Recently Used)。
- 删除指定节点:由于删除节点需要使用前一个节点的next指针,所以对于单链表,在执行这样的删除,插入【前一个插入】的时候,都需要遍历链表
- 而双向链表对此就很有优势,包括对于有序链表查找,由于可以判定往后还是往前【此处存疑,怎么从中间开始?】
- 对于执行较慢的程序,可以通过消耗更多的内存(空间换时间)来进行优化;而消耗过多内存的程序,可以 通过消耗更多的时间(时间换空间)来降低内存的消耗。

- 在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。而 链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。CPU在从内存读取数据的时候,会先把读取到的数据加载到CPU的缓存中。而CPU每次从内存读取数据并不是只读取那个特定 要访问的地址,而是读取一个数据块并保存到CPU缓存中,然后下次访问内存数据的时候就会先从CPU缓存开始查找,如果找到就不需要再从内存中取。这样就实现了比内存访问速度更快的机制,也就是CPU缓存存在的意义:为了弥补内存访问速度过慢与CPU执行速度快之间的差异而引入。对于数组来说,存储空间是连续的,所以在加载某个下标的时候可以把以后的几个下标元素也加载到CPU缓存这样执行速度会 快于存储空间不连续的链表存储。
- 链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区 别。如果我们用ArrayList存储了了1GB大小的数据,这个时候已经没有空闲空间了,当我们再插入数据 的时候,ArrayList会申请一个1.5GB大小的存储空间,并且把原来那1GB的数据拷⻉到新申请的空间上。听起来是不是就很耗时?
实现LRU缓存淘汰算法
- 维护一个单向链表,越靠近尾节点越是远古且不常用
- 插入数据时:
- 缓存已满,直接删除尾节点,将新数据插入头节点
- 缓存不满:
- 找到的该数据,删除原来的,在头节点加入
- 找不到,直接在头节点加入
课后题:如何判断一个字符串是否是回文字符串的问题,我想你应该听过,我们今天的思题目就是基于这个问题的改造版本。如果字符串是通过单链表来存储的,那该如何来判断是一个回文串呢?你有什么好的解决思路呢?相应的时间空间复杂度又是多少呢?【LeetCode 234 回文链表】
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (head == NULL || head->next == NULL) {
return true;
}
ListNode *prev = NULL;
ListNode *slow = head;
ListNode *fast = head;
while (fast != NULL && fast->next != NULL) {
//pre:前一个
//slow:慢指针&&后一段链表的开头
//fast:快指针,边界工具人
//操作就是要把慢指针经过的逆置过来,所以让pre作为前一个,slow作为当前工具人,使用next去记录下一个,就是slow的下一位
//变换时,先把前面的逆过来,slow再往下走
fast = fast->next->next;
ListNode *next = slow->next;
slow->next = prev;
prev = slow;
slow = next;
}
if (fast != NULL) {
//!=NULL的这个情况就是奇数的情况,此时,slow工具人站在中间,pre就位,因此要让slow往前一个
slow = slow->next;
}
while (slow != NULL) {
if (slow->val != prev->val) {
return false;
}
slow = slow->next;
prev = prev->next;
}
return true;
}
};
- 先通过快慢指针找到中点,此时链表分为两部分,把前半部分逆置,然后比较两列
07讲链表(下):如何轻松写出正确的链表代码
链表六技
技巧一:理解指针或引用的含义
- 指针的作用就在于存储对象的内存地址
- 将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
- p->next=q:p结点中的next指针存储了q结点的内存地址
- p->next=p->next->next:p结点的next指针存储了p 结点的下下一个结点的内存地址。
技巧二:警惕指针丢失和内存泄漏
- 这一点其实就是由于链表单向性的特征,所以我们需要,我们一旦改变某一个节点的next指向,就会丢失掉原来那个
- 插入结点时,一定要注意操作的顺序
技巧三:利用哨兵简化实现难度
- 针对链表的插入,删除操作,需要对插入第一个节点和删除最后一个节点的情况进行特殊处理
- 一个使用哨兵的例子:
int find(char* a, int n, char key) {
if(a == null || n <= 0) {
return -1;
}
int i = 0;
while (i < n) {
if (a[i] == key) {
return i;
}
++i;
}
return -1;
}
int find(char* a, int n, char key) {
if(a == null || n <= 0) {
return -1;
}
if (a[n-1] == key) {
return n-1;
}
a[n-1] = key;
int i = 0;
while (a[i] != key) {
++i;
}
a[n-1] = tmp;
if (i == n-1) {
return -1;
} else {