目录
一,前言
链表是一种物理上不一定连续而逻辑上连续的存储结构,数据元素的逻辑顺序通过链表节点中的引用链接次序实现。其结构非常多样,主要可分为带头/不带头、循环/不循环、单向/双向,组合排列共计八种。
本文主要讲解无头双向非循环链表,其逻辑结构如下图所示:
二,基本实现
1,接口定义
本次实现选择传递泛型,故接口类也写作泛型类。
java
public interface IDoubleLinkedList <T> {
// 头插
void addFirst(T val);
// 尾插
void addLast(T val);
// 头删
void removeFirst();
// 尾删
void removeLast();
// 在任意位置插入(节点从0开始计数)
void addIndex(int index, T val);
// 查找是否存在关键字为key的节点
boolean contains(T val);
// 删除第一次出现关键字为key的节点
void remove(T val);
// 删除所有关键字为key的节点
void removeAll(T val);
// 获取链表长度
int size();
// 清空链表
void clear();
}
2,实现类定义
创建实体类实现上述接口,并定义first、last成员变量存储头尾节点引用地址,定义size成员变量记录链表长度,定义私有静态内部类Node作为链表节点。
注:
节点内部类定义为私有访问类型可以避免在类外持有链表节点造成内存泄露
java
public class DoubleLinkedListImpl<T> implements IDoubleLinkedList<T> {
private Node<T> first;
private Node<T> last;
private int size;
/**
* 无参构造方法*/
public DoubleLinkedListImpl() {
this.first = null;
this.last = null;
this.size = 0;
}
/**
* 静态节点内部类定义*/
private static class Node<T> {
T val;
Node<T> prev;
Node<T> next;
Node(T val) {
this.val = val;
}
}
}
3,主要方法实现
<1> 头插 addFirst
头插时存在两种情况:
1,链表为空 ==> 新的节点同时作为该链表的头尾节点
2,链表不为空 ==> 新的节点插入到头节点前面,并维护first指向新的头节点引用修改逻辑如下图所示:
注:
后续所有引用修改的逻辑都是"先连接后继,再连接前驱"

java
/**
* 头插*/
@Override
public void addFirst(T val) {
// 创建节点
Node<T> node = new Node<>(val);
// 改变节点指向
if(isEmpty()) {
this.first = node;
this.last = node;
} else {
node.next = this.first;
this.first.prev = node;
this.first = node;
}
// 维护链表大小
this.size ++;
}
<2> 尾插 addLast
尾插时存在两种情况:
1,链表为空 ==> 新的节点同时作为该链表的头尾节点
2,链表不为空 ==> 新的节点插入到尾节点后面,并维护last指向新的尾节点。引用修改逻辑如下图所示:
java
/**
* 尾插*/
@Override
public void addLast(T val) {
// 创建节点
Node<T> node = new Node<>(val);
// 改变节点指向
if(isEmpty()) {
this.first = node;
this.last = node;
} else {
this.last.next = node;
node.prev = this.last;
this.last = node;
}
// 维护链表大小
this.size ++;
}
<3> 在任意位置插入 addIndex
在任意位置插入时,根据给定下标的不同,会存在以下三种情况:
1,下标不合法 ==> 抛出异常
2,下标为头尾 ==> 调用头插/尾插并返回
3,下标不为首尾 ==> 找到对应下标节点后插入新节点。引用修改逻辑如下图所示:
java
/**
* 在任意位置插入*/
@Override
public void addIndex(int index, T val) {
// 检查下标是否合法
if(index < 0 || index > this.size) {
throw new IndexIllegalException("索引下标非法,下标:" + index);
}
// 判断是否为头尾
if(index == 0) {
addFirst(val);
return;
}
if(index == this.size) {
addLast(val);
return;
}
// 新建节点
Node<T> node = new Node<>(val);
// 找到对应位置并插入
Node<T> cur = this.first;
for(int i = 0; i < index; i ++) {
cur = cur.next;
}
cur.prev.next = node;
node.prev = cur.prev;
node.next = cur;
cur.prev = node;
// 维护链表大小
this.size ++;
}
<4> 头删 removeFirst
头删时,根据链表元素个数不同,存在以下三种情况:
1,链表为空 ==> 直接返回或抛出异常均可
2,链表仅有一个元素 ==> 置空链表
3,链表存在两个及以上元素 ==> 维护first实现删除。引用修改如下图所示:
注:
由于节点类为私有内部类,无法在类外被持有,故当first和后继节点前驱引用不再指向原先第一个节点后,该节点便变为不可达,从而被GC回收,后续所有修改删除同理。

java
/**
* 头删*/
@Override
public void removeFirst() {
// 判空
if(isEmpty()) {
return;
}
// 仅有一个节点
if(this.first == this.last) {
this.first = null;
this.last = null;
this.size --;
return;
}
// 存在多个节点
Node<T> next = this.first.next;
this.first.val = null;
this.first.next = null;
// -需要维护头节点的前驱指针为null
this.first = next;
this.first.prev = null;
// 维护链表大小
this.size --;
}
<5> 尾删 removeLast
尾删时,根据链表元素个数不同,存在以下三种情况:
1,链表为空 ==> 直接返回或抛出异常均可
2,链表仅有一个元素 ==> 置空链表
3,链表存在两个及以上元素 ==> 维护last实现删除。引用修改如下图所示:
java
/**
* 尾删*/
@Override
public void removeLast() {
// 判空
if(isEmpty()) {
return;
}
// 仅有一个时
if(this.first == this.last) {
this.first = null;
this.last = null;
this.size --;
return;
}
// 存在多个时
Node<T> prev = this.last.prev;
this.last.val = null;
this.last.prev = null;
// -需要维护尾节点的后继节点为null
this.last = prev;
this.last.next = null;
// 维护链表大小
this.size --;
}
<6> 按值删除一个 remove
按值删除时,根据目标节点位置存在以下三种情况:
1,为头节点 ==> 头删
2,为尾节点 ==> 尾删
3,为中间节点 ==> 修改引用删除。引用修改逻辑如下图所示:

java
/**
* 删除第一次出现关键字为key的节点
* 没有置空被删除的索引*/
@Override
public void remove(T val) {
// 链表为空时
if(isEmpty()) {
return;
}
// 被删除元素为头节点时
if(Objects.equals(this.first.val, val)) {
// -仅有一个元素的情况
if(this.first == this.last) {
this.first = null;
this.last = null;
} else {
this.first = this.first.next;
this.first.prev = null;
}
this.size --;
return;
}
// 向后查找
Node<T> cur = this.first;
while(cur != null) {
if(Objects.equals(cur.val, val)) {
// -判断是否为尾节点
if(cur == this.last) {
this.last = this.last.prev;
this.last.next = null;
} else {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
this.size --;
return;
}
cur = cur.next;
}
}
<7> 按值删除全部 removeAll
按值删除全部与<6>同理,只不过此处我们删除后不会直接返回,而是创建循环进行删除。<6>也可采用该实现方式,只要在删除一个节点后直接返回即可。
java
/**
* 删除所有关键字为key的节点*/
@Override
public void removeAll(T val) {
Node<T> cur = this.first;
while(cur != null) {
// 保留下一个节点
Node<T> next = cur.next;
// 值相等,确认可以删除
if(Objects.equals(cur.val, val)) {
if(cur == this.first) {
// -判断是否为头节点
if(cur == this.last) {
this.first = null;
this.last = null;
} else {
this.first = this.first.next;
this.first.prev = null;
}
} else if(cur == this.last) {
// -判断是否为尾节点
this.last = this.last.prev;
this.last.next = null;
} else {
// -中间节点
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
// -清空已删除节点
cur.val = null;
cur.prev = null;
cur.next = null;
this.size --;
}
cur = next;
}
}
<8> 按值查找是否存在 contains
按插查找是否存在关键字为key的节点时,只需遍历链表即可。
java
/**
* 查找是否存在关键字为key的节点*/
@Override
public boolean contains(T val) {
// 判空
if(isEmpty()) {
return false;
}
// 查找
Node<T> cur = this.first;
while(cur != null) {
if(Objects.equals(cur.val, val)) {
return true;
}
cur = cur.next;
}
return false;
}
<9> 清空链表 clear
为了避免内存泄露,博主更建议将链表中的每个节点都置空后再置空引用,如此比较稳妥。
java
/**
* 清空链表*/
@Override
public void clear() {
// 清空所有节点
Node<T> cur = this.first;
while(cur != null) {
Node<T> next = cur.next;
cur.val = null;
cur.prev = null;
cur.next = null;
cur = next;
}
// 引用置空
this.first = null;
this.last = null;
this.size = 0;
}
三,链表与顺序表的区别及对比
主要对比如下图所示,顺序表和链表在优缺点方面大致互补。链表由于新增、删除只需要修改节点引用,故在进行头尾操作时,时间复杂度很低;而顺序表由于底层存储结构为数组,故其对指定下标的操作时间复杂度很低,具体的技术选型需要结合实际业务场景进行综合考量。