链表设计与实现

在谈链表之前,我们先谈谈我们平常编程会遇到的很常见的一个问题。如果在编程的时候,某个变量在后续编程中仍需使用,我们可以用一个局部变量来保存该值,除此之外一个更加常用的方法就是使用容器了。

那什么是容器呢?从字面上来说就是用来装某个东西的,比如我们的杯子,就是容器。在程序设计当中我们最常见的容器就是数组了,他可以存我们想保存的东西。在编程当中我们最常见的容器如下:

  • 在Python当中有列表、字典、元组、集合等等。
  • 在Java当中常见的容器有 ArrayListLinkedListHashMapHashSet等等。
  • 在C++当中有vectorlistunordered_mapunordered_set等等。

今天要谈到的链表在Java的LinkedList和C++的list当中就有使用到。

那什么是链表呢?链表是由一个一个的节点组成的,每个节点包含两个字段,其中一个字段data表示真实需要表示的数据,另外一个字段next表示指向下一个节点的指针(如果不了解指针也没有关系,就将其当做一个普通的变量既可,不影响我们的理解),data和next两者一起组成链表当中的节点(Node)。

其中data表示链表当中存储的真实的数据,而next表示指向下一个节点的指针(如果不了解指针也没有关系,就将其当做一个普通的变量既可,不影响我们的理解),datanext两者一起组成链表当中的节点(Node)。

Java代码:

class Node<E> {
E item;
Node<E> next;
public Node(E item, Node<E> next) {
this.item = item;
this.next = next;
}
}

单链表

所谓单链表就是只有一个指向其他节点的变量,比如下图当中只有一个next变量指向其他同样的节点。

双向链表

双向链表和单链表的区别就是他的指向有两个方向,而单链表只有一个方向,在双向链表的节点当中会有两个指向其他同样节点的变量,一个指向前一个节点,一个指向后一个节点,对应下图prev指向前一个节点,next指向后一个节点。

循环链表

这个概念也比较简单,就是链表首尾相连,形成一个环,比如单循环链表:

双向循环链表,第一个节点(头结点)的prev指向最后一个节点(尾节点),尾节点的next指向头结点:

静态链表

我们前面所提到的链表中的节点除了数据域(data)还有一个变量指向其他的节点,节点与节点之间的内存地址是不连续的,而静态链表和前面提到的链表不一样,它是使用数组来实现链表,只是将next变成一个int类型的数据,表示下一跳数据的下标,比如下图当中所表示的那样(其中-1表示链表的结尾,因为next域存储的是下一个节点的下标,下标肯定大于等于0,因此可以使用-1表示链表的结尾):

在上图当中对应的链表如下(通过分析上图当中next域的指向分析得到下图):

像这种使用数组实现的链表叫做静态链表,上面谈到的就是静态单链表,它对应的数据结构也很清楚:

private static class StaticNode<E> {
// 指向节点的真实存储的数据
E item;
// 指向下一个节点的下标
int next; public StaticNode(E item, int next) {
this.item = item;
this.next = next;
}
}

为什么需要链表?

回答这个问题之前,首先需要搞清楚我们面临什么样的需求:

  • 我们需要有一个容器可以保存我们的数据
  • 我们的数据有一定的顺序性,比如我们现在容器当中的数据个数是10个,我们想在下标为3的地方插入一个数据

​ 在数组长度够的情况下,我们需要将下标2之后的数据往后搬一个位置然后将新的数据放到下标为3的位置,这种插入的时间复杂度为 O(n),至于为什么是O(n)我们在谈ArrayList时我们再进行证明。

  • 但是如果我们采用的是链表的方法的话,我们的时间复杂度可以做到O(1)。

​ 对于上面这种插入情况,我们只需要稍微改变一下next的指向就可以了:

  • 如果我们需要在数组当中删除一个元素,同样的原理,因为某个数据被删除之后它所在的那个位置就空了,因此需要将后续的数据往前搬一个位置:

    比如我们需要删除下标为三的数据:

但是如果我们使用的是链表的话我们也只需要简单移动链表即可,比如要删除节点N,只需要将节点N的上一个节点的next指向节点N的下一个节点即可,同时将节点N的next设置为空。

​ 因为我们在操作的时候只需要调整一下next指针的指向即可,这个操作的时间复杂度是常数级别的,因此时间复杂度为O(1)。

​ 根据上面所谈到的内容,可以发现链表在这种需要频繁插入和删除的场景很适合。

Java代码实现双向链表

需求分析

在正式实现双向链表之前我们首先分析一下我们的需求:

  • 需要有一个方法判断链表里面是否有数据,也就是链表是否为空。

  • 需要有一个方法判断链表里面是否包含某个数据,这个包含的意思表示是否存在一个数据和当前的数据一样,并不是内存地址一致,相当于Java当中的equals方法。

  • 需要有一个方法往链表当中添加数据

  • 需要有一个方法往链表当中删除数据

我们的需求主要就是上面这些了,当然也可以增加一些其他的方法,比如说增加将链表变成数组的方法等等,为了简单我们只实现上述功能。

具体实现

  • 定义节点的数据结构

    根据前面的分析我们很容易可以设计出链表当中节点的结构,其代码如下所示:

    /**
    * 自己实现链表
    * @param <E> 泛型,表示容器当中存储数据的数据类型
    */
    public class MyLinkedList<E> { private static class Node<E> {
    // 指向节点的真实存储的数据
    E item;
    // 前向指针:指向前一个数据
    Node<E> prev;
    // 后向指针:指向后一个数据
    Node<E> next;
    public Node(E item, Node<E> prev, Node<E> next) {
    this.item = item;
    this.prev = prev;
    this.next = next;
    }
    }
    }
  • 为了符合设计模式,让我们的代码更加清晰和容易维护,我们可以设计一个接口(为了避免复杂的接口信息我们就用一个统一的接口表示)表示我们要实现的功能,其代码如下:

    public interface MyCollection<E> {
    
      /**
    * 往链表尾部加入一个数据
    * @param o 加入到链表当中的数据
    * @return
    */
    boolean add(E o); /**
    * 表示在第 index 位置插入数据 o
    * @param index
    * @param o
    * @return
    */
    boolean add(int index, E o); /**
    * 从链表当中删除数据 o
    * @param o
    * @return
    */
    boolean remove(E o); /**
    * 从链表当中删除第 index 个数据
    * @param index
    * @return
    */
    boolean remove(int index); /**
    * 往链表尾部加入一个数据,功能和 add 一样
    * @param o
    * @return
    */
    boolean append(E o); /**
    * 返回链表当中数据的个数
    * @return
    */
    int size(); /**
    * 表示链表是否为空
    * @return
    */
    boolean isEmpty(); /**
    * 表示链表当中是否包含数据 o
    * @param o
    * @return
    */
    boolean contain(E o);
    }
  • 链表当中应该有哪些变量?首先我们肯定需要知道链表当中有多少数据,其次因为我们是双向链表,需要能够从头或者从尾部进行链表的遍历,因此很自然我们需要变量指向链表当中的第一个节点和最后一个节点。

  // 表示链表当中数据的个数
private int size; // 链表当中第一个节点
private Node<E> first; // 表示链表当中最后一个节点
private Node<E> last;
  • 往链表尾部加入一个节点
  @Override
public boolean append(E o) {
final Node<E> l = last;
// 新增的节点需要将 prev 指向上一个节点,上一个节点就是链表的 last 节点
// 新增节点的下一个节点就 null
final Node<E> newNode = new Node<>(o, last, null);
last = newNode;
if (first == null) {
// 如果链表当中还没有节点,就将其作为第一个节点
first = newNode;
}else {
// 如果链表当中已经有节点,需要将新增的节点连接到链表的尾部
l.next = newNode;
}
size++;
return true;
}
  • 根据下标找到链表当中对应下标的节点
  /**
* 根据下标找节点
* @param index
* @return
*/
Node<E> findNodeByIndex(int index) {
if (index >= size)
throw new RuntimeException("输入 index 不合法链表中的数据个数为 " + size);
Node<E> x;
// 首先看看 index 和 size / 2 的关系
// 这里主要是看链表的首和尾部谁距离 index 位置近,那头近就从哪头遍历
// size >> 1 == size / 2
if (index < (size >> 1)) {
x = first;
for (int i = 0; i < index; i++)
x = x.next;
} else {
x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
}
return x;
}
  • 在链表当中删除某个节点
  void removeNode(Node<E> node) {
if (node == null)
throw new NullPointerException();
if (node.prev != null)
node.prev.next = node.next;
if (node.next != null)
node.next.prev = node.prev;
} /**
* 根据下标删除某个节点
* @param index
* @return
*/
@Override
public boolean remove(int index) {
// 首先找到第 index 个数据对应的节点
Node<E> node = findNodeByIndex(index);
// 删除节点
removeNode(node);
size--;
return true;
}
  • toString方法重写
  @Override
public String toString() { if (first == null)
return "[]"; StringBuilder builder = new StringBuilder();
builder.append("[");
Node<E> start = first;
builder.append(start.item.toString());
start = start.next;
while (start != null) {
builder.append(", ").append(start.item.toString());
start = start.next;
}
builder.append("]");
return builder.toString();
}
  • 测试代码
  public static void main(String[] args) {
MyLinkedList<Integer> list = new MyLinkedList<>();
System.out.println(list.contain(100));
for (int i = 0; i < 10; i++) {
list.add(i);
}
list.add(0, -9999);
System.out.println(list.size() / 2);
list.add(5, 9999);
list.append(Integer.MAX_VALUE);
System.out.println(list); list.remove(5);
list.add(6, 6666);
System.out.println(list);
System.out.println(list.contain(6666));
}

输出

false
5
[-9999, 0, 1, 2, 3, 9999, 4, 5, 6, 7, 8, 9, 2147483647]
[-9999, 0, 1, 2, 3, 4, 6666, 5, 6, 7, 8, 9, 2147483647]
true

双向链表实现完整代码:

/**
* 自己实现链表
* @param <E> 泛型,表示容器当中存储数据的数据类型
*/
public class MyLinkedList<E> implements MyCollection<E> { // 表示链表当中数据的个数
private int size = 0; // 链表当中第一个节点
private Node<E> first; // 表示链表当中最后一个节点
private Node<E> last; @Override
public boolean add(E o) {
return append(o);
} @Override
public boolean add(int index, E o) {
Node<E> node = findNodeByIndex(index);
insertBeforeNode(node, o);
size++;
return true;
} /**
* 在节点数据 node 之后插入数据 o
* @param node
* @param o
*/
void insertAfterNode(Node<E> node, E o) {
if (node == null)
throw new NullPointerException();
// newNode 前面的节点为 node 后面的节点是 node.next
Node<E> newNode = new Node<>(o, node, node.next);
if (node.next != null)
node.next.prev = newNode;
if (node == last)
last = newNode;
node.next = newNode;
} /**
* 在节点 node 之前插入数据 o
* @param node
* @param o
*/
void insertBeforeNode(Node<E> node, E o) {
if (node == null)
throw new NullPointerException();
// newNode 前面你的节点为 node.prev 后面的节点为 node
Node<E> newNode = new Node<>(o, node.prev, node);
if (node.prev != null)
node.prev.next = newNode;
else
first = newNode;
node.prev = newNode;
} /**
* 根据下标找节点
* @param index
* @return
*/
Node<E> findNodeByIndex(int index) {
if (index >= size)
throw new RuntimeException("输入 index 不合法链表中的数据个数为 " + size);
Node<E> x;
// 首先看看 index 和 size / 2 的关系
// 这里主要是看链表的首和尾部谁距离 index 位置近,那头近就从哪头遍历
// size >> 1 == size / 2
if (index < (size >> 1)) {
x = first;
for (int i = 0; i < index; i++)
x = x.next;
} else {
x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
}
return x;
} void removeNode(Node<E> node) {
if (node == null)
throw new NullPointerException();
if (node.prev != null)
node.prev.next = node.next;
if (node.next != null)
node.next.prev = node.prev;
} @Override
public boolean remove(E o) {
Node<E> start = first;
while (start != null) {
if (start.item.equals(o))
removeNode(start);
start = start.next;
}
size--;
return true;
} /**
* 根据下标删除某个节点
* @param index
* @return
*/
@Override
public boolean remove(int index) {
// 首先找到第 index 个数据对应的节点
Node<E> node = findNodeByIndex(index);
// 删除节点
removeNode(node);
size--;
return true;
} @Override
public boolean append(E o) {
final Node<E> l = last;
// 新增的节点需要将 prev 指向上一个节点,上一个节点就是链表的 last 节点
// 新增节点的下一个节点就 null
final Node<E> newNode = new Node<>(o, last, null);
last = newNode;
if (first == null) {
// 如果链表当中还没有节点,就将其作为第一个节点
first = newNode;
}else {
// 如果链表当中已经有节点,需要将新增的节点连接到链表的尾部
l.next = newNode;
}
size++;
return true;
} @Override
public int size() {
return size;
} @Override
public boolean isEmpty() {
return size == 0;
} @Override
public boolean contain(E o) {
Node<E> start = first;
while (start != null) {
if (start.item.equals(o))
return true;
start = start.next;
}
return false;
} private static class Node<E> {
// 指向节点的真实存储的数据
E item;
// 前向指针:指向前一个数据
Node<E> prev;
// 后向指针:指向后一个数据
Node<E> next;
public Node(E item, Node<E> prev, Node<E> next) {
this.item = item;
this.prev = prev;
this.next = next;
}
} @Override
public String toString() { if (first == null)
return "[]"; StringBuilder builder = new StringBuilder();
builder.append("[");
Node<E> start = first;
builder.append(start.item.toString());
start = start.next;
while (start != null) {
builder.append(", ").append(start.item.toString());
start = start.next;
}
builder.append("]");
return builder.toString();
} public static void main(String[] args) {
MyLinkedList<Integer> list = new MyLinkedList<>();
System.out.println(list.contain(100));
for (int i = 0; i < 10; i++) {
list.add(i);
}
list.add(0, -9999);
System.out.println(list.size() / 2);
list.add(5, 9999);
list.append(Integer.MAX_VALUE);
System.out.println(list); list.remove(5);
list.add(6, 6666);
System.out.println(list);
System.out.println(list.contain(6666));
}
}

关注公众号:一无是处的研究僧,了解更多计算机知识

下期我们仔细分析JDK内部LinkedList具体实现,我是LeHung,我们下期再见!!!

链表设计与Java实现,手写LinkedList这也太清楚了吧!!!的更多相关文章

  1. 45 容器(四)——手写LinkedList

    概念 LinkedList级双向链表,它的单位是节点,每一个节点都要一个头指针和一个尾指针,称为前驱和后继.第一个节点的头指针指向最后一个节点,最后一个节点的尾指针指向第一个节点,形成环路. 链表增删 ...

  2. java 从零开始手写 RPC (03) 如何实现客户端调用服务端?

    说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...

  3. java 从零开始手写 RPC (04) -序列化

    序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...

  4. java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端

    通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...

  5. java 从零开始手写 RPC (07)-timeout 超时处理

    <过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...

  6. java - day015 - 手写双向链表, 异常(续), IO(输入输出)

    类的内存分配 加载到方法区 对象在堆内存 局部变量在栈内存 判断真实类型,在方法区加载的类 对象.getClass(); 类名.class; 手写双向链表 package day1501_手写双向链表 ...

  7. java设计思想-池化-手写数据库连接池

     https://blog.csdn.net/qq_16038125/article/details/80180941 池:同一类对象集合 连接池的作用 1. 资源重用 由于数据库连接得到重用,避免了 ...

  8. Java精进-手写持久层框架

    前言 本文适合有一定java基础的同学,通过自定义持久层框架,可以更加清楚常用的mybatis等开源框架的原理. JDBC操作回顾及问题分析 学习java的同学一定避免不了接触过jdbc,让我们来回顾 ...

  9. Java修炼——手写服务器项目

    项目工程总览: 1.Dispatcher类(一个请求与响应就是一个Dispatcher) package com.bjsxt.server; import java.io.IOException; i ...

随机推荐

  1. 技术管理进阶——一线Leader怎么做?经理的速成宝典

    原创不易,求分享.求一键三连 本期培训材料关注公众号后回复:经理培训,获得 前段时间有个同学问我有没有一线Leader的速成培训课程,很好的问题,首先我们需要定义一下什么是小Leader: 所谓小Le ...

  2. Bugku练习题---MISC---FileStoragedat

    Bugku练习题---MISC---FileStoragedat flag:bugku{WeChatwithSteg0} 解题步骤: 1.观察题目,下载附件 2.下载后发现是一个后缀名为.dat的文件 ...

  3. muduo源码分析之回调模块

    这次我们主要来说说muduo库中大量使用的回调机制.muduo主要使用的是利用Callback的方式来实现回调,首先我们在自己的EchoServer构造函数中有这样几行代码 EchoServer(Ev ...

  4. jQuery操作标签,jQuery事件操作,jQuery动画效果,前端框架

    jQuery操作标签 jQuery代码查找标签绑定的变量名推荐使用 $xxxEle 样式类操作 addClass();// 添加指定的CSS类名. removeClass();// 移除指定的CSS类 ...

  5. Git 后续——分支与协作

    Git 后续--分支与协作 本文写于 2020 年 9 月 1 日 之前一篇文章写了 Git 的基础用法,但那其实只是「单机模式」,Git 之所以在今天被如此广泛的运用,是脱不开分支系统这一概念的. ...

  6. 提升站点SEO的7个建议

    1.使用HTTPS 谷歌曾发公告表示,使用安全加密协议(HTTPS),是搜索引擎排名的一项参考因素. 所以,在域名相同情况下,HTTPS站点比HTTP站点,能获得更好的排名. 在网络渠道分发或合作上, ...

  7. 如何定制.NET6.0的日志记录

    在本章中,也就是整个系列的第一部分将介绍如何定制日志记录.默认日志记录仅写入控制台或调试窗口,这在大多数情况下都很好,但有时需要写入到文件或数据库,或者,您可能希望扩展日志记录的其他信息.在这些情况下 ...

  8. 【低码】asp.net core 实体类可生产 CRUD 后台管理界面

    前言介绍 喜欢小规模团队的"单打独斗",有的时候即使在大公司,也经常做着3-5个人团队的小项目,相信很多人有类似的经历. 本文介绍如何将项目中已存在的[实体类],直接生产出 CRUD 后台管理界面. ...

  9. PostMan 快快走开, ApiFox 来了, ApiFox 强大的Api调用工具

    简介 为什么要用ApiFox呢, 一般现在程序员开发测试, 一般都是PostMan, PostWoman等Api调用工具, 我之前也是一直在用, 但是今天我发现了一款相比于Postman更加好用的工具 ...

  10. 模块re正则

    正则表达式 内容概要 正则表达式前戏 正则表达式之字符组 正则表达式特殊符号 正则表达式量词 正则表达式贪婪与非贪婪匹配 正则表达式取消转义 python内置模块之re模块 内容详情 正则表达式前戏 ...