链表通过「指针」将一组「零散的内存块」串联起来使用。所以理论上链表是可以无限大的。
链表的结构五花八门,最常用的有三种:单链表、双向链表和循环链表。
在学习链表之前先要搞清楚几个概念:结点、后继指针 next、头结点、尾结点
- 结点:链表的内存块。
- 后继结点 next:存储下一个结点的地址的指针。
- 头结点:第一个结点。
- 尾结点:最后一个结点,它的后继结点 next 不是指向下一个结点,而是 null。(只有单链表有尾结点)
单链表
与数组一样,链表也支持数据的查找、插入和删除操作。在链表中,插入和删除一个数据并不需要为了内存的连续性而搬移结点。所以,在链表中插入和删除一个数据的非常快,时间复杂度为 O(1)。但是在链表中想要随机访问第 k 个元素,就需要从头结点一个一个的依次遍历查找,时间复杂度为 O(n)。
public class SinglyLinkedList<K> {
private Node<K> mHead = null;
/**
* 表头插入
* 这种操作将输入的顺序相反,逆序
*
* @param value 要插入元素的值
*/
public void insertToHead(K value) {
Node<K> newNode = new Node<>(value, null);
insertToHead(newNode);
}
public void insertToHead(Node<K> node) {
if (mHead == null) {
mHead = node;
} else {
node.next = mHead;
mHead = node;
}
}
/**
* 顺序插入到链表尾部
*
* @param value 要插入元素的值
*/
public void insertTail(K value) {
Node<K> newNode = new Node<>(value, null);
if (mHead == null) {
mHead = newNode;
} else {
Node<K> p = mHead;
while (p.next != null) {
p = p.next;
}
p.next = newNode;
}
}
/**
* 插入到结点后面
*
* @param p 被插入的目标结点
* @param value 要插入元素的值
*/
public void insertAfter(Node<K> p, K value) {
Node<K> newNode = new Node<>(value, null);
insertAfter(p, newNode);
}
public void insertAfter(Node<K> p, Node<K> newNode) {
if (p == null) return;
// 防止 p 后面的链表丢失
newNode.next = p.next;
p.next = newNode;
}
/**
* 插入到目标结点前
*
* @param p 被插入的目标结点
* @param value 要插入元素的值
*/
public void insertBefore(Node<K> p, K value) {
Node<K> newNode = new Node<>(value, null);
insertBefore(p, newNode);
}
public void insertBefore(Node<K> p, Node<K> newNode) {
if (p == null) return;
if (mHead == p) {
insertToHead(newNode);
return;
}
Node<K> q = mHead;
// 找到目标结点的前置结点
while (q != null && q.next != p) {
q = q.next;
}
// 整个链表都找完了,还是没有找到 p 结点
if (q == null) return;
newNode.next = q.next;
q.next = newNode;
}
// 删除
public void deleteByNode(Node<K> node) {
if (mHead == null || node == null) return;
if (mHead == node) {
mHead = mHead.next;
return;
}
Node<K> p = mHead;
while (p != null && p.next != node) {
p = p.next;
}
if (p == null) return;
p.next = p.next.next;
}
public void deleteByValue(K value) {
if (mHead == null) return;
Node<K> p = mHead; // p 指向目标结点
Node<K> q = null; // q 用来保存目标结点的前置结点
while (p != null && p.data.equals(value)) {
q = p;
p = p.next;
}
if (p == null) return;
if (q == null) {
mHead = mHead.next;
} else {
q.next = q.next.next;
}
}
// 查找
public Node<K> findByValue(K value) {
Node<K> p = mHead;
while (p != null && p.data.equals(value)) {
p = p.next;
}
return p;
}
public Node<K> findByIndex(int index) {
Node<K> p = mHead;
int pos = 0;
while (p != null && pos != index) {
p = p.next;
pos++;
}
return p;
}
@Override
public String toString() {
Node<K> p = mHead;
StringBuilder sb = new StringBuilder();
while (p != null) {
sb.append(p.data);
sb.append(" -> ");
p = p.next;
}
sb.append("null");
return sb.toString();
}
public static class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> node) {
this.data = data;
next = node;
}
public T getData() {
return data;
}
}
}
循环链表
「循环链表」是一种特殊的单链表。它跟单链表唯一的区别就是,尾结点不是指向 null 而是指向头结点。
双向链表
「双向链表」是比单项链表和循环链表更加常用的数据结构。它支持两个方向,每个结点不止有后继结点 next 指向后面的结点,还有前驱结点 prev 指向前面的结点。
public class DoublyLinkedList {
private Node mHead = null;
private Node preFindNode = null;
// 查找
/**
* 查找链表中第一次出现目标值结点
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*
* @param value
* @return
*/
public Node findByValue(int value) {
if (mHead == null) return null;
Node p = mHead;
while (p != null && p.data != value) {
p = p.next;
}
return p;
}
/**
* 根据下标查找结点
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*
* @param index 需要查找的下标
* @return 返回找到的结点
*/
public Node findByIndex(int index) {
if (mHead == null) return null;
Node p = mHead;
int pos = 0;
while (p != null && pos != index) {
p = p.next;
pos++;
}
return p;
}
// 插入
/**
* 插入元素到头结点
* 一般用来逆序存储数据
* 时间复杂度 O(1)
* 空间复杂度 O(1)
*
* @param value 待插入的值
*/
public void insertToHead(int value) {
Node newNode = new Node(value, null, null);
insertToHead(newNode);
}
public void insertToHead(Node node) {
if (mHead == null) {
mHead = node;
} else {
node.next = mHead;
mHead.prev = node;
mHead = node;
}
}
/**
* 插入元素到尾结点
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*
* @param value 待插入的值
*/
public void insertTail(int value) {
Node newNode = new Node(value, null, null);
insertTail(newNode);
}
public void insertTail(Node node) {
if (mHead == null) {
mHead = node;
} else {
Node p = mHead;
while (p.next != null) {
p = p.next;
}
node.prev = p;
p.next = node;
}
}
/**
* 插入元素到目标结点的后面
* 时间复杂度 O(1)
* 空间复杂度 O(1)
*
* @param p 目标结点
* @param value 待插入的值
*/
public void insertAfter(Node p, int value) {
if (p == null || mHead == null) return;
Node newNode = new Node(value, null, null);
newNode.next = p.next;
p.next.prev = newNode;
p.next = newNode;
newNode.prev = p;
}
/**
* 插入元素到目标结点的前面
* 时间复杂度 O(1)
* 空间复杂度 O(1)
*
* @param p 目标结点
* @param value 待插入的值
*/
public void insertBefore(Node p, int value) {
if (p == null || mHead == null) return;
Node newNode = new Node(value, null, null);
Node q = p.prev; // 保存前置结点指针
newNode.next = p;
p.prev = newNode;
q.next = newNode;
newNode.prev = q;
}
// 删除
/**
* 删除第一个等于 value 的结点
* 时间复杂度 O(n),其实就是查找这个结点消耗的时间
* 空间复杂度 O(1)
*
* @param value 待删除的值
* @return 返回被删除的结点
*/
public Node deleteFirstValue(int value) {
Node p = findByValue(value);
// 没有找到这个值
if (p == null) return null;
p.prev.next = p.next;
p.next.prev = p.prev;
return p;
}
/**
* 删除所有等于 value 的结点
* 时间复杂度 O(n),要找到最后一个等于 value 的结点
* 空间复杂度 O(1)
*
* @param value 待删除的值
*/
public void deleteAllValue(int value) {
if (mHead == null) return;
Node p = mHead;
while (p != null) {
if (p.data == value) {
p.prev.next = p.next;
p.next.prev = p.prev;
}
p = p.next;
}
}
/**
* 删除目标结点
* 时间复杂度 O(1)
* 空间复杂度 O(1)
*
* @param node 待删除的结点
*/
public void deleteByNode(Node node) {
if (node == null) return;
node.prev.next = node.next;
node.next.prev = node.prev;
}
public static class Node {
private int data;
private Node next; // 后继结点
private Node prev; // 前驱结点
public Node(int data, Node next, Node prev) {
this.data = data;
this.next = next;
this.prev = prev;
}
public int getData() {
return data;
}
}
}
课后习题
如何基于链表实现 LRU 缓存淘汰算法
基于链表实现的 LRU 缓存淘汰算法
/**
* 基于单链表的 LRU 缓存
*
* @param <T>
*/
public class LRUBaseLinkedList<T> {
private static final int DEFAULT_CAPACITY = 10;
// 带头结点链表,头结点永远不存储数据
private Node<T> mHead = new Node<>(null, null);
private int mCapacity; // 链表的容量
private int mLength; // 链表的数据量
public LRUBaseLinkedList() {
mCapacity = DEFAULT_CAPACITY;
mLength = 0;
}
public LRUBaseLinkedList(int capacity) {
mCapacity = capacity;
mLength = 0;
}
/**
* 加入缓存列表
* 先检查链表是否有这个元素。
* 如果没有这个元素,再看缓存是否满了,如果没满就直接插入到头结点,如果满了就删除尾结点,再插入头结点
* 如果有这个元素,就先删除这个结点,再插入到头结点
*
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*
* @param data 加入缓存数据
*/
public void add(T data) {
// 检查链表是否有这个结点,并且把前置结点取出来
Node<T> preNode = findPreData(data);
if (preNode == null) {
if (mLength >= mCapacity) { // 缓存已经存满了,就删除尾结点
deleteLastNode();
}
insertToHead(data);
} else { // 缓存没满,就删除这个结点,再插入头结点
deleteNextNode(preNode);
insertToHead(data);
}
}
/**
* 找到目标值的前置结点
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*
* @param value 目标值
* @return 返回前置结点
*/
private Node<T> findPreData(T value) {
Node<T> p = mHead;
while (p.next != null) {
if (value.equals(p.next.data)) {
return p;
}
p = p.next;
}
return null; // 没有找到这个结点
}
private void insertToHead(T value) {
Node<T> node = new Node<>(value, null);
insertToHead(node);
}
/**
* 插入链表头
* 时间复杂度 O(1)
* 空间复杂度 O(1)
*
* @param node 待插入的元素
*/
private void insertToHead(Node<T> node) {
node.next = mHead.next;
mHead.next = node;
mLength++;
}
/**
* 删除当前结点的下一个结点
* 时间复杂度 O(1)
* 空间复杂度 O(1)
*
* @param preNode 目标结点
*/
private void deleteNextNode(Node<T> preNode) {
preNode.next = preNode.next.next;
mLength--;
}
/**
* 删除链表的最后一个结点
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*/
private void deleteLastNode() {
if (mHead.next == null) return;
Node<T> p = mHead;
// 得到倒数第二个结点
while (p.next.next != null) {
p = p.next;
}
p.next = null;
mLength--;
}
@Override
public String toString() {
Node<T> p = mHead;
StringBuilder sb = new StringBuilder();
while (p.next != null) {
sb.append(p.next.data);
sb.append(" -> ");
p = p.next;
}
sb.append("null");
return sb.toString();
}
private class Node<K> {
private K data;
private Node<K> next;
public Node(K data, Node<K> next) {
this.data = data;
this.next = next;
}
public K getData() {
return data;
}
}
public static void main(String[] args) {
LRUBaseLinkedList list = new LRUBaseLinkedList();
Scanner scanner = new Scanner(System.in);
while (true) {
list.add(scanner.nextInt());
System.out.println(list);
}
}
}
基于数组实现的 LRU 缓存淘汰算法
public class LRUBasedArray<T> {
private static final int DEFAULT_CAPACITY = 10;
private T[] mData;
private int mCapacity; // 链表的容量
private int mCount; // 链表的数据量
public LRUBasedArray() {
mCapacity = DEFAULT_CAPACITY;
mData = (T[]) new Object[mCapacity];
mCount = 0;
}
public LRUBasedArray(int capacity) {
mCapacity = capacity;
mData = (T[]) new Object[mCapacity];
mCount = 0;
}
public void add(T data) {
int index = findByValue(data);
// 缓存中没有这个元素
if (index == -1) {
if (mCount == mCapacity) {
mCount--;
}
insertToHead(data);
} else {
for (int i = index; i > 0; i--) {
mData[index] = mData[index - 1];
}
mData[0] = data;
}
}
private int findByValue(T data) {
for (int i = 0; i < mCount; i++) {
if (mData[i] == data) {
return i;
}
}
return -1;
}
private void insertToHead(T data) {
for (int i = mCount; i > 0; i--) {
mData[i] = mData[i - 1];
}
mData[0] = data;
mCount++;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < mCount - 1; i++) {
sb.append(mData[i]).append(", ");
}
sb.append(mData[mCount - 1]).append("]");
return sb.toString();
}
public static void main(String[] args) {
LRUBasedArray array = new LRUBasedArray();
Scanner scanner = new Scanner(System.in);
while (true) {
array.add(scanner.nextInt());
System.out.println(array);
}
}
}
如果字符串是通过单链表来存储的,那应该如何判断是一个回文串呢?
/**
* 判断回文
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*
* @return 是否是回文字符串
*/
public boolean palindrome() {
if (mHead == null) return false;
Node<K> fast = mHead;
Node<K> slow = mHead;
// 只有一个元素
if (slow.next == null) {
return true;
}
while (slow.next != null && fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 现在 slow 一定是中间点
if (fast.next != null) { // 偶数个
slow = slow.next;
}
// 翻转链表
Node reverse = inverseLinkList(slow);
while (slow != null && reverse != null) {
if (slow.data != reverse.data) {
return false;
}
slow = slow.next;
reverse = reverse.next;
}
return true;
}
/**
* 无头结点的链表翻转
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*
* @param p 链表的结点
* @return 返回翻转后的结点
*/
private Node<K> inverseLinkList(Node<K> p) {
Node<K> pre = null; // 保存前置结点
Node<K> reverse = mHead; // 保存翻转结点
Node<K> next = null; // 保存下一个结点
while (reverse != p) {
next = reverse.next;
reverse.next = pre;
pre = reverse;
reverse = next;
}
reverse.next = pre;
return reverse;
}