本节目标:
- 认识并且能够实现一个双链表
- 认识LinkedList类并且知道如何去使用
1.双链表
概念
在数据结构中,双链表(Doubly Linked List) 是一种常见的线性数据结构,它由一系列节点组成,每个节点不仅包含数据域,还包含两个指针域,分别指向其前驱节点(前一个节点) 和后继节点(后一个节点)。这种结构相比单链表(仅含后继指针),在节点访问和操作上更加灵活,可以表示成这样:

相较于单链表,双链表多了一个前驱节点prev,它储存的是这个节点的前一个节点的地址,即它指向这个节点的前一个节点。
实现双链表
在上一节中,我们已经实现不带头不循环的单链表了,那么这里实现的是不带头不循环的双链表。以下是这个双链表包含的一些操作方法:
// 不带头双向链表实现
public class MyLinkedList {
//头插法
public void addFirst(int data){ }
//尾插法
public void addLast(int data){}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){}
//判断双链表当中是否有包含key节点
public boolean contains(int key){
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){}
//删除所有值为key的节点
public void removeAllKey(int key){}
//得到双链表的长度
public int size(){
return -1;
}
//打印双链表中的数据
public void display(){}
//清空双链表
public void clear(){}
在实现这些操作之前,先创建节点类 ListNode:
java
public class ListNode {
public int val;
public ListNode prev;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
还是老规矩,实现顺序由简到繁。
(1) 打印双链表中的数据
要求:将双链表中的数据一个个打印出来。
思路:实现方式与单链表的打印一样,通过遍历的方式,一个个去打印。
java
public class MyLinkedList {
ListNode head;
ListNode last;
//打印双链表中的数据
public void display() {
ListNode cur = this.head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
}
因为双链表中前驱结点指向该节点的前一个节点,因此它能够从后往前访问,所以用 last
来表示双链表的尾节点。
(2) 得到双链表的长度
要求:通过这个方法获得双链表的长度。
思路:定义一个变量用于计数,通过遍历的方式进行计数,最后返回这个变量。
java
//得到双链表的长度
public int size() {
ListNode cur = this.head;
int size = 0;
while (cur != null) {
size++;
cur = cur.next;
}
return size;
}
(3) 判断双链表当中是否有包含key节点
要求:判断双链表中是否有包含key的节点,如果有就返回true,否则返回false。
思路:依旧通过遍历的方式去一个个判断。
java
//判断双链表当中是否有包含key节点
public boolean contains(int key){
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
(4) 头插法
要求:在双链表的头部位置插入一个新节点。
思路:这里与单链表不一样,需要考虑两种情况:1.插入前链表是空的;2.插入前链表不为空。
即下图所示:

java
//头插法
public void addFirst(int data){
ListNode cur = new ListNode(data);
if (this.head == null) {
this.head = this.last = cur;
}else {
cur.next = this.head;
this.head.prev = cur;
this.head = cur;
}
}
进行测试:
java
public class Test {
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addFirst(1);
myLinkedList.addFirst(2);
myLinkedList.addFirst(3);
myLinkedList.display();
}
}
//运行结果
3 2 1
符合预期。
(5) 尾插法
要求:在双链表的尾部插入一个新节点。
思路:这里也分两种情况,与头插法一样。需要注意的是,在链表不为空的情况下,我们应该对尾节点last进行操作,而不是头节点head。
java
//尾插法
public void addLast(int data){
ListNode cur = new ListNode(data);
if (this.head == null) {
this.head = this.last = cur;
}else {
this.last.next = cur;
cur.prev = this.last;
this.last = cur;
}
}
进行测试:
java
public class Test {
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addLast(1);
myLinkedList.addLast(2);
myLinkedList.addLast(3);
myLinkedList.display();
}
}
//运行结果
1 2 3
符合预期。
(6) 任意位置插入,第一个数据节点为0号下标
要求:在双链表中合法的任一位置插入一个新节点,第一个节点为0号下标。
思路:可以分3种情况处理:头插、尾插和中间插入。头插与尾插已经解决了,因此现在只需要解决中间插入即可。如下图所示:

当然,在进行插入操作前,需要判断插入位置是否合法。
插入位置非法异常类:
java
public class InsertIllegalException extends RuntimeException {
public InsertIllegalException() {
}
public InsertIllegalException(String str) {
super(str);
}
}
判断插入位置是否合法:
java
//判断插入位置是否合法
private void isIllegal(int index) {
if (index < 0 || index > size()) {
throw new InsertIllegalException("插入位置不合法!");
}
}
插入操作:
java
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
try {
isIllegal(index);
int len = size();
//头插
if (index == 0) {
addFirst(data);
return;
}
//尾插
if (index == len) {
addLast(data);
return;
}
//中间插入
ListNode newN = new ListNode(data);
ListNode cur = this.head;
while (index != 0) {
cur = cur.next;
index--;
}
newN.next = cur;
cur.prev.next = newN;
newN.prev = cur.prev;
cur.prev = newN;
}catch (InsertIllegalException e) {
e.printStackTrace();
}
}
进行测试:
java
public class Test {
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addLast(1);
myLinkedList.addLast(2);
myLinkedList.addLast(3);
myLinkedList.display();
myLinkedList.addIndex(1,100);
myLinkedList.display();
}
}
//运行结果
1 2 3
1 100 2 3
符合预期。
(7) 删除第一次出现关键字为key的节点
要求:将双链表中第一次出现包含key的节点删掉。
思路:这里要分为三种情况:key在链表头部、key在链表尾部和key在链表中间,但在开始前需要判断链表是否为空。分析如下图所示:

java
//删除第一次出现关键字为key的节点
public void remove(int key){
if (this.head == null) {
System.out.println("链表为空!");
return;
}
ListNode cur = this.head;
boolean mark = false; //用于标记链表中是否有key,有的话置为true
while (cur != null) {
//先找key
if (cur.val == key) {
mark = true;
//当cur位于头节点
if (cur == this.head) {
this.head = this.head.next;
this.head.prev = null;
}else {
//位于中间或者尾节点
cur.prev.next = cur.next;
//位于尾节点
if (cur.next == null) {
this.last = this.last.prev;
}else {
//位于中间
cur.next.prev = cur.prev;
}
}
//删除完成!
return;
}
cur = cur.next;
}
if (mark == false) {
System.out.println("链表中没有" + key);
}
}
但是这里还有一个不足的地方,就是当链表中仅有一个节点时(即head == last且该节点的值为key),这时候 head.prev就会引发空指针异常,所以要对key在头节点的情况进行补充处理:
java
//删除第一次出现关键字为key的节点
public void remove(int key){
if (this.head == null) {
System.out.println("链表为空!");
return;
}
ListNode cur = this.head;
boolean mark = false; //用于标记链表中是否有key,有的话置为true
while (cur != null) {
//先找key
if (cur.val == key) {
mark = true;
//当cur位于头节点
if (cur == this.head) {
this.head = this.head.next;
//当删除后链表不为空,即链表不仅仅有一个节点
if (this.head != null) {
this.head.prev = null;
}else {
//删除后链表为空,即链表仅有一个节点,这里更新尾节点last
this.last = null;
}
}else {
//位于中间或者尾节点
cur.prev.next = cur.next;
//位于尾节点
if (cur.next == null) {
this.last = this.last.prev;
}else {
//位于中间
cur.next.prev = cur.prev;
}
}
//删除完成!
return;
}
cur = cur.next;
}
if (mark == false) {
System.out.println("链表中没有" + key);
}
}
进行测试:
java
public class Test {
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addLast(1);
myLinkedList.addLast(2);
myLinkedList.addLast(3);
myLinkedList.addLast(1);
myLinkedList.display();
myLinkedList.remove(1);
myLinkedList.display();
}
}
//运行结果:
1 2 3 1
2 3 1
符合预期。
(8) 删除所有值为key的节点
要求:将链表中所有值为key的节点都删掉。
思路:只需要在删除第一次出现关键字为key的节点的方法的基础上进行修改即可,把return去掉,使得循环继续进行,继续找key的节点,继续删除,直到遍历结束,此时已将所有值为key的节点删除。
java
//删除所有值为key的节点
public void removeAllKey(int key) {
if (this.head == null) {
System.out.println("链表为空!");
return;
}
ListNode cur = this.head;
boolean mark = false; //用于标记链表中是否有key,有的话置为true
while (cur != null) {
//先找key
if (cur.val == key) {
mark = true;
//当cur位于头节点
if (cur == this.head) {
this.head = this.head.next;
//当删除后链表不为空,即链表不仅仅有一个节点
if (this.head != null) {
this.head.prev = null;
} else {
//删除后链表为空,即链表仅有一个节点,这里更新尾节点last
this.last = null;
}
} else {
//位于中间或者尾节点
cur.prev.next = cur.next;
//位于尾节点
if (cur.next == null) {
this.last = this.last.prev;
} else {
//位于中间
cur.next.prev = cur.prev;
}
}
}
cur = cur.next;
}
if (mark == false) {
System.out.println("链表中没有" + key);
}
}
不过这里可能会有一个潜在的问题,就是当我们删除某一个节点,cur指向的节点已经被移除,此时执行cur = cur.next可能会出现问题。举个例子:
现在有一个双链表:
2 <-> 3 <-> 2 <-> 4
现在要删掉所有的2
1.首先,cur指向第一个2,删除后head变为3节点;
2.接着执行cur = cur.next时,此时的cur仍然指向已经被删除的第一个 2 节点;
3.这个节点的
next
虽然可能还指向 3,但这是不安全的(已删除节点的引用应该被视为无效)
因此出于安全考虑,可以用一个变量记录cur的=指向的下一个节点。
java
//删除所有值为key的节点
public void removeAllKey(int key) {
if (this.head == null) {
System.out.println("链表为空!");
return;
}
ListNode cur = this.head;
boolean mark = false; //用于标记链表中是否有key,有的话置为true
while (cur != null) {
ListNode curN = cur.next;
//先找key
if (cur.val == key) {
mark = true;
//当cur位于头节点
if (cur == this.head) {
this.head = this.head.next;
//当删除后链表不为空,即链表不仅仅有一个节点
if (this.head != null) {
this.head.prev = null;
} else {
//删除后链表为空,即链表仅有一个节点,这里更新尾节点last
this.last = null;
}
} else {
//位于中间或者尾节点
cur.prev.next = cur.next;
//位于尾节点
if (cur.next == null) {
this.last = this.last.prev;
} else {
//位于中间
cur.next.prev = cur.prev;
}
}
}
cur = curN;
}
if (mark == false) {
System.out.println("链表中没有" + key);
}
}
进行测试:
java
public class Test {
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addLast(2);
myLinkedList.addLast(1);
myLinkedList.addLast(2);
myLinkedList.addLast(3);
myLinkedList.addLast(2);
myLinkedList.display();
myLinkedList.removeAllKey(2);
myLinkedList.display();
}
}
//运行结果:
2 1 2 3 2
1 3
符合预期。
(9) 清空双链表
要求:将双链表清空。
思路:粗暴的方式就不介绍了(直接将head和last置空),温柔的方式:通过遍历,一个个将节点的prev和next置空,最后将head和last置空。
java
//清空双链表
public void clear() {
ListNode cur = this.head;
while (cur != null) {
ListNode curN = cur.next;
cur.prev = null;
cur.next = null;
cur = curN;
}
this.head = null;
this.last = null;
}
进行测试:
java
public class Test {
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addLast(2);
myLinkedList.addLast(1);
myLinkedList.addLast(2);
myLinkedList.addLast(3);
myLinkedList.addLast(2);
myLinkedList.display();
myLinkedList.clear();
myLinkedList.display();
}
}
//运行结果:
2 1 2 3 2
符合预期。
到此,我们就成功实现了一个不带头不循环的双链表。
完整代码
java
public class MyLinkedList {
ListNode head;
ListNode last;
//打印双链表中的数据
public void display() {
ListNode cur = this.head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//得到双链表的长度
public int size() {
ListNode cur = this.head;
int size = 0;
while (cur != null) {
size++;
cur = cur.next;
}
return size;
}
//判断双链表当中是否有包含key节点
public boolean contains(int key){
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
//头插法
public void addFirst(int data){
ListNode cur = new ListNode(data);
if (this.head == null) {
this.head = this.last = cur;
}else {
cur.next = this.head;
this.head.prev = cur;
this.head = cur;
}
}
//尾插法
public void addLast(int data){
ListNode cur = new ListNode(data);
if (this.head == null) {
this.head = this.last = cur;
}else {
this.last.next = cur;
cur.prev = this.last;
this.last = cur;
}
}
//判断插入位置是否合法
private void isIllegal(int index) {
if (index < 0 || index > size()) {
throw new InsertIllegalException("插入位置不合法!");
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
try {
isIllegal(index);
int len = size();
//头插
if (index == 0) {
addFirst(data);
return;
}
//尾插
if (index == len) {
addLast(data);
return;
}
//中间插入
ListNode newN = new ListNode(data);
ListNode cur = this.head;
while (index != 0) {
cur = cur.next;
index--;
}
newN.next = cur;
cur.prev.next = newN;
newN.prev = cur.prev;
cur.prev = newN;
}catch (InsertIllegalException e) {
e.printStackTrace();
}
}
//删除第一次出现关键字为key的节点
public void remove(int key){
if (this.head == null) {
System.out.println("链表为空!");
return;
}
ListNode cur = this.head;
boolean mark = false; //用于标记链表中是否有key,有的话置为true
while (cur != null) {
//先找key
if (cur.val == key) {
mark = true;
//当cur位于头节点
if (cur == this.head) {
this.head = this.head.next;
//当删除后链表不为空,即链表不仅仅有一个节点
if (this.head != null) {
this.head.prev = null;
}else {
//删除后链表为空,即链表仅有一个节点,这里更新尾节点last
this.last = null;
}
}else {
//位于中间或者尾节点
cur.prev.next = cur.next;
//位于尾节点
if (cur.next == null) {
this.last = this.last.prev;
}else {
//位于中间
cur.next.prev = cur.prev;
}
}
//删除完成!
return;
}
cur = cur.next;
}
if (mark == false) {
System.out.println("链表中没有" + key);
}
}
//删除所有值为key的节点
public void removeAllKey(int key) {
if (this.head == null) {
System.out.println("链表为空!");
return;
}
ListNode cur = this.head;
boolean mark = false; //用于标记链表中是否有key,有的话置为true
while (cur != null) {
ListNode curN = cur.next;
//先找key
if (cur.val == key) {
mark = true;
//当cur位于头节点
if (cur == this.head) {
this.head = this.head.next;
//当删除后链表不为空,即链表不仅仅有一个节点
if (this.head != null) {
this.head.prev = null;
} else {
//删除后链表为空,即链表仅有一个节点,这里更新尾节点last
this.last = null;
}
} else {
//位于中间或者尾节点
cur.prev.next = cur.next;
//位于尾节点
if (cur.next == null) {
this.last = this.last.prev;
} else {
//位于中间
cur.next.prev = cur.prev;
}
}
}
cur = curN;
}
if (mark == false) {
System.out.println("链表中没有" + key);
}
}
//清空双链表
public void clear() {
ListNode cur = this.head;
while (cur != null) {
ListNode curN = cur.next;
cur.prev = null;
cur.next = null;
cur = curN;
}
this.head = null;
this.last = null;
}
}
2.LinkedList
2.1 概念
LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
在集合框架中,LinkedList也实现了List接口,具体如下:

【说明】
- LinkedList实现了List接口
- LinkedList的底层使用了双向链表
- LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
- LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
- LinkedList比较适合任意位置插入的场景
2.2 使用
构造方法
LinkedList的构造方法:
方法 | 解释 |
---|---|
LinkedList() | 无参构造 |
public LinkedList(Collection <? extends E> c) | 使用其他集合容器中元素构造List |
举例:
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class Test {
public static void main(String[] args) {
//使用第一种构造方法,构造一个空的LinkedList
LinkedList<Integer> linkedList = new LinkedList<>();
//List<Integer> linkedList = new LinkedList<>();这种写法等于上一条语句,
//因此LinkedList实现了List接口,因此可以用List去接收
//可以插入数据
linkedList.addFirst(1);
linkedList.addFirst(2);
linkedList.addFirst(3);
System.out.println(linkedList);
System.out.println("=============");
//使用第二种构造方法
List<Integer> list = new ArrayList<>(); //创建一个ArrayList,并往里面插入一些数据
list.add(1);
list.add(2);
list.add(3);
System.out.println(list);
System.out.println("=============");
//可以ArrayList构造LinkedList
List<Integer> list1 = new LinkedList<>(list);
System.out.println(list1);
}
}
//运行结果:
[3, 2, 1]
=============
[1, 2, 3]
=============
[1, 2, 3]
其他常用方法
方法 | 解释 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection <? extends E> c) | 尾插 c 中的元素 |
E remove(int index) | 删除 index 位置元素 |
boolean remove(Object o) | 删除遇到的第一个 o |
E get(int index) | 获取下标 index 位置元素 |
E set(int index, E element) | 将下标 index 位置元素设置为 element |
void clear() | 清空 |
boolean contains(Object o) | 判断 o 是否在线性表中 |
int indexOf(Object o) | 返回第一个 o 所在下标 |
int lastIndexOf(Object o) | 返回最后一个 o 的下标 |
List subList(int fromIndex, int toIndex) | 截取部分 list |
LinkedList的遍历
LinkedList的遍历与ArrayList的遍历一样,有三种方法,分别是通过for遍历、通过for-each遍历和通过迭代器遍历。具体请看:
2.3 ArrayList与LinkedList的区别
不同点 | ArrayList | LinkedList |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持:O(1) | 不支持:O(N) |
头插 | 需要搬移元素,效率低O(N) | 只需修改引用的指向,时间复杂度为O(1) |
插入 | 空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
到此,双链表与LinkedList的内容完结。感谢您的观看,如有不对的地方还请指出,谢谢!