简单介绍下单向链表
单向链表(单链表)是链表的一种,其特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始;链表是使用指针进行构造的列表;又称为结点列表,因为链表是由一个个结点组装起来的;其中每个结点都有指针成员变量指向列表中的下一个结点;
链表是由结点构成,head指针指向第一个成为表头结点,而终止于最后一个指向NULL的指针。
单向链表和双向链表简单对比下
jdk(目前所用8)中的LinkedList是用双向链表实现的
下图中,有头节点,尾结点,链表长度

其节点代码:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
双向链表较于单向链表:
- 单向链表只暴露头结点,尾结点指向null,每个节点Node只存储一个数据data和一个指向下个节点的next
- 而双向链表暴露头节点和尾结点,尾结点指向头结点,每个节点Node存储一个数据data和一个指向上个节点的prev和下个节点的next
- 双向链表实现是采用二分法,效率比单向链表要高些
- 但是由于双向链表的每个节点都比单向链表多存储一个prev节点,所以占用的内存空间就大,因此实际使用中,单向链表的使用率要比双向链表高
- 链表的插入删除效率比数组快,查找的效率比数组慢,是由于他每次查找都是要遍历的
- 数组存储时用一块连续的内存,而链表不需要在物理上相连
如果还不是很清楚,请跟着我,亲自实现一个单向链表.
——好记性不如烂笔头,同样的对对我们小猿员也是,自己动手比单纯的阅读理解,更迅速,记忆更牢靠
今天就用java实现一个单向链表,包括链表的增删改查
整个单向链表类的结构
/**
* @author qinlei
* @description 自定义单向链表
* @date 2021/2/1 9:49
*/
public class Linked<T> {
/**
* 向外暴露的头节点
*/
private Node head;
/**
* 链表的长度
*/
private int size;
/**
* 初始化链表
*/
public Linked() {
this.size = 0;
this.head = null;
}
/**
* 链表中的节点
*/
private class Node {
private Node next = null;//节点的引用,即下一个节点
private T data;//节点对象
public Node(T data) {
this.data = data;
}
public Node(T data, Node next) {
this.next = next;
this.data = data;
}
}
//some method 增删改查等
}
我简单画一个单向链表结构图
其实下图的是网上较多的

链表的遍历
- 链表本质上是一个线性表
- 顺序表(如数组)可以随机读取,由于其在逻辑及物理上是有顺序(索引)
- 链表是顺序表,其在逻辑层及物理层上都是无序的,可能存在于内存中的任何地方,因此在遍历时,必须从头开始。如:需要找到第i个元素,则需要从0开始遍历直至找到(i-1)个元素
链表的操作方法
对于增删,可以这么理解,你将单向链表想象成一条绳子,新增需要将在绳子某处剪断,将新增的绳子与剪断的绳子缝合,即3根有顺序的绳子连接
新增
新增整体的思路:
- 将新增的队列将新增的位置index找到
- 找到该位置的前节点和后节点
- 将该位置的前节点指向新增的节点,将该新增的节点指向后节点
这里就有个问题,当新增的是作为头结点时,会出现没有前节点的情况,所以,这种情况,要单独拿出来
代码:
/**
* 向头部插入
*
* @param data
*/
public void addFirstNode(T data) {
Node node = new Node(data);
//将node的next节点指向链表头结点
node.next = this.head;
//将该链表的头结点指向node
this.head = node;
//链表长度++
this.size++;
}
/**
* 链表尾部追加
*
* @param data
*/
public void addLastNode(T data) {
addNode(data, this.size);
}
/**
* 向链表中间插入节点
*
* @param d
*/
public void addNode(T d, int index) {
//表明插入的范围是0~size 0表示头部插入 size表示尾部插入 当参数不合标准时抛出参数异常
if (index < 0 || index > size) throw new IllegalArgumentException("index illegal !");
if (index == 0) {
addFirstNode(d);
return;
}
//查找index在链表的位置
Node preNode = head;//待插入节点的前节点
for (int i = 0; i < index - 1; i++) {
preNode = preNode.next;
}
Node next = preNode.next;//待插入节点的后节点
Node node = new Node(d, next);
preNode.next = node;
size++;
}
删除
删除整体思路:同新增一样需要找到前节点和后节点,独立出来头结点.
所有的删除都需要对链表进行校验
/**
* 校验链表是否存在(头存在/长度存在)
*
* @return
*/
private boolean checkExist() {
return Optional.ofNullable(head).isPresent() && size > 0;
}
/**
* 删除节点
*
* @param data 要删除的节点数据
*/
public void removeNode(T data) {
if (!checkExist()) return;
if (Objects.equals(head.data, data)) {
head = head.next;
size--;
return;
}
Node tmp = head;
//根据数据查找节点
while (Optional.ofNullable(tmp).isPresent() && Optional.ofNullable(tmp.next).isPresent()) {
if (Objects.equals(tmp.next.data, data)) {
tmp.next = tmp.next.next;
break;
}
tmp = tmp.next;
}
}
/**
* 删除头部节点
*/
public void removeFirstNode() {
if (!checkExist()) throw new NullPointerException("the linked is null");
head = head.next;
size--;
}
/**
* 删除尾部节点
*/
public void removeLastNode() {
if (!checkExist()) throw new NullPointerException("the linked is null");
if (size == 1) removeFirstNode();
Node tmp = this.head;
while (Optional.ofNullable(tmp).isPresent() && Optional.ofNullable(tmp.next).isPresent()) {
if (!Optional.ofNullable(tmp.next.next).isPresent()) {
tmp.next = null;
size--;
}
tmp = tmp.next;
}
}
修改
/**
*
* @param oldData 旧数据
* @param newData 新数据
* @return
*/
public Linked<T> updateNode(T oldData,T newData){
if (!checkExist()) throw new NullPointerException("the linked is null");
Node tmp = head;
while (Optional.ofNullable(tmp).isPresent()) {
if (Objects.equals(tmp.data,oldData)) {
tmp.data = newData;
}
tmp = tmp.next;
}
return this;
}
总结下
其实纵观修改和删除,其核心就是通过遍历比对数据data,不能像数组那样通过索引来直接操作
完整代码
package com.ql;
import java.util.Objects;
import java.util.Optional;
/**
* @author qinlei
* @description 自定义单向链表
* @date 2021/2/1 9:49
*/
public class Linked<T> {
/**
* 向外暴露的头节点
*/
private Node head;
/**
* 链表的长度
*/
private int size;
/**
* 每次初始化都是新创建的链表
*/
public Linked() {
this.size = 0;
this.head = null;
}
/**
* 链表中的节点
*/
private class Node {
private Node next = null;//节点的引用,即下一个节点
private T data;//节点对象
public Node(T data) {
this.data = data;
}
public Node(T data, Node next) {
this.next = next;
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"next=" + next +
", data=" + data +
'}';
}
}
/**
* 向头部插入
*
* @param data
*/
public void addFirstNode(T data) {
Node node = new Node(data);
//将node的next节点指向链表头结点
node.next = this.head;
//将该链表的头结点指向node
this.head = node;
//链表长度++
this.size++;
}
/**
* 链表尾部追加
*
* @param data
*/
public void addLastNode(T data) {
addNode(data, this.size);
}
/**
* 向链表中间插入节点
*
* @param d
*/
public void addNode(T d, int index) {
//表明插入的范围是0~size 0表示头部插入 size表示尾部插入
if (index < 0 || index > size) throw new IllegalArgumentException("index illegal !");
if (index == 0) {
addFirstNode(d);
return;
}
//查找index在链表的位置
Node preNode = head;//待插入节点的前节点
for (int i = 0; i < index - 1; i++) {
preNode = preNode.next;
}
Node next = preNode.next;//待插入节点的后节点
Node node = new Node(d, next);
preNode.next = node;
size++;
}
/**
* 删除节点
*
* @param data 要删除的节点数据
*/
public void removeNode(T data) {
if (!checkExist()) return;
if (Objects.equals(head.data, data)) {
head = head.next;
size--;
return;
}
Node tmp = head;
//根据数据查找节点
while (Optional.ofNullable(tmp).isPresent() && Optional.ofNullable(tmp.next).isPresent()) {
if (Objects.equals(tmp.next.data, data)) {
tmp.next = tmp.next.next;
break;
}
tmp = tmp.next;
}
}
/**
* 删除头部节点
*/
public void removeFirstNode() {
if (!checkExist()) throw new NullPointerException("the linked is null");
head = head.next;
size--;
}
/**
* 删除尾部节点
*/
public void removeLastNode() {
if (!checkExist()) throw new NullPointerException("the linked is null");
if (size == 1) removeFirstNode();
Node tmp = head;
while (Optional.ofNullable(tmp).isPresent() && Optional.ofNullable(tmp.next).isPresent()) {
if (!Optional.ofNullable(tmp.next.next).isPresent()) {
tmp.next = null;
size--;
}
tmp = tmp.next;
}
}
/**
*
* @param oldData 旧数据
* @param newData 新数据
* @return
*/
public Linked<T> updateNode(T oldData,T newData){
if (!checkExist()) throw new NullPointerException("the linked is null");
Node tmp = head;
while (Optional.ofNullable(tmp).isPresent()) {
if (Objects.equals(tmp.data,oldData)) {
tmp.data = newData;
}
tmp = tmp.next;
}
return this;
}
/**
* 判断链表是否为空
* @return
*/
public boolean isEmpty(){
return !checkExist();
}
/**
* 判断是否包含该元素
*
* @param data 元素数据
* @return 布尔 包含为true 否则为false
*/
public boolean contains(T data) {
if (!checkExist()) return false;
Node tmp = head;
while (Optional.ofNullable(tmp).isPresent()) {
if (Objects.equals(tmp.data, data)) {
return true;
}
tmp = tmp.next;
}
return false;
}
/**
* 校验链表是否存在(头存在/长度存在)
*
* @return
*/
private boolean checkExist() {
return Optional.ofNullable(head).isPresent() && size > 0;
}
/**
* 暴露出去 节点的长度
*
* @return
*/
public int getSize() {
return size;
}
@Override
public String toString() {
return "Linked{" +
"head=" + head +
'}';
}
}
测试代码
package com.ql;
/**
* @author qinlei
* @description todo
* @date 2021/2/2 11:41
*/
public class LinkedTest {
public static void main(String[] args) {
Linked<Integer> linked = new Linked<>();
//测试头部添加
linked.addFirstNode(5);
System.out.println(linked);
//测试尾部添加
linked.addLastNode(1);
System.out.println(linked);
//测试中间添加
linked.addNode(6,1);
System.out.println(linked);
//测试修改
linked.updateNode(6,10);
System.out.println(linked);
//测试是否包含
System.out.println(linked.contains(2));
System.out.println(linked.contains(1));
//测试删除头部数据
linked.removeFirstNode();
System.out.println(linked);
//测试删除尾部数据
linked.removeLastNode();
System.out.println(linked);
//测试删除任意位置数据
linked.removeNode(6);
System.out.println(linked);
}
}
后记
当然,如果在新增及删除时,没有size这个变量,那么可以通过,带新增(删除)的前节点/后节点来通过数据data的比对操作
这样的操作也可以叫做虚拟节点操作
参考
本文的学习参考于:
Java基础–单链表的实现——唐·吉坷德
1979

被折叠的 条评论
为什么被折叠?



