关注公众号 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,我第一时间解答。

最新文章

  1. textField设置输入文字距左边的距离
  2. 简单的跨平台c/c++日志记录
  3. ViewPager
  4. FastDFS实现文件上传下载实战
  5. Leetcode#115 Distinct Subsequences
  6. RAC 环境下修改归档模式
  7. hunnu 小明的烦恼——找字符串
  8. [Buffalo]MVC架构模式
  9. NSStirng、NSArray、以及枚举(Method小集合)
  10. iOS_SN_深浅拷贝( 百度的)_转载
  11. JQuery基础学习总结
  12. [置顶] 学习VB.NET编程最基本的三个问题
  13. IntelliJIDEA中如何使用JavaDoc
  14. 第二篇--上传git 代码
  15. C++ gui程序附加dos输出窗口
  16. C++与C的区别
  17. Python的字典dict和set
  18. 20145232 韩文浩 《Java程序设计》第3周学习总结
  19. mysql5.7.13.zip安装(windows)
  20. Print or Cout an Unsigned Char Variable 打印无符号字符

热门文章

  1. python字符串的常见操作
  2. mybatis进阶案例之多表查询
  3. c#序列化和反系列化json与类型对象转换
  4. Rational Rose 2007破解版
  5. DOM-XSS攻击原理与防御
  6. Elasticsearch 之聚合分析入门
  7. ubuntu 16.04服务器安装apache2 + php + mysql
  8. Natas0-34 Writeup
  9. Git在公司内部的使用规范
  10. shell脚本基础-语法