链表设计与实现

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

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

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

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

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

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

Java代码:

  1. class Node<E> {
  2. E item;
  3. Node<E> next;
  4. public Node(E item, Node<E> next) {
  5. this.item = item;
  6. this.next = next;
  7. }
  8. }

单链表

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

双向链表

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

循环链表

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

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

静态链表

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

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

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

  1. private static class StaticNode<E> {
  2. // 指向节点的真实存储的数据
  3. E item;
  4. // 指向下一个节点的下标
  5. int next;
  6. public StaticNode(E item, int next) {
  7. this.item = item;
  8. this.next = next;
  9. }
  10. }

为什么需要链表?

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

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

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

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

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

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

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

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

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

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

Java代码实现双向链表

需求分析

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

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

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

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

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

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

具体实现

  • 定义节点的数据结构

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

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

    1. public interface MyCollection<E> {
    2. /**
    3. * 往链表尾部加入一个数据
    4. * @param o 加入到链表当中的数据
    5. * @return
    6. */
    7. boolean add(E o);
    8. /**
    9. * 表示在第 index 位置插入数据 o
    10. * @param index
    11. * @param o
    12. * @return
    13. */
    14. boolean add(int index, E o);
    15. /**
    16. * 从链表当中删除数据 o
    17. * @param o
    18. * @return
    19. */
    20. boolean remove(E o);
    21. /**
    22. * 从链表当中删除第 index 个数据
    23. * @param index
    24. * @return
    25. */
    26. boolean remove(int index);
    27. /**
    28. * 往链表尾部加入一个数据,功能和 add 一样
    29. * @param o
    30. * @return
    31. */
    32. boolean append(E o);
    33. /**
    34. * 返回链表当中数据的个数
    35. * @return
    36. */
    37. int size();
    38. /**
    39. * 表示链表是否为空
    40. * @return
    41. */
    42. boolean isEmpty();
    43. /**
    44. * 表示链表当中是否包含数据 o
    45. * @param o
    46. * @return
    47. */
    48. boolean contain(E o);
    49. }
  • 链表当中应该有哪些变量?首先我们肯定需要知道链表当中有多少数据,其次因为我们是双向链表,需要能够从头或者从尾部进行链表的遍历,因此很自然我们需要变量指向链表当中的第一个节点和最后一个节点。

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

输出

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

双向链表实现完整代码:

  1. /**
  2. * 自己实现链表
  3. * @param <E> 泛型,表示容器当中存储数据的数据类型
  4. */
  5. public class MyLinkedList<E> implements MyCollection<E> {
  6. // 表示链表当中数据的个数
  7. private int size = 0;
  8. // 链表当中第一个节点
  9. private Node<E> first;
  10. // 表示链表当中最后一个节点
  11. private Node<E> last;
  12. @Override
  13. public boolean add(E o) {
  14. return append(o);
  15. }
  16. @Override
  17. public boolean add(int index, E o) {
  18. Node<E> node = findNodeByIndex(index);
  19. insertBeforeNode(node, o);
  20. size++;
  21. return true;
  22. }
  23. /**
  24. * 在节点数据 node 之后插入数据 o
  25. * @param node
  26. * @param o
  27. */
  28. void insertAfterNode(Node<E> node, E o) {
  29. if (node == null)
  30. throw new NullPointerException();
  31. // newNode 前面的节点为 node 后面的节点是 node.next
  32. Node<E> newNode = new Node<>(o, node, node.next);
  33. if (node.next != null)
  34. node.next.prev = newNode;
  35. if (node == last)
  36. last = newNode;
  37. node.next = newNode;
  38. }
  39. /**
  40. * 在节点 node 之前插入数据 o
  41. * @param node
  42. * @param o
  43. */
  44. void insertBeforeNode(Node<E> node, E o) {
  45. if (node == null)
  46. throw new NullPointerException();
  47. // newNode 前面你的节点为 node.prev 后面的节点为 node
  48. Node<E> newNode = new Node<>(o, node.prev, node);
  49. if (node.prev != null)
  50. node.prev.next = newNode;
  51. else
  52. first = newNode;
  53. node.prev = newNode;
  54. }
  55. /**
  56. * 根据下标找节点
  57. * @param index
  58. * @return
  59. */
  60. Node<E> findNodeByIndex(int index) {
  61. if (index >= size)
  62. throw new RuntimeException("输入 index 不合法链表中的数据个数为 " + size);
  63. Node<E> x;
  64. // 首先看看 index 和 size / 2 的关系
  65. // 这里主要是看链表的首和尾部谁距离 index 位置近,那头近就从哪头遍历
  66. // size >> 1 == size / 2
  67. if (index < (size >> 1)) {
  68. x = first;
  69. for (int i = 0; i < index; i++)
  70. x = x.next;
  71. } else {
  72. x = last;
  73. for (int i = size - 1; i > index; i--)
  74. x = x.prev;
  75. }
  76. return x;
  77. }
  78. void removeNode(Node<E> node) {
  79. if (node == null)
  80. throw new NullPointerException();
  81. if (node.prev != null)
  82. node.prev.next = node.next;
  83. if (node.next != null)
  84. node.next.prev = node.prev;
  85. }
  86. @Override
  87. public boolean remove(E o) {
  88. Node<E> start = first;
  89. while (start != null) {
  90. if (start.item.equals(o))
  91. removeNode(start);
  92. start = start.next;
  93. }
  94. size--;
  95. return true;
  96. }
  97. /**
  98. * 根据下标删除某个节点
  99. * @param index
  100. * @return
  101. */
  102. @Override
  103. public boolean remove(int index) {
  104. // 首先找到第 index 个数据对应的节点
  105. Node<E> node = findNodeByIndex(index);
  106. // 删除节点
  107. removeNode(node);
  108. size--;
  109. return true;
  110. }
  111. @Override
  112. public boolean append(E o) {
  113. final Node<E> l = last;
  114. // 新增的节点需要将 prev 指向上一个节点,上一个节点就是链表的 last 节点
  115. // 新增节点的下一个节点就 null
  116. final Node<E> newNode = new Node<>(o, last, null);
  117. last = newNode;
  118. if (first == null) {
  119. // 如果链表当中还没有节点,就将其作为第一个节点
  120. first = newNode;
  121. }else {
  122. // 如果链表当中已经有节点,需要将新增的节点连接到链表的尾部
  123. l.next = newNode;
  124. }
  125. size++;
  126. return true;
  127. }
  128. @Override
  129. public int size() {
  130. return size;
  131. }
  132. @Override
  133. public boolean isEmpty() {
  134. return size == 0;
  135. }
  136. @Override
  137. public boolean contain(E o) {
  138. Node<E> start = first;
  139. while (start != null) {
  140. if (start.item.equals(o))
  141. return true;
  142. start = start.next;
  143. }
  144. return false;
  145. }
  146. private static class Node<E> {
  147. // 指向节点的真实存储的数据
  148. E item;
  149. // 前向指针:指向前一个数据
  150. Node<E> prev;
  151. // 后向指针:指向后一个数据
  152. Node<E> next;
  153. public Node(E item, Node<E> prev, Node<E> next) {
  154. this.item = item;
  155. this.prev = prev;
  156. this.next = next;
  157. }
  158. }
  159. @Override
  160. public String toString() {
  161. if (first == null)
  162. return "[]";
  163. StringBuilder builder = new StringBuilder();
  164. builder.append("[");
  165. Node<E> start = first;
  166. builder.append(start.item.toString());
  167. start = start.next;
  168. while (start != null) {
  169. builder.append(", ").append(start.item.toString());
  170. start = start.next;
  171. }
  172. builder.append("]");
  173. return builder.toString();
  174. }
  175. public static void main(String[] args) {
  176. MyLinkedList<Integer> list = new MyLinkedList<>();
  177. System.out.println(list.contain(100));
  178. for (int i = 0; i < 10; i++) {
  179. list.add(i);
  180. }
  181. list.add(0, -9999);
  182. System.out.println(list.size() / 2);
  183. list.add(5, 9999);
  184. list.append(Integer.MAX_VALUE);
  185. System.out.println(list);
  186. list.remove(5);
  187. list.add(6, 6666);
  188. System.out.println(list);
  189. System.out.println(list.contain(6666));
  190. }
  191. }

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

下期我们仔细分析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. Exception in thread "main" java.awt.AWTError: Assistive Technology not found: org.GNOME.Accessibilit

    系统环境 Ubuntu 20.04 focal 问题分析 该异常出现的原因,从谷歌上可以得到答案 one of the more common causes of this exception is ...

  2. 攻防世界-MISC:simple_transfer

    这是攻防世界高手进阶区的题目,题目如下: 点击下载附件一,得到一个流量包,用wireshark打开搜索flag无果,无奈跑去查看WP,说是先查看一下协议分级,但是并没有像WP所说的协议的字节百分比占用 ...

  3. OAuth 2.1 框架

    OAuth 2.1 Draft 当前版本:v2-1-05 失效时间:2022/09/08 本文对部分原文翻译,同时加了一些笔记,以便理解. 单词 译意 identifiler 识别码 Resource ...

  4. Lab1:练习四——分析bootloader加载ELF格式的OS的过程

    练习四:分析bootloader加载ELF格式的OS的过程. 1.题目要求 通过阅读bootmain.c,了解bootloader如何加载ELF文件.通过分析源代码和通过qemu来运行并调试bootl ...

  5. 论文解读(GMT)《Accurate Learning of Graph Representations with Graph Multiset Pooling》

    论文信息 论文标题:Accurate Learning of Graph Representations with Graph Multiset Pooling论文作者:Jinheon Baek, M ...

  6. 羽夏 Bash 简明教程(上)

    写在前面   该文章根据 the unix workbench 中的 Bash Programming 进行汉化处理并作出自己的整理,并参考 Bash 脚本教程 和 BashPitfalls 相关内容 ...

  7. git clone指定分支

    技术背景 Git是代码版本最常用的管理工具,此前也写过一篇介绍Git的基本使用的博客,而本文介绍一个可能在特定场景下能够用到的功能--直接拉取指定分支的内容. Git Clone 首先看一下如果我们按 ...

  8. 使用 AgileConfig 动态配置 NLog

    NLog 是我们在 .NET 领域使用非常广泛的日志组件.它默认使用 xml 来维护它的配置.最近有几个同学问我当使用 AgileConfig 的时候如何配置 NLog .因为 AgileConfig ...

  9. Python Django 功能模块

    Python Django模块 Django模块,是针对有django基础,对django功能进行模块化,方便下次使用. 一.注册模块 该注册采用邮箱验证,注册成功后会发送激活链接到邮箱. 邮箱验证参 ...

  10. (持续更新)虚树,KD-Tree,长链剖分,后缀数组,后缀自动机

    真的就是讲课两天,吸收一个月呢! \(1.\)虚树 \(2.\)KD-Tree \(3.\)长链剖分 \(4.\)后缀数组 后缀数组 \(5.\)后缀自动机 后缀自动机