【Java 数据结构】双向链表

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

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<? 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当中还有特别多的方法,小伙伴们下去可以自行查阅手册,这里就不多讲述了!

3.3 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了!

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 上一篇文章已经把单链表的实现还有一些常见的写法实现了,这次我们将继续学习新的一种相似的数据结构------双向链表...
    先生zeng阅读 349评论 0 0
  • 前面两节课程主要介绍了动态数组、栈以及队列这样三种数据结构,这三种数据结构的底层都是依托于静态数组构建的,靠res...
    xkzhai阅读 420评论 0 0
  • 1. 双向链表的操作分析和实现 使用带 head 头的双向链表实现 –水浒英雄排行榜 分析 双向链表的遍历,添加,...
    21号新秀_邓肯阅读 249评论 0 1
  • 如需转载, 请咨询作者, 并且注明出处.有任何问题, 可以关注我的微博: coderwhy, 或者添加我的微信: ...
    coderwhy阅读 7,862评论 1 30
  • 上一节课主要学习了一种具有真正动态数据结构的数据结构——链表,实现了链表基本的增删改查等操作,基于链表的操作特性,...
    xkzhai阅读 357评论 0 0