【Java 数据结构】双向链表
创始人
2024-01-23 05:39:10
0
篮球哥温馨提示:编程的同时不要忘记锻炼哦!

圆圆的脑袋,大大耳朵,天天敲代码,找找找bug 


目录 

1、什么是双向链表

2、实现一个双向链表

2.1 实现前的约定

2.2 addFirst 方法

2.3 addLast 方法

2.4 addIndex 方法

2.5 contains 方法

2.6 removeAllKey 方法

2.7 clear 方法

2.8 size 方法

3、LinkedList 的学习 

3.1 认识下 LinkedList

​3.2 LinkedList 的构造方法 

3.3 LinkedList 的遍历

4、ArrayList 和 LinkedList 的区别


1、什么是双向链表

上期我们实现了一下单链表,在Java(1.8)中,链表为 LinkedList,而底层是一个双向链表,跟 ArrayList 一样,LinkedList 也实现了 List 接口,这里我们画一个图,让大家简单见识下双向链表:

如图我们可以看出,双向链表最少有三个域,分别是数据域和两个指针域,分别指向节点的前驱和后继,第一个节点没有前驱,最后一个节点没有后继。  


2、实现一个双向链表

2.1 实现前的约定

链表的每个元素是一个节点,我们仍然采用内部类的方式,既然是双向的,那么我们还需要在外部定义一个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(); //得到链表的长度

2.2 addFirst 方法

//头插法
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哦!

2.3 addLast 方法

//尾插法
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++;
}

与头插法相差不多,无非就是需要修改尾节点的引用,以及注意新节点的指针域指向问题,这里小伙伴们可以结合我的代码注释,尝试去理解,配合画图,相信你就能掌握好头插法和尾插法了!

2.4 addIndex 方法

//任意位置插入,第一个数据节点为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; //新节点的后一个为curcur.prev.next = newNode; //cur的前节点的后一个为新节点cur.prev = newNode; //cur的前节点为新节点return true;
}

对于在指定位置插入节点来说,如果给定的 index 位置大于我们的有效节点个数呢?也就是说假设我链表只有 5 个节点,你要在 8 位置插入元素显然是不合法的,其次,如果要在负数的位置插入那更不合法,所以我们要对 index 做判断,往后走我们还可以考虑两个点,如果index == 0 或者 index == size(),那么也就是对应着我们的头插和尾插,那么我们直接调用前面写的头插尾插方法即可,代码接着往后走,剩下的就是中间插入节点的情况了,逻辑很简单,首先要找到 index 对应的节点,接着改变相关节点指针域的指向即可,这里可以结合着代码以及注释,下来画图进行分析。

2.5 contains 方法

//查找是否包含关键字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 咯!

2.6 removeAllKey 方法

//删除所有值为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的后节点的前节点指针域改为nullthis.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 的节点,这道题思想不难,还是遍历链表嘛,如果值一样,修改相关节点引用即可,但是问题来了,删除头节点和尾节点,和删除中间节点的修改指向逻辑可不一样,所以我们要分别处理,分开处理这三种情况之后,如果只有一个节点的情况呢?也得处理,于是就有了我们上面的代码,当然可以有很多种写法,你只要把各种情况捋清楚了就好,至于修改指针域指向的逻辑画画图就能理解了!

2.7 clear 方法

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,不然会出问题的!\( ̄︶ ̄*\))

2.8 size 方法

这个方法还是很很简单的,直接 return this.size; 不就可以了吗?


3、LinkedList 的学习 

3.1 认识下 LinkedList 

  • LinkedList 并没有像 ArrayList 一样实现 RandomAccess 接口,所以 LinkedList 并不支持随机访问
  • LinkedList 实现了Cloneable接口,表明 LinkedList 是可以clone的
  • LinkedList 实现了Serializable接口,表明 LinkedList 是支持序列化的 
  • LinkedList 在任意位置插入和删除时效率比较高,时间复杂度为O(1)

接着来看一看 LinkedList 里面的成员变量:

3.2 LinkedList 的构造方法 

Java 中的 LinkedList 提供了两个构造方法:

方法解释说明
LinkedList()无参构造
LinkedList(Collection c)使用其他集合容器中元素进行构造

使用构造方法例子:

public static void main(String[] args) {// 构造一个空的双向链表LinkedList list1 = new LinkedList<>(); // 直接构造List list2 = new LinkedList<>(); // 向上转型// 使用ArrayList构造LinkedListArrayList arrayList = new ArrayList<>();arrayList.add(1);arrayList.add(2);arrayList.add(3);LinkedList list3 = new LinkedList<>(arrayList);List list4 = new LinkedList<>(arrayList);
}

至于LinkedList当中还有特别多的方法,小伙伴们下去可以自行查阅手册,这里就不多讲述了! 

3.3 LinkedList 的遍历

public static void main(String[] args) {LinkedList 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 it = list.listIterator();while (it.hasNext()) {System.out.print(it.next() + " ");}System.out.println();// 方法3:使用迭代器遍历->反向遍历ListIterator rit = list.listIterator(list.size());while (rit.hasPrevious()) {System.out.print(rit.previous() + " ");}System.out.println();
}

迭代器后期也会逐步接触到,这里能看得懂就ok了!


4、ArrayList 和 LinkedList 的区别

首先我们来说它们的相同点,都是Java中的集合,都是顺序结构,都实现了 List 接口等其他的接口。

重点是他们的区别,也就是不同点!

  • 从存储的角度来说,ArrayList 一定是空间连续的,因为底层是数组,数组是一块连续的存储空间,而 LinkedList 的空间不一定连续,每个节点是依靠节点的指针域进行连接起来的。
  • 从访问元素的角度来说,ArrayList 支持下标随机访问,而 LinkedList 并不支持,而且时间复杂度还是 O(n)。
  • 插入和删除的角度来说,ArrayList 尾插,尾删还好,其他都需要挪动元素了,效率低,时间复杂度是 O(n),而对于 LinkedList 来说,只需要修改指向即可,时间复杂度是 O(1)。
  • 从空间的角度来说,ArrayList 容量不够需要扩容,而 LinkedList 并没有扩容的概念,每次插入都会 new 一个新的节点。
  • 从应用场景的角度来说(目前的知识层面上),ArrayList 更适合频繁用到随机访问,而LinkedList 更适合频繁的插入和删除。

下期预告:【Java 数据结构】栈

相关内容

热门资讯

画虎画皮难画骨的近义词 画虎画皮难画骨的近义词有:知人知面不知心,画虎画皮难画骨[huà hǔ huà pí nán huà...
胡言乱语的近义词 胡言乱语的近义词有:一片胡言,一簧两舌,乱语胡言,信口开河,信口雌黄,口不择言,天花乱坠,奇谈怪论,...
光天化日,光天化日的意思,光... 光天化日guāng tiān huà rì [释义]充满阳光的天空;化生万物的太阳。旧时比喻太...
特意的近义词   一、【近义词】特地  二、【基本解释】  [释义](副)特地。(同音)特地。  [构成]偏正式:...
官高爵显的近义词 官高爵显的近义词有:高官显爵,官高爵显[guān gāo jué xiǎn]的意思:爵:爵位,官爵;...
切要关头的近义词 切要关头的近义词有:紧要关头,切要关头[qiè yào guān tóu]的意思:关头:关口。比喻有...
面面俱到,面面俱到的意思,面... 面面俱到miàn miàn jù dào [释义]俱:都。各方面都照顾到。也指虽然各方面都照顾...
人单势孤的近义词 人单势孤的近义词有:势单力薄,人单势孤[rén dān shì gū]的意思:人数少,力量单薄。出自...
间断的近义词   一、【近义词】  中断、中止、拆开、休止  二、【基本解释】  [释义](动)(连续的事情)中间...
乡壁虚造的近义词 乡壁虚造的近义词有:凭空捏造,无中生有,面壁虚构,乡壁虚造[xiāng bì xū zào]的意思:...
盎盂相敲的近义词 盎盂相敲的近义词有:盎盂相击,盎盂相敲[àng yú xiāng qiāo]的意思:比喻一家人争吵。...
高枕而卧的近义词 高枕而卧的近义词有:无忧无虑,高枕安卧,高枕安寝,高枕无忧,高枕而卧[gāo zhěn ér wò]...
破题儿第一遭的近义词 破题儿第一遭的近义词有:破题儿,破题儿头一遭,破题儿第一遭[pò tí ér dì yī zāo]的...
如堕烟海的近义词 如堕烟海的近义词有:如坐云雾,如堕烟雾,雾里看花,如堕烟海[rú duò yān hǎi]的意思:好...
雁杳鱼沉的近义词 雁杳鱼沉的近义词有:信断音绝,雁断鱼沉,雁逝鱼沉,雁杳鱼沉[yàn yǎo yú chén]的意思:...
汪洋大海的近义词 汪洋大海的近义词有:东洋大海,声势浩大,波澜壮阔,汪洋大海[wāng yáng dà hǎi]的意思...
明效大验的近义词 明效大验的近义词有:明验大效,明效大验[míng xiào dà yà]的意思:显著而又巨大的效验。...
难乎为继的近义词 难乎为继的近义词有:难以为继,难乎为继[nán hū wéi jì]的意思:难于继续下去。出自:清 ...
在所难免的近义词 在所难免的近义词有:在劫难逃,在所不免,在所无免,在所难免[zài suǒ nán miǎn]的意思...
恨入心髓的近义词 恨入心髓的近义词有:恨之入骨,恨之切骨,恨入骨髓,恨入心髓[hèn rù xīn suǐ]的意思:恨...