在本文中,我们将基于 MySingleLinkedList
类,深入探讨单链表的实现,包括创建、插入、删除等核心操作,同时分享完整的代码示例。单链表是一种灵活的数据结构,适用于处理需要频繁插入和删除操作的场景,例如实现队列 📜、链式栈 🔥、循环队列 🔄 等。在计算机科学和实际开发中,链表结构被广泛应用。它不仅可以高效地处理插入和删除操作,还能够根据实际需求实现不同的链表结构。🚀本篇不仅涵盖了基本功能的代码展示,还补充了各种操作的实现细节和优化建议,旨在让读者能够全面掌握单链表的使用与实现。😊💡
目录
a. 链表的基本操作 🔧
b. 插入操作 ➕
c. 删除操作 ❌
链表的基本结构 📊
单链表由节点(ListNode
)组成,每个节点包含三个部分:
- 值域(val):存储节点的值。💎
- 指针域(next):指向下一个节点。👉
- 前驱指针(prev):尽管在单链表中未实际使用,它可以支持双向链表的扩展。🔄
代码实现 🖥️
以下是链表的基础结构代码:
public class MySingleLinkedList {
static class ListNode {
public ListNode next;
public int val;
public ListNode prev;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head; // 头节点 🧑💻
public ListNode last; // 尾节点 🎯
}
讲解 🧑🏫
- 每个节点由
ListNode
类表示,包含数据域val
和两个指针域next
和prev
。 head
用于标记链表的起始位置,last
标记链表的尾节点(可选)。- 相较于数组,链表在插入和删除操作中更具灵活性,但随机访问性能较差。链表的插入和删除操作无需移动其他元素,且通过指针的变动即可实现动态扩展,极大提高了操作效率。
链表的基本操作 🔧
1. 链表的创建 🛠️
手动链接节点的方式可以快速构建一个简单的链表。我们通过手动指定每个节点的连接方式,构造一个简单的链表。它适用于学习和理解链表基本结构:
public void createList() {
ListNode listNode1 = new ListNode(1);
ListNode listNode2 = new ListNode(2);
ListNode listNode3 = new ListNode(3);
ListNode listNode4 = new ListNode(4);
ListNode listNode5 = new ListNode(5);
this.head = listNode1;
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
listNode4.next = listNode5;
listNode5.next = null;
}
动态创建链表 💻
动态创建链表可以根据输入动态生成节点并构建链表,这样不仅增加了代码的灵活性,而且便于应对不同长度和结构的链表需求:
public void createListDynamic(int[] values) {
if (values == null || values.length == 0) {
return;
}
this.head = new ListNode(values[0]);
ListNode cur = this.head;
for (int i = 1; i < values.length; i++) {
cur.next = new ListNode(values[i]);
cur = cur.next;
}
}
讲解 💡
- 动态连接节点:
node1.next = node2
是构建链表的核心逻辑,通过该步骤,node1
的next
指向了node2
,形成了节点之间的连接,链表的扩展就是通过这种方式完成的。🎯这种方式也适用于链表的动态增长,在需要时可以随时添加新的节点。 - 灵活性:动态方法使得链表的创建不再依赖固定的结构,可以根据用户的输入或其他需求动态生成节点,极大提高了代码的通用性。
2. 链表的遍历 🔍
通过遍历链表打印节点值,同时考虑链表为空时的情况,避免出现空指针异常。遍历链表时,需要从头节点开始依次访问每个节点:
public void display() {
if (head == null) {
System.out.println("链表为空");
return;
}
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
讲解 📚
- 动态连接节点:
node1.next = node2
是构建链表、插入节点的基础,解决了链表动态扩展问题。 - 安全遍历链表:
while (cur != null)
确保链表遍历过程可靠且高效,避免了指针错误或无限循环的问题。这样可以避免遍历时访问空指针或进入无限循环。⚠️ - 前进推进的流畅性:
cur = cur.next
是链表遍历的核心语句,它使得cur
从当前节点指向下一个节点,实现了链表的逐步推进。这个简洁的推进逻辑符合链表的递归结构特性,也使得代码更易于理解。😊
3. 获取链表长度 🔢
链表的长度可以通过遍历链表的所有节点来计算,直到达到链表的末尾:
public int size() {
ListNode cur = head;
int length = 0;
while (cur != null) {
length++;
cur = cur.next;
}
return length;
}
讲解 🧐
- 递增计数:每遍历一个节点,计数器
length
加 1,直至链表末尾,遍历过程确保了准确的节点统计。📊 - 优化建议:可以增加一个
size
属性,在链表增删时动态更新,避免每次都需要遍历整个链表来计算长度。
插入操作 ➕
1. 头插法 🏁
public void addFirst(int val) {
ListNode listNode = new ListNode(val);
listNode.next = head;
head = listNode;
}
讲解 ⚡
- 直接插入头部:将新节点的
next
指向当前头节点,并更新head
指针,使新节点成为链表的头节点。这种方式的时间复杂度为O(1)
,是最为高效的插入方法,尤其适用于栈结构等需要优先级插入的场景。🔝
2. 尾插法 🔚
public void addLast(int val) {
ListNode listNode = new ListNode(val);
if (head == null) {
head = listNode;
return;
}
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = listNode;
}
讲解 📈
- 遍历尾部:通过遍历找到尾节点,将其
next
指向新节点。尾插法的时间复杂度是O(n)
,如果链表较长,效率较低。⏳ - 优化建议:通过维护尾指针
last
,可以直接将新节点插入尾部,避免每次遍历链表,提升效率。⚙️
3. 任意位置插入 🔄
public void addIndex(int index, int val) {
if (index < 0 || index > this.size()) {
System.out.println("索引不合法");
return;
} else if (index == 0) {
this.addFirst(val);
return;
} else if (index == this.size()) {
this.addLast(val);
return;
} else {
ListNode cur = findIndexSubOne(index);
ListNode node = new ListNode(val);
node.next = cur.next;
cur.next = node;
}
}
private ListNode findIndexSubOne(int index) {
int count = 0;
ListNode cur = head;
while (count != index - 1) {
cur = cur.next;
count++;
}
return cur;
}
讲解 📘
- 定位前驱节点:通过
findIndexSubOne
方法找到目标索引的前驱节点,便于在目标位置进行插入。 - 边界处理:针对索引为 0 或链表尾部的情况,分别调用头插法或尾插法。对其他位置的插入需要找到前驱节点,并调整指针,使得新节点被插入到正确的位置。🎯
删除操作 ❌
1. 删除第一个匹配的节点
public void remove(int val) {
if (head == null) return;
if (head.val == val) {
head = head.next;
return;
}
ListNode cur = head;
while (cur.next != null) {
if (cur.next.val == val) {
cur.next = cur.next.next;
return;
}
cur = cur.next;
}
}
讲解 🧠
- 遍历匹配:从头开始遍历链表,查找目标值所在节点。找到匹配节点后,通过调整前后节点的
next
指针,实现删除。 - 特殊情况:若目标值位于头节点,则直接更新
head
指针,避免访问无效的null
指针。⚠️
2. 删除所有匹配的节点
public void removeAllKey(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
if (cur == head) {
head = head.next;
} else {
cur.prev.next = cur.next;
if (cur.next != null) {
cur.next.prev = cur.prev;
}
}
}
cur = cur.next;
}
}
讲解 🔍
- 批量删除:遍历链表并删除所有匹配的节点。
- 双向链表的优势:通过
prev
指针更方便地更新节点关系,实现更高效的删除操作。⚡
3. 清空链表
public void clear() {
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = null;
cur.prev = null;
cur = next;
}
head = last = null;
}
讲解 💬
- 逐一断开连接:通过遍历依次清除每个节点的引用,确保没有悬挂引用。
- 释放内存:同时将头尾指针设为
null
,彻底清空链表。🚀
总结 🌟
通过本文的讲解,我们实现了以下操作:
- 链表的创建和遍历。
- 插入操作:头插、尾插和任意位置插入。
- 删除操作:删除单个节点、删除所有匹配节点,以及清空链表。
链表作为基础数据结构,适用于动态插入和删除频繁的场景,但其随机访问性能较差。由于链表无法通过索引直接访问特定位置的节点,需要从头节点开始逐一遍历,因此在随机访问频繁的场景中可能不如数组高效。在实际开发中,链表的灵活性可以解决许多复杂的数据管理问题。希望这篇文章能帮助您更好地理解单链表的实现,也为后续学习更复杂的链表结构(如双向链表和循环链表)打下基础!😊🎉