关注公众号 MageByte,有你想要的精彩内容。文中涉及的代码可访问 GitHub:https://github.com/UniqueDong/algorithms.git

上一篇《链表导论心法》讲解了链表的理论知识以及链表操作的实现原理。talk is cheap, show me the code ! 今天让我以一起把代码撸一遍,在写代码之前一定要根据上一篇的原理多画图才能写得好代码。举例画图,辅助思考。

废话少说,撸起袖子干。

接口定义

首先我们定义链表的基本接口,为了显示出 B 格,我们模仿我们 Java 中的 List 接口定义。

package com.zero.algorithms.linear.list;

public interface List<E> {

    /**
* Returns <tt>true</tt> if this list contains no elements.
*
* @return true if this list contains no elements
*/
boolean isEmpty(); /**
* Returns the number of elements in this list.
*
* @return the number of elements in this list
*/
int size(); /**
* Returns the element at the specified position in this list.
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
E get(int index); /**
* Appends the specified element to the end of this list (optional operation).
*
* @param e element to be appended to this list
* @return
*/
boolean add(E e); /**
* Inserts the specified element at the specified position in this list
* (optional operation). Shifts the element currently at that position
* (if any) and any subsequent elements to the right (adds one to their
* indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
*/
void add(int index, E element); /**
* return true if this list contains the specified element
*
* @param o element whose presence in this list is to be tested
* @return true if this list contains the specified element
*/
boolean contains(Object o); /**
* Removes the first occurrence of the specified element from this list,
* if it is present (optional operation).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
boolean remove(Object o); /**
* Removes the element at the specified position in this list (optional
* operation). Shifts any subsequent elements to the left (subtracts one
* from their indices). Returns the element that was removed from the
* list.
*
* @param index the index of the element to be removed
* @return the element previously at the specified position
*/
E remove(int index); /**
* Returns the index of the first occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the lowest index <tt>i</tt> such that
* <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
* or -1 if there is no such index.
*
* @param o element to search for
* @return the index of the first occurrence of the specified element in
* this list, or -1 if this list does not contain the element
*/
int indexOf(Object o);
}

抽象链表模板

看起来是不是很像骚包,接着我们再抽象一个抽象类,后续我们还会继续写双向链表,循环链表。双向循环链表。把他们的共性放在抽象类中,将不同点延迟到子类实现。

package com.zero.algorithms.linear.list;

public abstract class AbstractList<E> implements List<E> {
protected AbstractList() {
} public abstract int size(); /**
* Inserts the specified element at the beginning of this list.
*
* @param e the element to add
*/
public abstract void addFirst(E e); @Override
public boolean isEmpty() {
return size() == 0;
} public void add(int index, E element) {
throw new UnsupportedOperationException();
} public E remove(int index) {
throw new UnsupportedOperationException();
} /**
* check index
* @param index to check
*/
public final void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
} /**
* Tells if the argument is the index of a valid position for an
* iterator or an add operation.
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size();
} /**
* Constructs an IndexOutOfBoundsException detail message.
* Of the many possible refactorings of the error handling code,
* this "outlining" performs best with both server and client VMs.
*/
private String outOfBoundsMsg(int index) {
return "Index: " + index + ", Size: " + size();
} 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();
} }

Node 节点

单向链表的每个节点有两个字段,一个用于保存当前节点的数据 item,另外一个则是 指向下一个节点的 next 指针。所以我们在单向链表定义一个静态内部类表示 Node 节点。

构造方法分别将数据构建成 Node 节点,默认 next 指针是 null

    /**
* Node data
*
* @param <E>
*/
private static class Node<E> {
/**
* Node of data
*/
E item;
/**
* pointer to next Node
*/
Node<E> next; public Node(E item, Node<E> next) {
this.item = item;
this.next = next;
} public Node(E item) {
this(item, null);
}
}

单项链表,size 属性保存当前节点数量,Node<E> head指向 第一个节点的指针,并且存在

(head == null && last == null) || (head.prev == null && head.item !=null)

是真事件。以及 last指针指向最后的节点。最后我们还要重写 toString 方法,便于测试。

代码如下所示:

public class SingleLinkedList<E> extends AbstractList<E> {

    transient int size = 0;
/**
* pointer to head node
*/
transient Node<E> head;
/**
* pointer to last node
*/
transient Node<E> last; public SingleLinkedList() {
} @Override
public String toString() {
StringBuilder sb = new StringBuilder();
Node<E> cur = this.head;
while (Objects.nonNull(cur)) {
sb.append(cur.item).append("->");
cur = cur.next;
}
sb.append("NULL");
return sb.toString();
}
}

添加元素

添加元素可能存在三种情况:

  1. 头结点添加。
  2. 中间任意节点添加。
  3. 尾节点添加。

在尾节点添加

将数据构造成 newNode 节点,将原先的 last 节点 next 指向 newNode 节点,如果 last 节点是 null 则将 head 指向 newNode ,同时 size + 1

public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element
*
* @param e data
*/
private void linkLast(E e) {
// 先取出原 last node
final Node<E> l = last;
final Node<E> newNode = new Node<>(e);
// 修改 last 指针到新添加的 node
last = newNode;
// 如果原 last 是 null 则将 newNode 设置为 head,不为空则原 last.next 指针 = newNode
if (Objects.isNull(l)) {
head = newNode;
} else {
l.next = newNode;
}
size++;
}

在头结点添加

将数据构造成 newNode 节点,同时 newNode.next 指向原先 head节点,并将 head 指向 newNode 节点,如果 head == null 标识当前链表还没有数据,将 last 指针指向 newNOde 节点。

    @Override
public void addFirst(E e) {
final Node<E> h = this.head;
// 构造新节点,next 指向原先的 head
final Node<E> newNode = new Node<>(e, h);
head = newNode;
// 如果原先的 head 节点为空,则 last 指针也指向新节点
if (Objects.isNull(h)) {
last = newNode;
}
size++;
}

在指定位置添加

分两种情况,当 index = size 的时候意味着在最后节点添加,否则需要找到当前链表指定位置的节点元素,并在该元素前面插入新的节点数据,重新组织两者节点的 next指针。

linkLast 请看前面,这里主要说明 linkBefore。

首先我们先查询出 index 位置的 Node 节点,并将 newNode.next 指向 该节点。同时还需要找到index 位置的上一个节点,将 pred.next = newNode。这样就完成了节点的插入,我们只要画图辅助思考就很好理解了。

    @Override
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size) {
linkLast(element);
} else {
linkBefore(element, index);
}
}
/**
* Inserts element e before non-null Node of index.
*/
void linkBefore(E element, int index) {
final Node<E> newNode = new Node<>(element, node(index));
if (index == 0) {
head = newNode;
} else {
Node<E> pred = node(index - 1);
pred.next = newNode;
}
size++;
} /**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
Node<E> x = this.head;
for (int i = 0; i < index; i++) {
x = x.next;
}
return x;
}

判断是否存在

indexOf(Object o) 用于查找元素所在位置,若不存在则返回 -1。

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

查找方法

根据 index 查找该位置的节点数据,其实就是遍历该链表。所以这里的时间复杂度是 O(n)。

    @Override
public E get(int index) {
return node(index).item;
}

删除节点

删除有两种情况,分别是删除指定位置的节点和根据数据找到对应的节点删除。

先来看第一种根据 index 删除节点:

先检验 index 是否合法,然后根据 index 找到待删除 node。这里的删除比较复杂,老铁们记得多画图来辅助理解,防止出现指针指向错误造成意想不到的结果。

    @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();
}

最难理解的就是后面这段代码了,但是只要多画图,多思考就好多了。

  1. x 节点就是当前要删除的节点数据,我们先把该节点保存的 item 以及 next 指针保存到临时变量。 第 10 ~13 这块 while 循环主要是用于找出被删除节点的 引用 cur 以及上一个节点的引用 prev。
  2. 在找到被删除的 cur 指针以及 cur 上一个节点指针 prev 后我们做删除操作,这里有三种情况:当链表只有一个节点的时候,当一个以上节点情况下分为删除头结点、尾节点、其他节点。
    /**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
final E item = x.item;
final Node<E> next = x.next;
Node<E> cur = head;
Node<E> prev = null;
// 寻找被删除 node cur 节点以及 cur 的上一个节点 prev
while (!cur.item.equals(item)) {
prev = cur;
cur = cur.next;
}
// 当只有一个节点的时候 prev = null,next = null
// 如果删除的是头结点,则 head = x.next,否则 prev.next = next 打断与被删除节点的联系
if (prev == null) {
head = next;
} else {
prev.next = next;
}
// 如果删除最后一个节点,则 last 指向 prev,否则打断被删除的节点 next = null
if (next == null) {
last = prev;
} else {
x.next = null;
} size--;
x.item = null;
return item;
}

单元测试

我们使用 junit做单元测试,引入maven 依赖。

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency> <dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency> </dependencies>

创建测试用例

package com.zero.linkedlist;

import com.zero.algorithms.linear.list.SingleLinkedList;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; @DisplayName("单向链表测试")
public class SingleLinkedListTest {
SingleLinkedList<Integer> singleLinkedList = new SingleLinkedList<>(); @BeforeAll
public static void init() {
} @AfterAll
public static void cleanup() {
} @BeforeEach
public void tearup() {
System.out.println("当前测试方法开始");
System.out.println(singleLinkedList.toString());
} @AfterEach
public void tearDown() {
System.out.println("当前测试方法结束");
System.out.println(singleLinkedList.toString());
} @DisplayName("add 默认末尾添加")
@Test
void testAdd() {
singleLinkedList.add(1);
singleLinkedList.add(2);
} @DisplayName("在指定位置添加 add")
@Test
void testAddIndex() {
singleLinkedList.add(0, 1);
singleLinkedList.add(0, 0);
singleLinkedList.add(1, 2);
} @DisplayName("addFirst 表头添加测试")
@Test
void testAddFirst() {
singleLinkedList.addFirst(0);
singleLinkedList.addFirst(1);
} @DisplayName("contains 测试")
@ParameterizedTest
@ValueSource(ints = {1})
void testContains(int e) {
singleLinkedList.add(e);
boolean contains = singleLinkedList.contains(e);
Assertions.assertTrue(contains);
} @DisplayName("testIndexOf")
@ParameterizedTest
@ValueSource(ints = {3})
void testIndexOf(int o) {
singleLinkedList.add(1);
singleLinkedList.add(2);
singleLinkedList.add(3);
int indexOf = singleLinkedList.indexOf(o);
Assertions.assertEquals(2, indexOf); } @DisplayName("test Get")
@Test
void testGet() {
singleLinkedList.addFirst(3);
singleLinkedList.addFirst(2);
singleLinkedList.addFirst(1);
Integer result = singleLinkedList.get(1);
Assertions.assertEquals(2, result);
} @DisplayName("testRemoveObject 删除")
@Test
void testRemoveObjectWithHead() {
singleLinkedList.add(1);
singleLinkedList.add(2);
singleLinkedList.add(3);
singleLinkedList.addFirst(0);
singleLinkedList.remove(0);
singleLinkedList.remove(Integer.valueOf(3));
singleLinkedList.remove(Integer.valueOf(2));
singleLinkedList.remove(Integer.valueOf(1));
} }

到这里单向链表的代码就写完了,我们一定要多写才能掌握指针打断的正确操作,尤其是在删除操作最复杂。

课后思考

  1. Java 中的 LinkedList 是什么链表结构呢?
  2. 如何 java 中的LinkedList 实现一个 LRU 缓存淘汰算法呢?

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

6L-单向链表实现的更多相关文章

  1. Reverse Linked List II 单向链表逆序(部分逆序)

    0 问题描述 原题点击这里. 将单向链表第m个位置到第n个位置倒序连接.例如, 原链表:1->2->3->4->5, m=2, n =4 新链表:1->4->3-& ...

  2. 【编程题目】输入一个单向链表,输出该链表中倒数第 k 个结点

    第 13 题(链表):题目:输入一个单向链表,输出该链表中倒数第 k 个结点.链表的倒数第 0 个结点为链表的尾指针.链表结点定义如下: struct ListNode {int m_nKey;Lis ...

  3. 输出单向链表中倒数第k个结点

    描述 输入一个单向链表,输出该链表中倒数第k个结点,链表的倒数第0个结点为链表的尾指针. 链表结点定义如下: struct ListNode { int       m_nKey; ListNode* ...

  4. Linus:利用二级指针删除单向链表

    Linus大神在slashdot上回答一些编程爱好者的提问,其中一个人问他什么样的代码是他所喜好的,大婶表述了自己一些观点之后,举了一个指针的例子,解释了什么才是core low-level codi ...

  5. 【转】Linus:利用二级指针删除单向链表

    原文作者:陈皓 原文链接:http://coolshell.cn/articles/8990.html 感谢网友full_of_bull投递此文(注:此文最初发表在这个这里,我对原文后半段修改了许多, ...

  6. C语言实现单向链表及其各种排序(含快排,选择,插入,冒泡)

    #include<stdio.h> #include<malloc.h> #define LEN sizeof(struct Student) struct Student / ...

  7. 数据结构——Java实现单向链表

    结点类: /** * @author zhengbinMac * 一个OnelinkNode类的对象只表示链表中的一个结点,通过成员变量next的自引用方式实现线性表中各数据元素的逻辑关系. */ p ...

  8. 输入一个单向链表,输出该链表中倒数第K个结点

    输入一个单向链表,输出该链表中倒数第K个结点,具体实现如下: #include <iostream> using namespace std; struct LinkNode { publ ...

  9. 单向链表JAVA代码

        //单向链表类 publicclassLinkList{       //结点类     publicclassNode{         publicObject data;         ...

  10. C++ 单向链表反转

    单向链表反转,一道常见的面试题,动手实现下. #include "stdafx.h" #include <stdlib.h> struct Node{ int data ...

随机推荐

  1. Elasticsearch数据建模笔记

    数据建模 数据建模是创建数据模型的过程 数据模型是对真实世界进行抽象描述的一种工具和方法,实现对现实世界的映射 三个过程:概念模型=>逻辑模型=>数据模型 数据模型:结合具体的数据库,在满 ...

  2. python如何用format进行进制转换与如何删除进制前缀

    10进行十进制,十六进制,八进制,二进制的转换:(#:保留进制前缀)对于带着进制前缀的,如"0x"“0o”“0b”,可以直接在后面加上’x’‘o’'b’进行删除:其他的一些小技巧: ...

  3. 13.浏览器屏幕缩放bug修复

    目录 问题:浏览器缩放时,轮播图显示不全,滚动水平滚动条,发现图片缺失 解决:隐藏水平滚动条,页面都只提供垂直滚动条的需求 问题:浏览器缩放时,轮播图显示不全,滚动水平滚动条,发现图片缺失 解决:隐藏 ...

  4. Jenkins的凭证管理

    Jenkins的凭证管理 什么是凭证? 凭证(cridential)是Jenkins进行受限操作时的凭据.比如使用SSH登录远程机器时,用户名和密码或SSH key就是凭证.而这些凭证不可能以明文写在 ...

  5. 03-Vue数据请求

    1. vue-resource vue-resource jsonp请求 <body> <div id="app"> <!-- v-model 监听表 ...

  6. 我的Keras使用总结(1)——Keras概述与常见问题整理

    今天整理了自己所写的关于Keras的博客,有没发布的,有发布的,但是整体来说是有点乱的.上周有空,认真看了一周Keras的中文文档,稍有心得,整理于此.这里附上Keras官网地址: Keras英文文档 ...

  7. Docker容器中使用Redis

    加载镜像 查询官方镜像及其版本信息 $ docker search redis 加载最新镜像 $ docker pull redis:lastest 查看本地镜像 $ docker images RE ...

  8. BigInteger实现除法取余

    BigInteger实现除法取余 BigInteger是什么? Java中,整形的最大范围是64位的long型整数.但是如果我们使用的整数超过了64位呢?这时候就用到了BigInteger.BigIn ...

  9. vuepress-theme-reco + Github Actions 构建静态博客,部署到第三方服务器

    最新博客链接 Github链接 查看此文档前应先了解,vuepress基本操作 参考官方文档进行配置: vuepress-theme-reco VuePress SamKirkland / FTP-D ...

  10. 「newbee-mall新蜂商城开源啦」GitHub 上最热门的 Spring Boot 项目,我也要做一次靓仔!

    没有一个冬天不可逾越,也没有一个春天不会到来. 介绍一下新蜂商城的近况,同时,新蜂商城 Vue 版本目前也在开发中,在这篇文章里我也向大家公布一下新蜂商城 Vue 版本的开发进度,和大家同步一下,在不 ...