链表是基本的数据结构,尤其双向链表在应用中最为常见,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. 手动搭建webpack + vue项目之初体验

    在使用vue做开发时,大部分人只会使用官方提供的脚手架搭建项目,脚手架虽然很好用,但想要成为一名优秀的前端开发者,webpack这一道坎是绕不开的,所以我们要学会脱离脚手架,利用webpack手动搭建 ...

  2. SpringCloud微服务:Sentinel哨兵组件,管理服务限流和降级

    源码地址:GitHub·点这里||GitEE·点这里 一.基本简介 1.概念描述 Sentinel 以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度保护服务的稳定性.包括核心的独立类库,监 ...

  3. Fiddler1 简单使用

    1.Fiddler下载地址:https://www.telerik.com/download/fiddler 2.Fiddler设置: Fiddler是强大的抓包工具,它的原理是以web代理服务器的形 ...

  4. Swift5.2 新特性

    Print 函数传参新格式 let param = "参数" print(#"这是一个\#(param) xxxxx"#) 允许在模块中定义和标准库中名称一样的 ...

  5. 一些Nmap NSE脚本推荐

    前言 Nmap是一款强大的开源扫描工具.同时Nmap提供了强大的脚本引擎(Nmap Scripting Engine),支持通过Lua脚本语言来扩展Nmap的功能,在Nmap的发行版中已经包含了数百个 ...

  6. 面试刷题17:线程两次start()会发生什么?

    线程是并发编程的基础元素,是系统调度的最小单元,现代的jvm直接对应了内核线程.为了降低并发编程的门槛,go语言引入了协程. 你好,我是李福春,我在准备面试,今天的题目是? 一个线程两次调用start ...

  7. OpenLDAP 多主复制(基于docker容器模式部署)

    **本文主要讲述在docker环境下如何进行 OpenLDAP 多主复制,至于 OpenLDAP 原理可以先参考这篇文章了解:https://cloud.tencent.com/developer/a ...

  8. 浏览器与DNS解析过程

    浏览器解析 1.地址栏输入地址后,浏览器检查自身DNS缓存 地址栏输入chrome://net-internals/#dns 查看. 2.浏览器缓存中未找到,那么Chrome会搜索操作系统自身的DNS ...

  9. windows10环境下QtCreator中出现skipping incompatible xxx when searching for xxx 问题解决办法

    windows10环境下QtCreator中出现skipping incompatible xxx when searching for xxx 我再QtCreator中想导入一个外部库时,他提示不匹 ...

  10. 详解firewalld 和iptables

    在RHEL7里有几种防火墙共存:firewalld.iptables.ebtables,默认是使用firewalld来管理netfilter子系统,不过底层调用的命令仍然是iptables等. fir ...