链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表也是属于「线性表」的一种,它和数组最大的区别就是数据元素无需存储在一块连续的内存空间里。内存空间不连续了,那该如何判断元素之间的前后顺序呢?
链表要求每个节点除了保存数据元素本身,还需要包含一个或多个额外的指针,来指向它关联的元素,元素之间通过指针来形成链结构。
链表的分类:
- 单向链表
- 双向链表
- 单向循环链表
- 双向循环链表
如下图,是一个单向链表结构。
在Java语言中,LinkedList就是一个底层采用双向链表结构的容器。
java
public void linkedList() {
LinkedList<Integer> list = new LinkedList();
list.add(1);
list.add(2);
Integer first = list.getFirst();
Integer last = list.getLast();
}
1. 链表的特点
1.1 地址可以不连续
链表的数据元素可以存储在地址连续的存储单元里,也可以存储在地址不连续的存储单元中。这是它和数组最大的区别。
1.2 无界
数组需要一块连续的内存空间,创建时需要申明容量,定长且不支持扩容,属于有界表。
链表没有这些限制,添加元素时,直接链尾的next指针指向新元素即可,理论上,只要内存足够,链表是没有界限的,可以一直添加,即使是碎片化的内存。
1.3 插入、删除效率高
数组元素的插入和删除,需要移动元素,时间复杂度是O(n)。
链表相反,它的插入和删除效率极高,只需要修改指针的指向即可,时间复杂度是O(1)。
1.4 访问效率低
链表由于内存空间不是连续的,因此它是没有索引的概念的。尽管Java中的LinkedList依然提供了get(int index)
方法获取第index个元素,但是你可以看下它的源码实现,它依然是循环访问next来查找的,时间复杂度是O(n)。
java
// 获取第index个节点
Node<E> node(int index) {
// 如果index过半,从链尾开始查找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 否则从链头开始查找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
2. 链表的实现
使用Java语言编写一个简单的单向链表,支持add
和get
操作。
java
public class Linked<T> {
private int size;
private Node<T> first;
private Node<T> last;
public void add(T data) {
Node<T> node = new Node(data, null);
if (first == null) {
first = last = node;
} else {
last.next = node;
last = node;
}
size++;
}
public T get(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException("size:" + size + ",index:" + index);
}
Node<T> node = first;
while (index-- > 0) {
node = node.next;
}
return node.data;
}
private class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
}
3. 链表实现LRU淘汰算法
【要求】
设计一个给定容量的容器,支持add
操作,元素不可重复。支持print
操作,保证输出的元素顺序就是其访问顺序。如果元素超出最大容量,则自动淘汰掉最久未被访问的元素。
【分析】
使用一个单向链表,如果添加的元素存在,则将它移动到链头,如果不存在则插入到链头,如果链表到达存储阈值,则移除掉链尾元素。这样从链头到链尾就依次是元素的访问顺序。
【实现】
java
public class LRULinked<T> {
private int capacity;//容量
private int size;//元素数量
private Node<T> head;//链头
public LRULinked(int capacity) {
head = new Node<>(null, null);
this.capacity = capacity;
}
/**
* 向链表中添加data
* 1.如果存在则移动到链头
* 2.不存在则插入到链头
* 3.到达阈值,要淘汰掉最久的数据
* @param data
*/
public void add(T data) {
Node<T> node;
Node<T> prevNode = findPrevNode(data);
if (prevNode != null) {
// 存在,移除原节点
node = prevNode.next;
prevNode.next = prevNode.next.next;
} else {
// 不存在
node = new Node<>(data, null);
if (size == capacity) {
// 淘汰链尾
Node<T> tail = head;
while (tail.next != null) {
if (tail.next.next == null) {
tail.next = null;
break;
}
tail = tail.next;
}
}else {
size++;
}
}
node.next = head.next;
head.next = node;
}
// 找到data节点的前一个节点
private Node<T> findPrevNode(T data) {
Node<T> node = head;
while (node.next != null) {
node = node.next;
if (data == node.data || (data.hashCode() == node.hashCode() && data.equals(node.data))) {
return node;
}
}
return null;
}
// 打印链表
public void print() {
Node<T> node = head.next;
while (node != null) {
System.out.println(node.data);
node = node.next;
}
}
// 链表内部的Node节点
private class Node<T> {
private final T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
}
4. 总结
链表和数组最大的区别是存储空间无需连续,因此理论上只要内存足够,它是没有界限的。元素的插入和删除只需修改节点的指针,时间复杂度为O(1),效率极高。链表没有索引的概念,数据的访问效率低,要访问第n个元素,只能通过next不断查找,时间复杂度是O(n)。