前言

前面两节内容我们详细介绍了ArrayList,一是手写实现ArrayList数据结构,而是通过分析ArrayList源码看看内置实现,关于集合内容一如既往,本节课我们继续学习集合LinkedList,我们首先入门LinkedList数据结构,然后再去看看LinkedList源码是如何实现的,我们开始吧。

LinkedList入门

LinkedList内置是通过双链表数据结构来存储数据,和ArrayList不同的是,ArrayList属于真正意义物理意义上的线性结构,而LinkedList也属于线性链表,只不过需要通过我们手动来关联前后节点数据,同时呢,双链表和单链表只是在结构上有所不同而已,只是双链表多了一个前驱节点,其他无差异,那么到底何为双链表呢?在我们日常生活中到处都是这样的例子,比如我们音乐播放器应该算比较形象了,如下:

单链表自定义实现

接下来我们来实现单链表,然后对单链表进行改造成双链表,我们看到如上播放器,单链表只是少了前驱节点,但是有后继节点(如上写错了),所以我们需要定义一个节点,然后在此节点上有连接下一节点的引用(在C或C++中为指针),和当前节点所存储的数据,所以我们定义如下泛型节点类:

public class Node<T> {

    //当前节点值
public T data; //后继节点
public Node next; public Node(T data) {
this.data = data;
}
}

接下来则是定义链表来操作上述节点类并存储数据了, 这里我们稍微做的简单点,在链表中会存在头节点和尾节点,这里呢我们通过来头节点来操作,等我们升级到双链表时再来定义尾节点,所以在单链表中有头节点和链表长度两个变量,如下:

public class MyLinkedList<T> {

    //头节点
private Node head; //链表元素长度
private int length; }

温馨提示:这里我就不给大家画图演示了,自行脑补,实在感觉绕的话自己在画板或纸上画一下就明白了,我也是在纸上画了一番才动手写代码的。首先我们需要考虑头节点和尾节点即播放器中第一首歌和最后一首歌,然后针对指定位置添加歌曲通过next串联就形成了歌曲列表,更为形象的例子当属我们吃过的串串了。那么接下来我们完成往播放器列表中添加第一个首歌,此时我们应该想,头节点是否添加了第一首歌,若不存在则直接实例化头节点即可,若已存在第一首歌,我们则将重新实例化一首歌,然后将其已添加的第一首歌的引用赋值给新添加的歌曲的next,所以就有了如下方法:

//添加至头结点
public void addToHead(T data) {
if (head == null) {
head = new Node(data);
} else {
Node temp = head;
head = new Node(data);
head.next = temp;
}
length++;
}

好了,将新添加的歌曲放在第一首我们已经完全搞定了,然后我们再来往歌曲列表中最后添加一首歌曲,这个时候我们肿么知道是最后一首呢,只要next为空,说明就是最后一首歌曲,这就是判断依据,这点就不用我再过多解释了,那么就有了如下方法:

//添加至尾节点
public void addToTail(T data) {
Node temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = new Node(data);
length++;
}

单链表的确定就在这里,我们只能循环遍历才能找到最后一首,然后添加对应歌曲,所以当数据量足够大时,可想其性能。接下来则是最重要的一块了,我们想要在指定歌曲下添加歌曲,这个时候就涉及到找到对应歌曲索引然后添加数据,

    //添加到指定索引元素
public void add(int index, T data) {
if (index < 0) {
throw new RuntimeException("非法索引");
}
if (index > length) {
throw new RuntimeException("超出索引边界");
}
if (head == null || index == 0) {
addToHead(data);
return;
}
//头节点
Node temp = head;
//指定索引下一节点
Node holder;
for (int i = 0; i < index - 1 && temp.next != null; i++) {
temp = temp.next;
}
//未插入节点时指定索引下一节点
holder = temp.next;
//指定索引节点下一节点即待插入的节点
temp.next = new Node(data);
//将列表中指定索引节点下一节点引用指向指定待插入节点(此时指定索引下节点即为待插入节点,然后再下一节点即为待插入节点)
temp.next.next = holder;
length++;
}

接下来则是根据指定索引查找元素,我就不解释了,直接上代码,如下

    //根据索引查找元素
public T find(int index) {
if (index < 0) {
throw new RuntimeException("非法索引");
}
if (length == 0 || index > length) {
throw new RuntimeException("超出索引边界");
}
Node temp = head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return (T) temp.data;
}

最后老规矩重写toString方法,打印链表数据,如下:

    //链表元素大小
public int size() {
return length;
} @Override
public String toString() {
StringBuilder sb = new StringBuilder();
Node temp = head;
while (temp != null) {
sb.append(temp.data);
sb.append(",");
temp = temp.next;
}
if (sb.charAt(sb.length() - 1) == ',') {
sb.delete(sb.length() - 1, sb.length());
}
return sb.toString();
}

最后我们来往播放器列表中添加歌曲做个测试吧,走你,如下:

public class Main {

    public static void main(String[] args) {
MyLinkedList<Integer> list = new MyLinkedList<>();
//添加元素11到头节点
list.addToHead(11);
System.out.println(list); //添加元素15到尾节点
list.addToTail(15);
System.out.println(list); //添加元素12到头节点
list.addToHead(12);
System.out.println(list); //添加元素13到头节点
list.addToHead(13);
System.out.println(list); //添加元素8到尾节点
list.addToTail(8);
//添加元素7到尾节点
list.addToTail(7);
list.add(2, 9);
System.out.println(list); //在索引2位置添加元素9
list.add(2, 9);
System.out.println(list); //删除索引为4的元素
list.delete(4);
System.out.println(list);
}
}

双链表自定义实现

有了如上单链表的铺垫,接下来我们再来实现双链表则是轻而易举了,只不过添加了前驱节点和链表中的尾结点而已,走你,我们往节点类中添加前驱节点,如下:

public class Node<T> {

    //当前节点值
public T data; //前驱节点
public Node previous; //后继节点
public Node next; public Node(T data) {
this.data = data;
}
}

同理,我们在链表类中添加尾节点字段,如下:

public class MyLinkedList<T> {
//头节点
private Node head; //尾节点
private Node tail; //链表元素长度
private int length;
}

同样,当添加歌曲至首位时,此时我们也需初始化头节点,只不过这时多了个尾节点,没关系,这个时候头节点就是尾节点,我们封装一个初始化头节点和尾节点的方法,如下:

 //初始化头接点和尾节点
void initHead(T data) {
//初始化头节点
head = new Node(data);
//此时尾节点即头节点
tail = head;
}

然后添加歌曲至头节点时,只不过多了个前驱节点,也就相应多了一行代码而已,就是将已添加首位歌曲的前驱节点赋给待添加的首位歌曲,如下:

    //添加元素至头结点
public void addToHead(T data) {
if (head == null) {
initHead(data);
} else {
Node temp = head;
head = new Node(data);
head.next = temp;
temp.previous = head;
}
length++;
}

而添加歌曲至末位时就和上述单链表就有些不同了,单链表中是直接循环遍历,这里我们定义了尾节点,所以直接操作尾节点即可,如下:

    //添加至尾节点
public void addToTail(T data) {
if (size() == 0) {
initHead(data);
} else {
Node temp = tail;
tail = new Node(data);
temp.next = tail;
tail.previous = temp;
}
length++;
}

接下来又是添加指定索引元素的核心方法了,其实也非常简单,我都将注释给你写好了,还是看不懂,建议到纸上画画哈。

    //添加指定索引元素
public void add(int index, T data) {
if (index < 0) {
throw new RuntimeException("非法索引");
}
if (index > length) {
throw new RuntimeException("超出索引边界");
}
if (head == null || index == 0) {
initHead(data);
return;
}
//头节点
Node temp = head;
//定义获取指定索引节点下一节点
Node holder;
for (int i = 0; i < index - 1 && temp.next != null; i++) {
temp = temp.next;
}
//当前节点的下一节点
holder = temp.next;
//要添加的下一节点
temp.next = new Node(data);
//插入节点的后继节点为当前节点下一节点
temp.next.next = holder;
//当前节点下一前驱节点为插入节点
temp.next.next.previous = temp.next;
length++;
}

无论是添加还是删除最重要的是我们需要想清楚,添加时和删除后前驱节点和后继节点分别指向谁,把这个问题想明白了,那也就没什么了,走你,删除方法:

    //删除指定索引元素
public void delete(int index) {
if (index < 0) {
throw new RuntimeException("非法索引");
}
if (length == 0 || index > length) {
throw new RuntimeException("超出索引边界");
}
Node temp = head;
for (int i = 0; i < index - 1 && temp.next != null; i++) {
temp = temp.next;
}
temp.next.next.previous = temp;
temp.next = temp.next.next;
length--;
}

为了验证我们所写代码,我们打印出对应节点的前驱和后继节点,如下:

    public int size() {
return length;
} @Override
public String toString() {
StringBuilder sb = new StringBuilder();
Node temp = head;
while (temp != null) {
sb.append(temp.data);
sb.append(",");
if (temp.previous != null && temp.next != null) {
System.out.println(temp.previous.data + "<-(" + temp.data + ")->" + temp.next.data);
}
temp = temp.next;
}
if (sb.charAt(sb.length() - 1) == ',') {
sb.delete(sb.length() - 1, sb.length());
}
return sb.toString();
}

控制台测试数据和单链表中一样,结果数据如下(当然我们可以分开打印对应节点前驱和后继节点去验证也是阔以的,这里我也验证过来,么有任何问题):

总结

本节我们通过手写代码实现了单链表和双链表,还是非常简单,下一节我们详细分析LinkedList源码,感谢您的阅读,我们下节见

双链表算法原理【Java实现】(八)的更多相关文章

  1. JAVA 链表操作:单链表和双链表

    主要讲述几点: 一.链表的简介 二.链表实现原理和必要性 三.单链表示例 四.双链表示例 一.链表的简介 链表是一种比较常用的数据结构,链表虽然保存比较复杂,但是在查询时候比较便捷,在多种计算机语言都 ...

  2. 数组、单链表和双链表介绍 以及 双向链表的C/C++/Java实现

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

  3. 最全排序算法原理解析、java代码实现以及总结归纳

    算法分类 十种常见排序算法可以分为两大类: 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序. 线性时间非比较类排序:不通过 ...

  4. 图解双链表(Java实现)

    原创公众号:bigsai 文章已收录在 全网都在关注的数据结构与算法学习仓库 前言 前面有很详细的讲过线性表(顺序表和链表),当时讲的链表以但链表为主,但实际上在实际应用中双链表的应用多一些就比如Li ...

  5. java实现双链表(差点没写吐系列...)

    刚才把单链表写完了,现在又把双链表写了,双链表和单链表的区别就是每个节点有prior和next两个指针,不同于单链表的一个next指针,而且,正是因为有这两个指针,所以双链表可以前后两个方向去移动指针 ...

  6. 梯度迭代树(GBDT)算法原理及Spark MLlib调用实例(Scala/Java/python)

    梯度迭代树(GBDT)算法原理及Spark MLlib调用实例(Scala/Java/python) http://blog.csdn.net/liulingyuan6/article/details ...

  7. 【LeetCode-面试算法经典-Java实现】【109-Convert Sorted List to Binary Search Tree(排序链表转换成二叉排序树)】

    [109-Convert Sorted List to Binary Search Tree(排序链表转换成二叉排序树)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 ...

  8. 【LeetCode-面试算法经典-Java实现】【114-Flatten Binary Tree to Linked List(二叉树转单链表)】

    [114-Flatten Binary Tree to Linked List(二叉树转单链表)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given a bin ...

  9. 如何做系列(4)-微博URL短网址生成算法原理(java版、php版实现实例)

    短网址(Short URL),顾名思义就是在形式上比较短的网址.通常用的是asp或者php转向,在Web 2.0的今天,不得不说,这是一个潮流.目前已经有许多类似服务,借助短网址您可以用简短的网址替代 ...

随机推荐

  1. 四步实现在一台电脑上使用多个github账号

    四步实现在一台电脑上同时使用多个GitHub账号 今天和大家聊一下如何在一台电脑上同时使用多个GitHub账号,通过以下四个步骤就可以实现,其中第二个步骤为了便于叙述分成了几个小步骤. 1. 取消全局 ...

  2. net core 3.1 跨域 Cors 找不到 “Access-Control-Allow-Origin”

    首先在ConfigureServices添加 public void ConfigureServices(IServiceCollection services) { services.AddCors ...

  3. bossplayersCTF 1: Vulnhub Walkthrough

    主机扫描: http://10.10.202.130/ <!--WkRJNWVXRXliSFZhTW14MVkwaEtkbG96U214ak0wMTFZMGRvZDBOblBUMEsK--> ...

  4. 剑指offer-36:数组中的逆序对

    参考:1. https://www.geeksforgeeks.org/merge-sort/ 2.<剑指Offer:名企面试官精讲典型编程题> 题目描述 在数组中的两个数字,如果前面一个 ...

  5. MyBatis结果集一对多映射

    MyBatis结果集一对多映射 需求:重画二维码配置类,根据sizeCode将查询出来的imageCode分组. DROP TABLE IF EXISTS `size_code`; CREATE TA ...

  6. Xposed反射字段流程分析

    在XposedBridge源码中,反射字段的方法封装在de.robv.android.xposed.XposedHelpers类里面.下面来看看Xposed是如何获取和设置字段的值的 获取字段的值 获 ...

  7. C语言笔记 03_常量&存储类

    常量 常量是固定值,在程序执行期间不会改变.这些固定的值,又叫做字面量. 常量可以是任何的基本数据类型,比如整数常量.浮点常量.字符常量,或字符串字面值,也有枚举常量. 整数常量 整数常量可以是十进制 ...

  8. 解决ES报错NoNodeAvailableException[None of the configured nodes are available:问题

    elasticSearch的错误 NoNodeAvailableException[None of the configured nodes are available: [{#transport#- ...

  9. LeetCode 二叉树的锯齿形层次遍历

    第103题 给定一个二叉树,返回其节点值的锯齿形层次遍历.(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行). 例如: 给定二叉树 [3,9,20,null,null,15,7] ...

  10. Android8.1 源码修改之插入SIM卡默认启用Volte功能

    前言 公用电话产品,插入SIM卡后要求自动打开Volte功能,即插即用,用完拔卡就走 实现 第一步 开关对应的代码 通过打印日志和全局查找,源码位置 vendor/mediatek/proprieta ...