赞
踩
目录
上期我们实现了一下单链表,在Java(1.8)中,链表为 LinkedList,而底层是一个双向链表,跟 ArrayList 一样,LinkedList 也实现了 List 接口,这里我们画一个图,让大家简单见识下双向链表:
如图我们可以看出,双向链表最少有三个域,分别是数据域和两个指针域,分别指向节点的前驱和后继,第一个节点没有前驱,最后一个节点没有后继。
链表的每个元素是一个节点,我们仍然采用内部类的方式,既然是双向的,那么我们还需要在外部定义一个head和last,分别为头节点和尾节点的引用。
- public class MyLinkedList {
-
- private class ListNode {
- private int val; //数据域
- private ListNode prev; //前指针域
- private ListNode next; //后指针域
-
- private ListNode(int val) {
- this.val = val;
- }
- }
- private ListNode head; //头节点引用
- private ListNode last; //尾节点引用
- private int size; //链表有效数据个数
- }
同时我们还要实现以下几个方法:
- public void addFirst(int data); //头插法
-
- public void addLast(int data); //尾插法
-
- public boolean addIndex(int index,int data) //任意位置插入,第一个数据节点为0号下标
-
- public boolean contains(int key); //查找是否包含关键字key是否在单链表当中
-
- public void removeAllKey(int key); //删除所有值为key的节点
-
- public void clear(); //清空链表
-
- public int size(); //得到链表的长度
- //头插法
- public void addFirst(int data) {
- ListNode newNode = new ListNode(data);
- // 链表为空的情况
- if (this.head == null) {
- this.head = newNode;
- this.last = newNode;
- this.size++;
- return;
- }
- newNode.next = this.head; //新节点的后一个为头节点
- this.head.prev = newNode; //头节点的前一个为新节点
- this.head = newNode; //新节点成为新的头节点
- this.size++;
- }
与单链表不同,由于双向链表有头尾节点引用,所以这里我们要在第一次插入元素的时候进行特殊处理,当第一次插入元素我们需要将头尾节点的引用都指向这个节点,后续插入只需要改变头节点的引用即可,最后插入完成别忘记链表有效节点个数自增1哦!
- //尾插法
- public void addLast(int data) {
- ListNode newNode = new ListNode(data);
- // 链表为空的情况
- if (this.head == null) {
- this.head = newNode;
- this.last = newNode;
- this.size++;
- return;
- }
- newNode.prev = this.last; //新节点的前一个为尾节点
- this.last.next = newNode; //尾节点的后一个为新节点
- this.last = newNode; //新节点成为新的尾节点
- this.size++;
- }
与头插法相差不多,无非就是需要修改尾节点的引用,以及注意新节点的指针域指向问题,这里小伙伴们可以结合我的代码注释,尝试去理解,配合画图,相信你就能掌握好头插法和尾插法了!
- //任意位置插入,第一个数据节点为0号下标
- public boolean addIndex(int index,int data) {
- // 判断index下标的合法性
- if (index < 0 || index > this.size) {
- return false;
- }
- // index为0表示头插
- if (index == 0) {
- addFirst(data);
- return true;
- }
- // index为size长度表示尾插
- if (index == this.size) {
- addLast(data);
- return true;
- }
-
- //其他情况为中间插入
- ListNode newNode = new ListNode(data);
- ListNode cur = this.head;
- while (index != 0) {
- cur = cur.next;
- index--;
- }
- newNode.prev = cur.prev; //新节点的前一个为cur的前一个
- newNode.next = cur; //新节点的后一个为cur
- cur.prev.next = newNode; //cur的前节点的后一个为新节点
- cur.prev = newNode; //cur的前节点为新节点
- this.size++;
- return true;
- }
对于在指定位置插入节点来说,如果给定的 index 位置大于我们的有效节点个数呢?也就是说假设我链表只有 5 个节点,你要在 8 位置插入元素显然是不合法的,其次,如果要在负数的位置插入那更不合法,所以我们要对 index 做判断,往后走我们还可以考虑两个点,如果index == 0 或者 index == size(),那么也就是对应着我们的头插和尾插,那么我们直接调用前面写的头插尾插方法即可,代码接着往后走,剩下的就是中间插入节点的情况了,逻辑很简单,首先要找到 index 对应的节点,接着改变相关节点指针域的指向即可,这里可以结合着代码以及注释,下来画图进行分析。
- //查找是否包含关键字key是否在双链表当中
- public boolean contains(int key) {
- ListNode cur = this.head;
- while (cur != null) {
- if (cur.val == key) {
- return true;
- }
- cur = cur.next;
- }
- return false;
- }
这个方法跟我们之前写单链表的时候相差无几,相信有了前面单链表的基础,这个简直是信手拈来了吧,无非就是遍历这个链表,只要 cur 没有遍历到 null,也就是没有到最后一个节点的 next 位置,我们就遍历找有没有 key,找到了返回 true 找不到返回 false 咯!
- //删除所有值为key的节点
- public void removeAllKey(int key) {
- ListNode cur = this.head;
- while (cur != null) {
- if (cur.val == key) {
- //如果被删除cur是头节点
- if (cur == this.head) {
- //只有一个节点的情况
- if (this.head.next == null) {
- this.head = null;
- this.last = null;
- } else {
- cur.next.prev = null; //cur的后节点的前节点指针域改为null
- this.head = cur.next; //头节点变成cur的下一个节点
- }
- } else {
- //如果被删除cur是尾节点
- if (cur == this.last) {
- this.last = cur.prev; //尾节点变成cur的前节点
- } else {
- cur.next.prev = cur.prev; //cur的后节点的前节点指针域改为cur的前节点
- }
- cur.prev.next = cur.next; //cur的前节点的后节点指针域改为cur的后节点
- }
- this.size--;
- }
- cur = cur.next;
- }
- }
要删除所有值为 key 的节点,这道题思想不难,还是遍历链表嘛,如果值一样,修改相关节点引用即可,但是问题来了,删除头节点和尾节点,和删除中间节点的修改指向逻辑可不一样,所以我们要分别处理,分开处理这三种情况之后,如果只有一个节点的情况呢?也得处理,于是就有了我们上面的代码,当然可以有很多种写法,你只要把各种情况捋清楚了就好,至于修改指针域指向的逻辑画画图就能理解了!
- public void clear() {
- // 遍历链表
- ListNode cur = this.head;
- while (cur != null) {
- ListNode curNext = cur.next;
- cur.prev = null;
- cur.next = null;
- cur = curNext;
- }
- this.head = null;
- this.last = null;
- this.size = 0;
- }
双向链表的清空方法可不能直接头节点置空,因为直接头节点置空的话,别忘了Java中是某一块空间没有被引用的时候,才会被自动回收掉,但是这是双向链表,中间节点都是互相引用的,所以我们需要每个都手动置空,我们要定义一个curNext引用,指向cur的下一个,防止置空cur的指针域的时候,cur找不到下一个节点了!最后别忘了 size 要等于 0,不然会出问题的!
这个方法还是很很简单的,直接 return this.size; 不就可以了吗?

- LinkedList 并没有像 ArrayList 一样实现 RandomAccess 接口,所以 LinkedList 并不支持随机访问
- LinkedList 实现了Cloneable接口,表明 LinkedList 是可以clone的
- LinkedList 实现了Serializable接口,表明 LinkedList 是支持序列化的
- LinkedList 在任意位置插入和删除时效率比较高,时间复杂度为O(1)
接着来看一看 LinkedList 里面的成员变量:
3.2 LinkedList 的构造方法 Java 中的 LinkedList 提供了两个构造方法:
| 方法 | 解释说明 |
|---|---|
| LinkedList() | 无参构造 |
| LinkedList(Collection<? extends E> c) | 使用其他集合容器中元素进行构造 |
使用构造方法例子:
- public static void main(String[] args) {
- // 构造一个空的双向链表
- LinkedList<Integer> list1 = new LinkedList<>(); // 直接构造
- List<Integer> list2 = new LinkedList<>(); // 向上转型
-
- // 使用ArrayList构造LinkedList
- ArrayList<Integer> arrayList = new ArrayList<>();
- arrayList.add(1);
- arrayList.add(2);
- arrayList.add(3);
- LinkedList<Integer> list3 = new LinkedList<>(arrayList);
- List<Integer> list4 = new LinkedList<>(arrayList);
- }
至于LinkedList当中还有特别多的方法,小伙伴们下去可以自行查阅手册,这里就不多讲述了!
- public static void main(String[] args) {
- LinkedList<Integer> list = new LinkedList<>();
- for (int i = 1; i <= 5; i++) {
- list.add(i); //add默认是尾插
- }
- // 方法1:使用foreach遍历
- for (int x : list) {
- System.out.print(x + " ");
- }
- System.out.println();
-
- // 方法2:使用迭代器遍历->正向遍历
- ListIterator<Integer> it = list.listIterator();
- while (it.hasNext()) {
- System.out.print(it.next() + " ");
- }
- System.out.println();
-
- // 方法3:使用迭代器遍历->反向遍历
- ListIterator<Integer> rit = list.listIterator(list.size());
- while (rit.hasPrevious()) {
- System.out.print(rit.previous() + " ");
- }
- System.out.println();
- }
迭代器后期也会逐步接触到,这里能看得懂就ok了!
首先我们来说它们的相同点,都是Java中的集合,都是顺序结构,都实现了 List 接口等其他的接口。
重点是他们的区别,也就是不同点!
下期预告:【Java 数据结构】栈
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。