数据结构 双链表与LinkedList
本节目标:
- 认识并且能够实现一个双链表
- 认识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:
public class ListNode {public int val;public ListNode prev;public ListNode next;public ListNode(int val) {this.val = val;}
}
还是老规矩,实现顺序由简到繁。
(1) 打印双链表中的数据
要求:将双链表中的数据一个个打印出来。
思路:实现方式与单链表的打印一样,通过遍历的方式,一个个去打印。
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) 得到双链表的长度
要求:通过这个方法获得双链表的长度。
思路:定义一个变量用于计数,通过遍历的方式进行计数,最后返回这个变量。
//得到双链表的长度public int size() {ListNode cur = this.head;int size = 0; while (cur != null) {size++;cur = cur.next;}return size;}
(3) 判断双链表当中是否有包含key节点
要求:判断双链表中是否有包含key的节点,如果有就返回true,否则返回false。
思路:依旧通过遍历的方式去一个个判断。
//判断双链表当中是否有包含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.插入前链表不为空。
即下图所示:
//头插法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 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。
//尾插法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;}}
进行测试:
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种情况处理:头插、尾插和中间插入。头插与尾插已经解决了,因此现在只需要解决中间插入即可。如下图所示:
当然,在进行插入操作前,需要判断插入位置是否合法。
插入位置非法异常类:
public class InsertIllegalException extends RuntimeException {public InsertIllegalException() {}public InsertIllegalException(String str) {super(str);}
}
判断插入位置是否合法:
//判断插入位置是否合法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();}}
进行测试:
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在链表中间,但在开始前需要判断链表是否为空。分析如下图所示:
//删除第一次出现关键字为key的节点public void remove(int key){if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false; //用于标记链表中是否有key,有的话置为truewhile (cur != null) {//先找keyif (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在头节点的情况进行补充处理:
//删除第一次出现关键字为key的节点public void remove(int key){if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false; //用于标记链表中是否有key,有的话置为truewhile (cur != null) {//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;}else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.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);}}
进行测试:
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的节点删除。
//删除所有值为key的节点public void removeAllKey(int key) {if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false; //用于标记链表中是否有key,有的话置为truewhile (cur != null) {//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;} else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.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的=指向的下一个节点。
//删除所有值为key的节点public void removeAllKey(int key) {if (this.head == null) {System.out.println("链表为空!");return;}ListNode cur = this.head;boolean mark = false; //用于标记链表中是否有key,有的话置为truewhile (cur != null) {ListNode curN = cur.next;//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;} else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.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 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置空。
//清空双链表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;}
进行测试:
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
符合预期。
到此,我们就成功实现了一个不带头不循环的双链表。
完整代码
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,有的话置为truewhile (cur != null) {//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;}else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.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,有的话置为truewhile (cur != null) {ListNode curN = cur.next;//先找keyif (cur.val == key) {mark = true;//当cur位于头节点if (cur == this.head) {this.head = this.head.next;//当删除后链表不为空,即链表不仅仅有一个节点if (this.head != null) {this.head.prev = null;} else {//删除后链表为空,即链表仅有一个节点,这里更新尾节点lastthis.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 |
举例:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;public class Test {public static void main(String[] args) {//使用第一种构造方法,构造一个空的LinkedListLinkedList<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构造LinkedListList<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遍历和通过迭代器遍历。具体请看:
数据结构 ArrayList与顺序表-CSDN博客
2.3 ArrayList与LinkedList的区别
不同点 | ArrayList | LinkedList |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持:O(1) | 不支持:O(N) |
头插 | 需要搬移元素,效率低O(N) | 只需修改引用的指向,时间复杂度为O(1) |
插入 | 空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
到此,双链表与LinkedList的内容完结。感谢您的观看,如有不对的地方还请指出,谢谢!