链表(Linked list)
- 用空间换时间
当内存空间充足的时候,如果我们更加追求代码的执行速度
,我们就可以选择空间复杂度相对较高
、但时间复杂度相对很低
的算法或者数据结构。
如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。
- 缓存清除
先进先出策略(FIFO)
, 最少使用策略(LFU)
, 最近最少使用策略(LRU)
1.链表的种类
单链表: 头结点 + 尾结点 + 结点 + 后继指针;
双向链表: 前向指针 + 后向指针
循环链表: 首尾相连
双向循环链表: 首尾相连 + 前向指针 + 后项指针
2.实现LRU缓存淘汰策略
- 链表实现LRU缓存淘汰策略
(1). 当访问的数据没有存储在缓存的链表中时,直接将数据插入链表表头,时间复杂度为O(1
);
(2). 当访问的数据存在于存储的链表中时,先要搜索到该结点(时间复杂度为O(n)), 然后将该数据对应的节点在该位置删除,将其插入到链表表头,总的时间复杂度为O(n
);
(3). 如果缓存被占满,则从链表尾部的数据开始清理,时间复杂度为O(1)
。
- 数组实现LRU缓存淘汰策略
方式一:首位置保存最新访问数据,末尾位置优先清理.
当访问的数据未存在于缓存的数组中时,直接将数据插入数组第一个元素位置,此时数组所有元素需要向后移动1个位置,时间复杂度为O(n);
当访问的数据存在于缓存的数组中时,查找到数据并将其插入数组的第一个位置,此时亦需移动数组元素,时间复杂度为O(n);
缓存用满时,则清理掉末尾的数据,时间复杂度为O(1)。
方式二:首位置优先清理,末尾位置保存最新访问数据.
当访问的数据未存在于缓存的数组中时,直接将数据添加进数组作为当前最有一个元素时间复杂度为O(1);
当访问的数据存在于缓存的数组中时,查找到数据并将其插入当前数组最后一个元素的位置,此时亦需移动数组元素,时间复杂度为O(n)。
缓存用满时,则清理掉数组首位置的元素,且剩余数组元素需整体前移一位,时间复杂度为O(n)。
2.快慢指针
快慢指针:是指设定两个指针,其中快的指针的移动速度是慢的指针的移动速度的两倍;
“快慢指针”方法主要用来解决两类问题,即“判断一个链表是否为循环链表”以及“寻找一个有序链表的中位数”.
寻找中位数:
int GetMedian(List *head)
{
List *fast = *slow = head;
while(fast && slow){
if(fast->next != NULL && fast->next->next == NULL) // 个人修改,这是针对偶数数列求中位数
{
return (slow->datslow->next->data)/2;
}
else if(fast->next == NULL)
{ // 个人修改,这是针对奇数数列求中位数
return (double)slow->data;
}
else
{
fast = fast->next->next;
slow = slow->next;
}
}
}
3. 快速写出链表代码
技巧一: 理解指针或引用的含义
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针;
技巧二: 警惕指针丢失和内存泄漏
插入结点时,一定要注意操作的顺序(先将要插入的结点接上它的屁股结点,然后将前结点相连
);
技巧三:利用哨兵简化实现难度
哨兵
: 解决的是边界问题,链表中针对第一个结点和最后一个结点需要特殊处理.
技巧四: 重点留意边界条件处理
经常用来检查链表代码是否正确的边界条件有这样几个:
如果链表为空时,代码是否能正常工作?
如果链表只包含一个结点时,代码是否能正常工作?
如果链表只包含两个结点时,代码是否能正常工作?
代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
技巧五:举例画图,辅助思考
技巧六:多写多练,没有捷径