链表是基本的数据结构,尤其双向链表在应用中最为常见,LinkedList 就实现了双向链表。今天我们一起手写一个双向链表。

文中涉及的代码可访问 GitHub:https://github.com/UniqueDong/algorithms.git

上次我们说了「单向链表」的代码实现,今天带大家一起玩下双向链表,双向链表的节点比单项多了一个指针引用 「prev」。双向链表就像渣男,跟「前女友」和「现女友」,还有一个「备胎』都保持联系。前女友就像是前驱节点,现女友就是 「当前 data」,而「next」指针就像是他套住的备胎。每个 Node 节点有三个属性,类比就是 「前女友」+ 「现女友」 + 「备胎」。

使用这样的数据结构就能实现「进可攻退可守」灵活状态。

接下来让我们一起实现『渣男双向链表』。

定义Node

节点分别保存现女友、前女友、跟备胎的联系方式,这样就能够实现一三五轮换运动(往前看有前女友,往后看有备胎),通过不同指针变可以找到前女友跟备胎。就像渣男拥有她们的联系方式。


private static class Node<E> {
//现女友
E item;
// 备胎
Node<E> next;
// 前女友
Node<E> prev; public Node(Node<E> prev, E item, Node<E> next) {
this.prev = prev;
this.item = item;
this.next = next;
}
}

代码实现

定义好渣男节点后,就开始实现我们的双向链表。类似过来就是一个渣男联盟排成一列。我们还需要定义两个指针分别指向头结点和尾节点。一个带头大哥,一个收尾小弟。

public class DoubleLinkedList<E> extends AbstractList<E> implements Queue<E> {
transient int size = 0; /**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first; /**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
}

头节点添加

新的渣男进群了,把他设置成群主带头大哥。首先构建新节点,prev = null,带头大哥业务繁忙,不找前女友,所以 prev = null;next 则指向原先的 first。

  1. 如果链表是空的,则还要把尾节点也指向新创建的节点。
  2. 若果链表已近有数据,则把原先 first.prev = newNode。
    @Override
public void addFirst(E e) {
linkFirst(e);
}
/**
* 头结点添加数据
*
* @param e 数据
*/
private void linkFirst(E e) {
final Node<E> f = this.first;
Node<E> newNode = new Node<>(null, e, f);
// first 指向新节点
first = newNode;
if (Objects.isNull(f)) {
// 链表是空的
last = newNode;
} else {
// 将原 first.prev = newNode
f.prev = newNode;
}
size++;
}

尾节点添加

将新进来的成员放在尾巴。

第一步构建新节点,把 last 指向新节点。

第二步判断 last 节点是否是空,为空则说明当前链表是空,还要把 first 指向新节点。否则就需要把原 last.next 的指针指向新节点。

    @Override
public boolean add(E e) {
addLast(e);
return true;
}
private void addLast(E e) {
final Node<E> l = this.last;
Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (Objects.isNull(l)) {
// 链表为空的情况下,设置 first 指向新节点
first = newNode;
} else {
// 原 last 节点的 next 指向新节点
l.next = newNode;
}
size++;
}

指定位置添加

分为两种情况,一个是在最后的节点新加一个。一种是在指定节点的前面插入新节点。

在后面添加前面尾巴添加已经说过,对于在指定节点的前面插入需要我们先找到指定位置节点,然后改变他们的 prev next 指向。


@Override
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size) {
linkLast(element);
} else {
linkBefore(element, node(index));
}
} /**
* Links e as last element.
*/
void linkLast(E element) {
addLast(element);
} /**
* Inserts element e before non-null Node succ.
*/
private void linkBefore(E element, Node<E> succ) {
// assert succ != null
final Node<E> prev = succ.prev;
// 构造新节点
final Node<E> newNode = new Node<>(prev, element, succ);
succ.prev = newNode;
if (Objects.isNull(prev)) {
first = newNode;
} else {
prev.next = newNode;
}
size++;
}

节点查找

为了优化,根据 index 查找的时候先判断 index 落在前半部分还是后半部分。前半部分通过 first 开始查找,否则通过 last 指针从后往前遍历。

    @Override
public E get(int index) {
checkElementIndex(index);
return node(index).item;
} /**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// 优化查找,判断 index 在前半部分还是后半部分。
if (index < (this.size >> 2)) {
// 前半部分,从头结点开始查找
Node<E> x = this.first;
for (int i = 0; i < index; i++) {
x = x.next;
}
return x;
} else {
// 后半部分,从尾节点开始查找
Node<E> x = this.last;
for (int i = size - 1; i > index; i--) {
x = x.prev;
}
return x;
}
}

查找 Object 所在位置 indexOf ,若找不到返回 -1

    @Override
public int indexOf(Object o) {
int index = 0;
if (Objects.isNull(o)) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
return index;
}
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item.equals(o)) {
return index;
}
index++;
}
}
return -1;
}

判断 链表中是否存在 指定对象 contains ,其实还是利用 上面的 indexOf 方法,当返回值 不等于 -1 则说明包含该对象。


@Override
public boolean contains(Object o) {
return indexOf(o) != -1;
}

节点删除

有两种删除情况:

  1. 根据下标删除指定位置的节点。
  2. 删除指定数据的节点。

删除指定位置节点

  1. 首先判断该 index 是否合法存在。
  2. 查找要删除的节点位置,重新设置被删除节点关联的指针指向。

node() 方法已经在前面的查找中封装好这里可以直接调用,我们再实现 unlink 方法,该方法还会用于删除指定对象,所以这抽出来实现复用。也是最核心最不好理解的方法,我们多思考画图理解下。

    @Override
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
public final void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* Tells if the argument is the index of an existing element.
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size();
} /**
* Unlinks non-null node x.
*/
private E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 若 只有一个节点,那么会执行 prev == null 和 next == null 分支代码
// 若 prev == null 则说明删除的是头结点,主要负责 x 节点跟前驱节点的引用处理
if (Objects.isNull(prev)) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 若 next 为空,说明删除的是尾节点,主要负责 x 与 next 节点 引用的处理
if (Objects.isNull(next)) {
last = prev;
} else {
next.prev = prev;
x.next = null;
} x.item = null;
size--;
return element;
}

分别找出被删除节点 x 的前驱和后继节点,要考虑当前链表只有一个节点的情况,最后还要把被删除节点的 的 next 指针 ,item 设置 null,便于垃圾回收,防止内存泄漏。

删除指定数据

这里判断下数据是否是 null , 从头节点开始遍历链表,当找到索要删除的节点的时候低啊用前面封装好的 unlink 方法实现删除。


@Override
public boolean remove(Object o) {
if (Objects.isNull(o)) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

完整代码可以参考 GitHub:https://github.com/UniqueDong/algorithms.git

加群跟我们一起探讨,欢迎关注 MageByte,我第一时间解答。

推荐阅读

1.跨越数据结构与算法

2.时间复杂度与空间复杂度

3.最好、最坏、平均、均摊时间复杂度

4.线性表之数组

5.链表导论-心法篇

6.单向链表正确实现方式

原创不易,觉得有用希望读者随手「在看」「收藏」「转发」三连。

7L-双线链表实现的更多相关文章

  1. JAVA 基本数据结构--数组、链表、ArrayList、Linkedlist、hashmap、hashtab等

    概要 线性表是一种线性结构,它是具有相同类型的n(n≥0)个数据元素组成的有限序列.本章先介绍线性表的几个基本组成部分:数组.单向链表.双向链表:随后给出双向链表的C.C++和Java三种语言的实现. ...

  2. MySQL记录之间是单向链表还是双向链表?

    前言 本文的观点是基于MySQL使用Innodb存储引擎的情况下进行的! 很多渠道说:MySQL数据按照主键大小依次排列,记录之间是双向链表连起来.如果说我告诉你这种说法很大程度上是错的,你肯定说我在 ...

  3. Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例

    概要  前面,我们已经学习了ArrayList,并了解了fail-fast机制.这一章我们接着学习List的实现类——LinkedList.和学习ArrayList一样,接下来呢,我们先对Linked ...

  4. Java集合系列:-----------05LinkedList的底层实现

    前面,我们已经学习了ArrayList,并了解了fail-fast机制.这一章我们接着学习List的实现类--LinkedList.和学习ArrayList一样,接下来呢,我们先对LinkedList ...

  5. Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  6. LinkedList源代码深入剖析

    第1部分 LinkedList介绍LinkedList简介 public class LinkedList<E> extends AbstractSequentialList<E&g ...

  7. LinkedHashMap源码分析及实现LRU

    概述 从名字上看LinkedHashMap相比于HashMap,显然多了链表的实现.从功能上看,LinkedHashMap有序,HashMap无序.这里的顺序指的是添加顺序或者访问顺序. 基本使用 @ ...

  8. 【由浅入深理解java集合】(三)——集合 List

    第一篇文章中介绍了List集合的一些通用知识.本篇文章将集中介绍List集合相比Collection接口增加的一些重要功能以及List集合的两个重要子类ArrayList及LinkedList. 一. ...

  9. LinkedList源码分析和实例应用

    1. LinkedList介绍 LinkedList是继承于AbstractSequentialList抽象类,它也可以被当作堆栈.队列或者双端队列使用. LinkedList实现了Deque接口,即 ...

  10. java集合系列之LinkList

    概要  第1部分 LinkedList介绍第2部分 LinkedList数据结构第3部分 LinkedList源码解析(基于JDK1.6.0_45) 第5部分 LinkedList示例 转载请注明出处 ...

随机推荐

  1. def跨域+jwt

    1.跨域 由于浏览器具有“同源策略”的限制.如果在同一个域下发送ajax请求,浏览器的同源策略不会阻止.如果在不同域下发送ajax,浏览器的同源策略会阻止. 总结 域相同,永远不会存在跨域. crm, ...

  2. 搭建私有 Nuget 服务器教程(1)

    对于 .NET 开发者来说,nuget 是必不可少的程序包管理工具.相应地,大部分开发团队都需要在内部搭建 Nuget 服务器,以管理私有 nupkg 包.本教程所使用的 Nuget 服务器,不是微软 ...

  3. 干货来啦。Flask框架看这一篇就够了,关注不迷路,Jeff带你看源码。开发技术时时更新

    目录 一.初识Flask 1.1 什么是flask? 1.2 为什么要有flask? 二.Flask快速启动 三.Flask四剑客 三.flask的配置文件 可以配置的属性 四.flask路由 4.1 ...

  4. Linux与unix shell编程指南

    第14章 环境和shell变量 1.使用变量时,尽量用花括号将之括起来,防止shell误解变量值. 2.设置变量时的不同模式 variable-name=value 设置实际值到variable-na ...

  5. Java基础 - 原码、反码、补码

    目录 机器数 真值 原码 反码 补码 为什么使用原码. 反码. 补码 机器数 所有数字在计算机底层都是以二进制形式存在的.它的表现形式叫做机器数,这个数有正负之分,最高位为符号位.0 表示正数, 1 ...

  6. Jenkinsfile里定义对象和函数,获取git提交人, 发送钉钉通知

    自从开始使用Jenkinsfile作为Jenkins配置后就一发不可收,因为开发者自定义CI脚本实在太方便了. 比如,最近开发的以一个项目涉及多人,提交冲突挺多的,有的人自己没编译通过就提交了,导致后 ...

  7. [剑指offer]62.圆圈中最后剩下的数字

    62.圆圈中最后剩下的数字 题目 0,1,...,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字.求出这个圆圈里剩下的最后一个数字. 例如,0.1.2.3.4这5个数字组成 ...

  8. JS三个事件绑定方法

    1. JS三个事件绑定方法 1.1. 使用html进行事件绑定 1.直接在html标签上写入事件类型和事件处理方法. <button onclick = "alert('hello w ...

  9. Mac下 eclipse target runtime com.genuitec.runtime 解决方法

    Mac下 eclipse target runtime com.genuitec.runtime 解决方法 解决步骤如下: 首先是找到工程项目一个名叫.settings的文件夹,里面有个叫 org.e ...

  10. Java基础语法(4)-流程控制

    title: Java基础语法(4)-流程控制 blog: CSDN data: Java学习路线及视频 1.程序流程控制 流程控制语句是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一定 ...