6L-单向链表实现
关注公众号 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 ? get(i)==null : 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();
}
}
添加元素
添加元素可能存在三种情况:
- 头结点添加。
- 中间任意节点添加。
- 尾节点添加。
在尾节点添加
将数据构造成 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();
}
最难理解的就是后面这段代码了,但是只要多画图,多思考就好多了。
x
节点就是当前要删除的节点数据,我们先把该节点保存的item
以及next
指针保存到临时变量。 第 10 ~13 这块 while 循环主要是用于找出被删除节点的 引用 cur 以及上一个节点的引用 prev。- 在找到被删除的 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));
}
}
到这里单向链表的代码就写完了,我们一定要多写才能掌握指针打断的正确操作,尤其是在删除操作最复杂。
课后思考
Java
中的LinkedList
是什么链表结构呢?- 如何
java
中的LinkedList
实现一个LRU
缓存淘汰算法呢?
加群跟我们一起探讨,欢迎关注 MageByte
,我第一时间解答。
6L-单向链表实现的更多相关文章
- Reverse Linked List II 单向链表逆序(部分逆序)
0 问题描述 原题点击这里. 将单向链表第m个位置到第n个位置倒序连接.例如, 原链表:1->2->3->4->5, m=2, n =4 新链表:1->4->3-& ...
- 【编程题目】输入一个单向链表,输出该链表中倒数第 k 个结点
第 13 题(链表):题目:输入一个单向链表,输出该链表中倒数第 k 个结点.链表的倒数第 0 个结点为链表的尾指针.链表结点定义如下: struct ListNode {int m_nKey;Lis ...
- 输出单向链表中倒数第k个结点
描述 输入一个单向链表,输出该链表中倒数第k个结点,链表的倒数第0个结点为链表的尾指针. 链表结点定义如下: struct ListNode { int m_nKey; ListNode* ...
- Linus:利用二级指针删除单向链表
Linus大神在slashdot上回答一些编程爱好者的提问,其中一个人问他什么样的代码是他所喜好的,大婶表述了自己一些观点之后,举了一个指针的例子,解释了什么才是core low-level codi ...
- 【转】Linus:利用二级指针删除单向链表
原文作者:陈皓 原文链接:http://coolshell.cn/articles/8990.html 感谢网友full_of_bull投递此文(注:此文最初发表在这个这里,我对原文后半段修改了许多, ...
- C语言实现单向链表及其各种排序(含快排,选择,插入,冒泡)
#include<stdio.h> #include<malloc.h> #define LEN sizeof(struct Student) struct Student / ...
- 数据结构——Java实现单向链表
结点类: /** * @author zhengbinMac * 一个OnelinkNode类的对象只表示链表中的一个结点,通过成员变量next的自引用方式实现线性表中各数据元素的逻辑关系. */ p ...
- 输入一个单向链表,输出该链表中倒数第K个结点
输入一个单向链表,输出该链表中倒数第K个结点,具体实现如下: #include <iostream> using namespace std; struct LinkNode { publ ...
- 单向链表JAVA代码
//单向链表类 publicclassLinkList{ //结点类 publicclassNode{ publicObject data; ...
- C++ 单向链表反转
单向链表反转,一道常见的面试题,动手实现下. #include "stdafx.h" #include <stdlib.h> struct Node{ int data ...
随机推荐
- MySQL 【教程一】
前言 什么是数据库? 数据库(Database)是按照数据结构来组织.存储和管理数据的仓库. 每个数据库都有一个或多个不同的 API 用于创建,访问,管理,搜索和复制所保存的数据. 我们也可以将数据存 ...
- 使用StreamHttpResponse和FileResponse下载文件的注意事项及文件私有化
为什么需要编写下载视图方法? 你或许知道,我们上传的文件默认放在media文件夹中的,且Django会为每个上传的静态文件分配一个静态url.在模板中,你可以使用{{ mymodel.file.url ...
- Python之locust踩坑指北
坑点1:locust安装报错 其中一种情况:error:Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visua ...
- 痞子衡嵌入式:恩智浦SDK驱动代码风格检查工具预览版
大家好,我是痞子衡,是正经搞技术的痞子. 接上文 <恩智浦SDK驱动代码风格.模板.检查工具> 继续聊,是的,过去的三天里我花了一些时间做了一个基于 PyQt5 的 GUI 工具,可以帮助 ...
- Python Django撸个WebSSH操作Kubernetes Pod
优秀的系统都是根据反馈逐渐完善出来的 上篇文章介绍了我们为了应对安全和多分支频繁测试的问题而开发了一套Alodi系统,Alodi可以通过一个按钮快速构建一套测试环境,生成一个临时访问地址,详细信息可以 ...
- Servlet(简介,请求参数,页面跳转,生命周期,创建,配置,ServletContext,线程)
1.Servlet简介 servlet是java servlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序, 主要功能在于交互式浏览和修改数据,生成动态的web内容 服务端运行的 ...
- JS事件流模型
JS事件流模型 事件捕获Event Capturing是一种从上而下的传播方式,以click事件为例,其会从最外层根节向内传播到达点击的节点,为从最外层节点逐渐向内传播直到目标节点的方式. 事件冒泡E ...
- 关于git 远程仓库账户密码错误的问题
主要是电脑凭证把第一次输入的账户密码记录了下来,在控制面板->用户账户->凭据管理器里, 选择windows凭证, 你会找到git:凭据,直接删掉或者更改都可以! 对应的Git的凭证,删除 ...
- 微信小程序api封装
写多 之后,吸取之前的经验,瞎写了一个简单的封装api,有幸看到的朋友,就随便看看哈,如果能给到你帮助就一直棒了,额呵呵呵! 新建constant.js和api.js文件 在constant.js中统 ...
- [dp+博弈]棋盘的必胜策略
链接:https://ac.nowcoder.com/acm/problem/21797来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32768K,其他语言65536K ...