策略
常见的策略有三种:先进先出策略
- FIFO ( First In , First Out )、
- 最少使用策略 LFU ( Least Frequently Used )、
- 最近最少使用策略 LRU ( Least Recently Used
链表
一、什么是链表?
- 和数组一样,链表也是一种线性表。
- 从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。
- 链表中的每一个内存块被称为节点Node。节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针 next
链表分类
- 单链表
- 循环链表
- 双向链表
- 双向循环链表

其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如图所示,我们把这个记录下个结点地址的指针叫作后继指针next。
其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点。其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址NULL,表示这是链表上最后一个结点。
空间换时间
对于执行较慢的程序,可以通过消耗更多的内存(空间换时间)来进行优化;而消耗过多内存的程序,可以通过消耗更多的时间(时间换空 间)来降低内存的消耗。
时间换空间
相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用 时间换空间的设计思路。
选择数组还是链表?
- 插入、删除和随机访问的时间复杂度 数组:插入、删除的时间复杂度是 O(n) ,随机访问的时间复杂度是 O(1) 。 链表:插入、删除的时间复杂度是 O(1) ,随机访问的时间复杂端是 O(n) 。
- 数组缺点 1 )若申请内存空间很大,比如 100M ,但若内存空间没有 100M 的连续空间时,则会申请失败,尽管内存可用空间超过 100M 。 2 )大小固定,若存储空间不足,需进行扩容,一旦扩容就要进行数据复制,而这时非常费时的。
- 链表缺点 1)内存空间消耗更大,因为需要额外的空间存储指针信息。 2 )对链表进行频繁的插入和删除操作,会导致频繁的内存申请和释放,容易造成内存碎片,如果是 Java 语言,还可能会造成频繁的 GC (自动垃圾回收器) 操作
操作链表的一些技巧
- 理解指针或引用的含义
- 不管是 “ 指针 ” 还是 “ 引用,都是存储所指对象的内存地址
- 警惕指针丢失和内存泄漏
- 丢失:注意添加时的先后顺序,避免自己指向自己,造成丢失。
- 利用哨兵简化实现难度
- 种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表
- 边界条件处理(检查和思考)
- 如果链表为空时,代码是否能正常工作?
- 如果链表只包含一个结点时,代码是否能正常工作?
- 如果链表只包含两个结点时,代码是否能正常工作?
- 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
- 画图,辅助思考
练习:
- 单链表反转
- 链表中环的检测
- 两个有序的链表合并
- 删除链表倒数第n个结点
- 求链表的中间结点
- (LeetCode 对应编号: 206 , 141 , 21 , 19 , 876)
LeetCode解答小结:
反转一个单链表。 示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
解1:
可以理解成两个栈,我们把
head当成栈顶,不断从栈顶取出放入另一个栈底,使栈顶的next指针指向当前栈,直到原栈没有数据(null)。
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode curr = head;
while(curr!=null){
ListNode tempNext = curr.next;
// 分离出一个栈顶Node节点
curr.next = pre;
// 相当于放到另一个栈的栈顶
pre = curr;
// 重新移动head指针
curr = tempNext;
}
// 返回新栈
return pre;
}
}
解2:
递归实现,
class Solution {
public ListNode reverseList(ListNode head) {
// 解2:递归实现
if(head == null || head.next == null){
return head;
}
ListNode node = reverseList(head.next);
// 关键代码,head是node的前节点,这里为什么不能用node?
// 因为这里每次返回值是node,相当于重新修改了next值,最终只剩两个[5,1]
// 而head.next是未修改时,也相当于head的后继节点
head.next.next = head;
head.next = null;
return node;
}
}
LinkedList
特点
- 允许null
- 非synchronized,非线程安全
- fail-fast特性
- Deque(double ended queue)
源码
add
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
remove
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}