关注公众号 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. 峰哥说技术:04-Spring Boot基本配置

    Spring Boot深度课程系列 峰哥说技术—2020庚子年重磅推出.战胜病毒.我们在行动 04 Spring Boot基本配置 1)容器的相关配置 在Spring Boot中可以内置Tomcat. ...

  2. 关于JS的数据类型与转化(自动与强制)

    在我们谈到JS的数据类型转化时,一定会知道分为自动转化和强制转化两种方式吧,通俗来讲,自动就是在某种条件下,电脑浏览器自己会把其他类型的数据转化为相应的数据类型,而强制则是咋们程序员应该手动来做的了, ...

  3. 基于 HTML + WebGL 结合 23D 的疫情地图实时大屏 PC 版【转载】

    前言 2019年12月以来,湖北省武汉市陆续发现了多例肺炎病例,现已证实为一种新型冠状病毒感染引起的急性呼吸道传染病并蔓延全国,肺炎疫情牵动人心,人们每天起来第一件事变成了关注疫情进展,期望这场天灾早 ...

  4. 曹工说Spring Boot源码(23)-- ASM又立功了,Spring原来是这么递归获取注解的元注解的

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  5. 爬虫 | cnblog文章收藏排行榜(“热门文摘”)

    目录 需要用的module 单页测试 批量抓取 数据保存 背景说明 因为加入cnblog不久,发现上面有很多优秀的文章. 无意中发现cnblog有整理文章的收藏排行榜,也就是热门文摘. 不过有点坑的是 ...

  6. JWT签发token

    目录 一. 认证的发展历程简介 二. JWT签发Token源码分析 2.1 JWT工作原理及简介 2.2 JWT生成token源码分析 返回目录 一. 认证的发展历程简介 这里真的很简单的提一下认证的 ...

  7. 第十周Java实验作业

    实验十  泛型程序设计技术 实验时间 2018-11-1 1.实验目的与要求 (1) 理解泛型概念: 泛型:也称参数化类型,就是在定义类,接口和方法时,通过类型参数只是将要处理的类型对象.(如Arra ...

  8. 欢乐C++ —— 2. 深复制与浅复制

    1. 简述 ​ 通俗点讲,深复制与浅复制一般对指针而言, ​ 深复制复制指针所指向的内容, ​ 浅复制复制指针的值. 2. 举例 ​ 栗子: ​ 当我们有现在有指针A指向一块数据,和指针B. 深复制- ...

  9. Ansible Playbook 初识

    Ansible Playbook 基本概述与使用案例 主机规划 添加用户账号 说明: 1. 运维人员使用的登录账号: 2. 所有的业务都放在 /app/ 下「yun用户的家目录」,避免业务数据乱放: ...

  10. mysql两表合并,对一列数据进行处理

    加班一时爽,一直加班~一直爽~  欢迎收看http://www.996.icu/ 今天弄了下MySQL中两表合并的并且要处理一列数据,这列数据原来都是小写字母,处理时将这列数据改成驼峰命名的~~ 基本 ...