您现在的位置是:首页 >学无止境 >[Data structure]单链表 | 一文介绍线性数据结构之一的单链表(Java实现)网站首页学无止境
[Data structure]单链表 | 一文介绍线性数据结构之一的单链表(Java实现)
⭐作者介绍:大二本科网络工程专业在读,持续学习Java,努力输出优质文章
⭐作者主页:@逐梦苍穹
⭐所属专栏:数据结构。数据结构专栏主要是在讲解原理的基础上拿Java实现
⭐如果觉得文章写的不错,欢迎点个关注一键三连😉有写的不好的地方也欢迎指正,一同进步😁
单链表
1、简介
单链表(Singly linked list)是一种常见的线性数据结构,它由若干个节点组成,每个节点都包含数据元素和一个指向下一个节点的指针。单链表中的第一个节点称为头节点,最后一个节点称为尾节点,尾节点的指针指向NULL。
单链表的优点在于,可以高效地进行节点的插入和删除操作,而不需要像数组那样频繁地移动数据元素。因此,单链表适用于频繁的插入和删除操作的场景。
下面是单链表的一些基本操作:
- 链表的创建:在创建链表时,需要首先创建头节点,并将其指针设置为NULL。然后逐个创建后续节点,并将前一个节点的指针指向当前节点,直到创建完整个链表。
- 链表的遍历:从头节点开始,逐个遍历链表中的节点,直到遍历到尾节点为止。
- 链表的插入:可以在链表的任意位置插入一个新节点。具体操作是先将新节点的指针指向其下一个节点,然后将前一个节点的指针指向新节点。
- 链表的删除:可以删除链表中的任意节点。具体操作是将待删除节点的前一个节点的指针指向待删除节点的下一个节点,然后释放待删除节点的内存空间。
单链表的时间复杂度如下:
- 链表的创建时间复杂度为O(n),其中n为链表中节点的个数。
- 链表的遍历时间复杂度为O(n),其中n为链表中节点的个数。
- 链表的插入时间复杂度为O(1)。
- 链表的删除时间复杂度为O(1)。
需要注意的是,单链表不支持随机访问,只能从头节点开始逐个遍历
,因此其查找操作的时间复杂度为O(n),其中n为链表中节点的个数。此外,由于单链表中每个节点只包含一个指针,因此其空间复杂度较低,是一种比较节省空间的数据结构。
2、实现单链表注意事项
在实现单链表的过程中,需要注意以下几点:
- 头结点的处理:在单链表中,通常会定义一个头结点,头结点不存储数据,只是为了方便操作链表。因此,在实现链表的过程中,需要特别处理头结点的位置,以及它的next引用。
- 节点的添加和删除:在添加或删除节点时,需要特别注意节点的前后关系,以及边界条件的处理。比如,插入节点时,需要遍历找到最后一个节点,然后在它的next引用处插入新节点;而删除节点时,需要找到待删除节点的前一个节点,然后将它的next引用指向待删除节点的下一个节点。
- 链表长度的维护:在添加或删除节点时,需要维护链表的长度。可以定义一个size变量,记录链表的长度。
- 内存管理:在使用单链表时,需要注意内存的管理,避免出现内存泄漏等问题。在删除节点时,需要将待删除节点的内存释放掉。
- 并发安全:在多线程环境下使用单链表时,需要考虑并发安全的问题。可以使用线程安全的容器类,或者在访问链表时使用锁来保证并发安全。
综上所述,实现单链表时需要注意以上几点,以保证链表的正确性和高效性。
3、代码思路总览
因为链表的头节点约定不动,所以后面很多方法都会使用一个辅助节点temp来辅助实现相应的功能。
辅助节点一开始是直接指向链表头节点的,也就是说,辅助节点和头节点指向的是同一块内存空间。
⭐而且增删改查的过程,指针定位的不是目标节点,而是目标节点的前一个节点。因为单链表是单向的,只能访问下一个指针域的内存地址,如果指向目标节点,那么就无法改变上一个节点指向的地址,会损伤链表结构
首先创建节点对象代码Node类,创建成员变量:唯一标识符id,节点的值value,节点Node类型的next(重点)以及重写toString方法。
然后创建单链表类SingleList,先初始化头节点,然后依次编写各种方法:获取头节点、添加节点(两种方式)、删除节点、修改节点、显示链表内容。[总体实现思路如下图所示]:
4、Node
重点说一下这里的重写toString方法。
可以使用IDEA自动生成重写后的toString代码,但是重写后的toString,在显示Node对象的时候,next会把后面所有的节点内容都带上,不便于阅读。
所以作出调整:如果是最后一个节点,则next的值为null;否则,直接打印next指向的地址。
this.getClass().getName() + "@" + Integer.toHexString(this.hashCode())
来自Object类的toString源码:
下面是Node的代码:
package linkedList.singleList;
/**
* @author 逐梦苍穹
* @date 2023/4/27 13:55
*/
public class Node{
public int id;
private int value;
private Node next;
public Node(int id,int value){
this.id = id;
this.value = value;
}
@Override
public String toString() {
if (this.next==null){
return "Node{" +
"id=" + this.id +
", value=" + this.value +
", next=" + null +
'}';
}
return "Node{" +
"id=" + this.id +
", value=" + this.value +
", next=" + this.getClass().getName() + "@" + Integer.toHexString(this.hashCode()) +
'}';
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
5、SingleList
SingleList的全部代码如下,下面循序渐进详细讲解:
package linkedList.singleList;
/**
* @author 逐梦苍穹
* @date 2023/4/27 14:07
*/
public class SingleList {
Node headNode = new Node(0, 0); //初始化头节点
/**
* 获取头节点
*/
public Node getHead() {
return headNode;
}
/**
* 添加节点(直接按调用顺序添加)
*/
public void addNode(Node node) {
Node temp = this.headNode;
while (true) {
if (temp.getNext() == null) {
break;
} else {
//辅助节点后移
temp = temp.getNext();
}
}
//跳出循环说明到达链表尾部,可以添加
temp.setNext(node);//把最后一个节点的next指针置为下一个要添加的节点的地址值
}
/**
* 根据id查找位置插入
*/
public void addNodeByOrder(Node node) {
Node temp = this.headNode;
while (true) {
if (temp.getNext() == null) {
temp.setNext(node);
break;
}
if (node.id < temp.getNext().id) {
node.setNext(temp.getNext());
temp.setNext(node);
break;
}
temp = temp.getNext();
}
}
/**
* 删除节点
*/
public void deleteNode(int id) {
Node temp = this.headNode;
if (temp.getNext() == null) {
System.out.println("链表空无法删除");
return;
}
while (true) {
if (temp.getNext() == null) break;
if (temp.getNext().id == id) {
temp.setNext(temp.getNext().getNext());
System.out.println("Node{id=" + id + "}:删除成功");
return;
}
temp = temp.getNext();
}
System.out.println("无此节点");
}
/**
* 修改节点
*/
public void updateNode(int id, int newValue) {
Node temp = this.headNode;
if (temp.getNext() == null) {
System.out.println("链表空");
}
while (true) {
if (temp.getNext() == null) break;
if (temp.getNext().id == id) {
temp.getNext().setValue(newValue);
System.out.println("Node{id=" + id + ", newValue=" + newValue + "}");
return;
}
temp = temp.getNext();
}
System.out.println("无此节点");
}
/**
* 打印整个链表内容
*/
public void outLinkedList() {
//方式一
Node temp = this.headNode.getNext();
if (temp == null) {
System.out.println("链表空");
return;
}
while (true) {
System.out.println(temp);
if (temp.getNext() == null) break;
temp = temp.getNext();
}
//方式二
// if (headNode.getNext() == null){
// System.out.println("链表空");
// }
// Node temp = this.headNode.getNext();
// while (true){
// if (temp == null){
// break;
// }
// System.out.println(temp);
// temp = temp.getNext();
// }
}
}
5.1、添加节点
添加节点有两种方式:直接添加到链表尾部、按唯一标识id按顺序添加
5.1.1、addNode
直接添加到链表尾部:先判断当前节点的next域是不是null,如果是,说明到达链表尾部,可以添加;如果不是,则链表指针需要后移,直到抵达尾部:
5.1.2、addNodeByOrder
按唯一标识id按顺序添加:这里在while中有两个if判断,一开始是指向头节点的,这时候链表如果为空,则直接添加数据即可;当链表不为空的时候,进入的是第二个if分支,由于每一次插入的时候,都是保持有序的,所以在插入节点的时候只需要判断:想要插入节点的位置的下一个节点的id大于想要插入节点的id即可。
插入的时候,比如要在节点A和C之间插入节点B。一开始A的next指向的是C,此时找到节点A,先把节点B的next置为C的地址,然后把A的next指向节点B的地址,即完成插入。
5.2、删除节点
删除节点的操作,同样需要一个辅助节点,指针同样需要指向目标节点的前一个节点。
被删除的节点,是不需要手动释放资源的,JVM有自动回收机制。
5.3、修改节点
5.4、显示链表内容
显示链表内容的思路,其实无非也就是遍历。这里采用的是while进行遍历,因为并不知道链表到底有多长,所以采用while遍历更合适。
遍历终止条件:链表为空 或者 到达最后一个节点(即在判断不为空的情况下出现next为null)
方式一:
方式二: