java:单链表基础操作:插入、删除、移动节点
java:单链表基础操作:插入、删除、移动节点
1 前言
在Java中实现单链表插入节点,需根据插入位置(头部、尾部、中间)设计逻辑。同时探讨单链表的节点删除、移动操作。
2 使用
2.1 单链表
2.1.1 定义单链表节点类,并实现链表节点插入
public class ListNode {int val;ListNode next;public ListNode(int val) {this.val = val;this.next = null;}
}
单链表的插入操作定义:
public class SingleLinkedList {ListNode head;/*** 头插法*/public void insertHead(int val) {ListNode node = new ListNode(val);node.next = head;head = node;}/*** 尾插法*/public void insertTail(int val) {ListNode node = new ListNode(val);if (head == null) {head = node;return;}ListNode current = head;while (current.next != null) {current = current.next;}current.next = node;}/*** 按照索引值,指定位置插入节点*/public void insertWithIndex(int pos, int val) {if (pos < 0)throw new IllegalArgumentException("索引值错误,不能小于0.");if (pos == 0) {insertHead(val);return;}ListNode node = new ListNode(val);ListNode current = head;if(current == null) {head = node;return;}for (int i = 0; current.next != null && i < pos - 1; i++) {current = current.next;}node.next = current.next;current.next = node;}public void print() {ListNode current = head;System.out.println("打印链表节点数据:");if(current != null) {System.out.print(current.val);while (current.next != null) {System.out.print("=>" + current.next.val);current = current.next;}System.out.println("打印链表节点结束。");}}}
- 头部插入:创建新节点,将其指向当前头节点,然后更新头节点为新节点。
- 尾部插入:遍历到链表末尾,将末尾节点的next指向新节点。
- 指定位置插入: 处理索引为0的情况,直接调用头部插入。 遍历到目标位置的前一个节点,调整指针将新节点插入。
测试类:
public class TestSingleLinkedList {public static void main(String[] args) {SingleLinkedList singleLinkedList = new SingleLinkedList();singleLinkedList.insertHead(9);singleLinkedList.insertHead(1);singleLinkedList.insertTail(4);singleLinkedList.insertWithIndex(2, 7);singleLinkedList.print();}}
执行结果如下:
打印链表节点数据:
1=>9=>7=>4打印链表节点结束。
例子:
public static void main(String[] args) {SingleLinkedList singleLinkedList = new SingleLinkedList();singleLinkedList.insertWithIndex(6, 6);singleLinkedList.print();
}
执行结果如下:
打印链表节点数据:
6打印链表节点结束。
再举个例子:
public static void main(String[] args) {SingleLinkedList singleLinkedList = new SingleLinkedList();singleLinkedList.insertWithIndex(6, 6);singleLinkedList.insertWithIndex(6, 9);singleLinkedList.print();
}
结果如下:
打印链表节点数据:
6=>9打印链表节点结束。
- 边界条件:插入位置为0或链表末尾时需特殊处理。
- 异常处理:当索引无效时(如负数或超出长度),抛出异常确保程序健壮性。
- 时间复杂度:
头部插入
为O(1)
,尾部和指定位置插入
为O(n)
。 - 空间复杂度:
头部插入
(只需创建一个新节点,并修改指针指向。无论链表多长,额外内存占用恒定,仅一个新节点和固定数量的临时指针)为O(1)
,尾部和指定位置插入
(虽然需要遍历链表找到尾部(时间复杂度 O(n)),但空间占用仅为一个新节点和一个临时指针(如 current),与链表长度无关)为O(1)
。
2.1.2 如何优化上述的单链表节点插入操作?
在 Java 链表的操作中,哨兵节点(Sentinel Node) 是一种简化边界条件处理的技巧,常用于避免对头节点(head)进行特殊判断。
哨兵节点的核心作用
- 统一操作逻辑
无论链表是否为空,添加或删除节点时都可以用相同的代码逻辑处理,避免对 head == null 的特殊判断。
- 简化头节点的修改
当需要修改头节点时(如插入或删除头节点),无需单独处理头指针的变化,只需通过哨兵节点的 next 指针操作。
- 防止空指针异常
在处理空链表时,哨兵节点作为占位符,确保代码不会因操作 null 而崩溃。
public class NewSingleLinkedList {ListNode head;/*** @param val 头插法,节点值*/public void insertHead(int val) {ListNode node = new ListNode(val);node.next = head;head = node;}/*** @param val 尾插法,节点值*/public void insertTail(int val) {ListNode node = new ListNode(val);ListNode sentry = new ListNode(-99);sentry.next = head;ListNode current = sentry;int i = 0;for(; current.next != null; i++) {current = current.next;}current.next = node;head = sentry.next;}/*** @param pos 按照索引值,指定插入的位置* @param val 插入的链表节点值*/public void insertWithIndex(int pos, int val) {if(pos < 0)throw new IllegalArgumentException("索引值不能小于0.");ListNode node = new ListNode(val);ListNode sentryNode = new ListNode(-99);sentryNode.next = head;ListNode current = sentryNode;int i = 0;for(; current.next != null && i < pos; i++) {current = current.next;}node.next = current.next;current.next = node;head = sentryNode.next;}public void print(){ListNode current = head;System.out.println("打印链表节点数据:");if(current != null) {System.out.print(current.val);while (current.next != null) {System.out.print("=>" + current.next.val);current = current.next;}System.out.println("打印链表节点结束。");}}}
public class TestNewSingleLinkedList {public static void main(String[] args) {NewSingleLinkedList singleLinkedList = new NewSingleLinkedList();singleLinkedList.insertHead(9);singleLinkedList.insertHead(1);singleLinkedList.insertTail(4);singleLinkedList.insertWithIndex(2, 7);singleLinkedList.insertWithIndex(0, 8);singleLinkedList.insertWithIndex(8, 5);singleLinkedList.print();}}
结果如下:
打印链表节点数据:
8=>1=>9=>7=>4=>5打印链表节点结束。
哨兵节点的优缺点
- 优点
代码简洁,减少边界条件判断。
提高可读性和可维护性。
- 缺点
轻微的内存开销(多一个节点)。
需注意返回结果时跳过哨兵节点(如返回 sentinel.next)。
2.2.1 实现单链表节点删除
思路:
- 删除头节点:
检查链表是否为空,若为空则直接返回。
将头节点指向当前头节点的下一个节点。
- 删除尾节点:
检查链表是否为空,若为空则返回。
若链表只有一个节点,则将头节点置空。
遍历链表直到倒数第二个节点,将其next指针置空。
上述即,如果需要删除链表节点,删除单链表头结点
,只需要将头节点head指向头结点的下一个节点,即head.next,时间复杂度为O(1)
;如果是删除单链表的尾结点
,需要遍历链表,找到尾结点的前驱结点
,将前驱结点prev.next指向tail.next即可,时间复杂度为O(n)
。
public class DeleteSingleLinkedList extends NewSingleLinkedList{/*** 删除单链表头节点*/public void deleteHead() {if(head == null) return;head = head.next;}/*** 删除单链表尾结点*/public void deleteTail() {if(head == null || head.next == null) {head = null;return;}ListNode prev = head;while(prev.next.next != null) {prev = prev.next;}prev.next = null;}public static void main(String[] args) {DeleteSingleLinkedList deleteSingleLinkedList = new DeleteSingleLinkedList();deleteSingleLinkedList.insertHead(4);deleteSingleLinkedList.insertTail(9);deleteSingleLinkedList.insertWithIndex(1, 6);deleteSingleLinkedList.print();deleteSingleLinkedList.deleteTail();deleteSingleLinkedList.print();deleteSingleLinkedList.insertHead(7);deleteSingleLinkedList.insertTail(1);deleteSingleLinkedList.print();deleteSingleLinkedList.deleteHead();deleteSingleLinkedList.print();}
}
执行结果:
打印链表节点数据:
4=>6=>9打印链表节点结束。
打印链表节点数据:
4=>6打印链表节点结束。
打印链表节点数据:
7=>4=>6=>1打印链表节点结束。
打印链表节点数据:
4=>6=>1打印链表节点结束。
说明:
头节点删除
:直接将head指向head.next,Java的垃圾回收会自动处理原头节点的内存。
尾节点删除
:遍历到倒数第二个节点,修改其next指针为null。对于单节点链表,直接置空head。
边界条件
:处理空链表和单节点链表的情况,避免空指针异常。
时间复杂度
:删除头部节点
,直接修改头指针指向第二个节点即可,无需遍历链表,时间复杂度为O(1)
;删除尾部节点
,需要从头节点开始遍历链表,找到倒数第二个节点(前驱结点),然后将其 next 指针置为 null,时间复杂度为O(n)
。
空间复杂度
:删除头部节点
,仅需修改指针,不依赖链表长度,无额外内存分配,空间复杂度为O(1)
;删除尾部节点
,仅需一个临时指针变量(如prev,即尾部节点的前驱节点),不依赖链表长度,时间复杂度为O(1)
。
新增java单链表按照指定索引删除节点的方法:
public void deleteAtIndex(int index) {if(index < 0) throw new IllegalArgumentException("index error");if(head == null) return;ListNode prev = null;ListNode cur = head;int i = 0;// 找到对应索引的前驱节点,若index超过了链表长度,// 那么cur就是null,prev是尾结点;for(; cur != null && i < index; i++) {prev = cur;cur = cur.next;}if(prev == null) {deleteHead();return;}if(cur == null) {deleteTail();return;}prev.next = cur.next;
}
验证代码逻辑:
DeleteSingleLinkedList deleteSingleLinkedList = new DeleteSingleLinkedList();
deleteSingleLinkedList.insertHead(4);
deleteSingleLinkedList.insertTail(9);
deleteSingleLinkedList.insertWithIndex(1, 6);
deleteSingleLinkedList.print();
System.out.println("删除尾部节点:");
deleteSingleLinkedList.deleteTail();
deleteSingleLinkedList.print();
deleteSingleLinkedList.insertHead(7);
deleteSingleLinkedList.insertTail(1);
deleteSingleLinkedList.print();
System.out.println("删除头部节点:");
deleteSingleLinkedList.deleteHead();
deleteSingleLinkedList.print();
System.out.println("删除指定索引节点:");
deleteSingleLinkedList.deleteAtIndex(1);
deleteSingleLinkedList.print();
deleteSingleLinkedList.deleteAtIndex(0);
deleteSingleLinkedList.print();
deleteSingleLinkedList.deleteAtIndex(0);
deleteSingleLinkedList.print();
执行结果如下:
打印链表节点数据:
4=>6=>9打印链表节点结束。
删除尾部节点:
打印链表节点数据:
4=>6打印链表节点结束。
打印链表节点数据:
7=>4=>6=>1打印链表节点结束。
删除头部节点:
打印链表节点数据:
4=>6=>1打印链表节点结束。
删除指定索引节点:
打印链表节点数据:
4=>1打印链表节点结束。
打印链表节点数据:
1打印链表节点结束。
打印链表节点数据:
上述可知,删除链表节点的思路,无非找到待删除节点的前驱节点,将前驱节点的next指向待删除节点的next即可。
这里根据单链表节点删除的逻辑,举个栗子,见leetcode83. 删除排序链表中的重复元素
:
给定一个已排序的链表的头head , 删除所有重复的元素,使每个元素只出现一次 。返回已排序的链表 。
解法如下:
class Solution {public ListNode deleteDuplicates(ListNode head) {if (head == null || head.next == null)return head;ListNode prev = null;ListNode cur = head;while (cur != null) {prev = cur;cur = cur.next;if (cur != null) {if (prev.val == cur.val) {prev.next = cur.next;cur = prev;}}}return head;}
}
上述解法就是根据单链表删除的基础操作思考得来:首先找到删除节点的前驱节点
,这里因为单链表数据的特殊性,是升序排序,且去掉其重复的值,那么需要删除的重复节点,其前驱节点就是和相邻后续节点刚好值相等的情况,再进行删除相邻后节点的操作即可,注意这里因为重复值可能是连续多个以上(>=3),所以需要将prev即前驱节点重新指向cur节点,再与后续相邻节点再次进行判断即可。
但是注意,并不是一些需要断开指针连接的方式都会用到上述的删除操作,比如leetcode上经典的反转链表题目,如下:
leetcode 206. 反转链表
:
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val = val; }* ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode reverseList(ListNode head) {if(head == null || head.next == null) return head;ListNode prev = null;ListNode cur = head;ListNode combine = null;while(cur != null) {combine = cur.next;cur.next = prev;prev = cur;cur = combine;}return prev;}
}
实际上解法是巧妙的指针指向的变化来实现链表反转的。
2.3.1 实现单链表节点移动
有了上述单链表节点的插入、删除的了解,下述的链表节点移动,实际上就是链表节点删除和链表节点插入操作的一个整合操作。
单链表节点移动操作步骤
- 确定移动目标:
将节点X移动到节点Y之后。
- 查找节点及前驱:
找到X的前驱节点prevX,找到目标节点Y。
- 解除原链接(也就是上述的删除节点):
将X从原位置移除,调整prevX的next指针。
- 插入新位置(也就是上述的插入节点):
将X插入到Y之后,调整Y的next指针。
边界条件处理
X是头节点
:prevX为null,需更新头指针。
Y是尾节点
:X成为新的尾节点。
X和Y相邻或相同
:无需操作或特殊处理。
X或Y不存在于链表中
:提前返回或报错。
下面的示例,以分别移动单链表节点到头部和尾部来进行说明:
public class MoveSingleLinkedList extends NewSingleLinkedList {public void moveToHead(ListNode target) {// 特殊说明: head == target说明目标节点已在头部,所以不做处理if(target == null || head == null || head == target) {return;}ListNode prevNode = getPrevNode(target);// 如果前驱节点为null,则不处理节点移动if(prevNode == null) return;// 首先根据找到的前驱节点,删除目标节点prevNode.next = target.next;// 再将目标节点,插入到头部target.next = head;// 最后将head头结点,重新指向当前最新的头结点head = target;}public void moveToTail(ListNode target) {// 特殊说明: head.next == null说明单链表只有1个节点,所以不做移动尾部处理// target.next: 说明目标节点已经是最尾部的节点了,所以不做移动尾部处理if(target == null || head == null ||head.next == null || target.next == null) {return;}ListNode prevNode = getPrevNode(target);// 注意:移动到尾部和头部有区别,如果前驱节点为null,头部则不处理节点移动// 因为移动到头部,前驱结点为null,可能目标节点已经是头部节点,或者是目标节点在单链表中不存在// 但是移动到尾部的时候,前驱节点如果为null,说明:目标节点已经是头部节点,// 当然也可能是目标节点不存在导致的,所以不能直接return
// if(prevNode == null) return;if(prevNode == null) {// 可能是目标节点的前驱节点不存在,即目标节点是最头部节点;或者目标节点不存在于单链表// 所以下面仅判断 目标节点是最头部节点的情况,这里排除目标节点不存在于单链表中的情况if(target != head) {return;}// 如果目标节点就是头节点,需要移动到尾部,那么先删除头节点head = head.next;// 找到当前单链表的尾部节点ListNode cur = head;while(cur.next != null) {cur = cur.next;}// 插入目标节点:注意!!!目标节点的next指针需要清除,否则就是一个循环引用了// 和上面的移动头节点,思路保持一致:(1)先修改前驱节点的next指针// (2)再修改目标节点的next指针 (3)若有必要,修改head头指针指向cur.next = target;target.next = null;return;}// target.next为null,说明此时target已经是尾部节点,直接返回// 前面已经判断过 target.next == null的情况,所以这里不用额外判断了
// if(target.next == null) {
// return;
// };// 首先根据找到的前驱节点,删除目标节点// 注意这里的target可能是尾部节点,也可能是除了头部节点外的其他节点prevNode.next = target.next;// 找到单链表的尾部节点ListNode cur = prevNode.next;while(cur.next != null) {cur = cur.next;}// 再将目标节点,插入到尾部// 思路:(1)先修改前驱节点的next指针// (2)再修改目标节点的next指针 (3)若有必要,修改head头指针指向cur.next = target;target.next = null;}/*** @param target 单链表的目标节点* @return 目标节点的前驱节点,也就是链表节点查询*/public ListNode getPrevNode(ListNode target) {if(target == null || target == head) {return null;}ListNode prev = null;ListNode cur = head;while(cur != null && cur != target) {prev = cur;cur = cur.next;}// cur为null,说明target节点不存在于链表中,直接返回if(cur == null) {return null;}return prev;}public static void main(String[] args) {MoveSingleLinkedList moveSingleLinkedList = new MoveSingleLinkedList();moveSingleLinkedList.insertTail(9);moveSingleLinkedList.insertTail(1);moveSingleLinkedList.insertHead(8);moveSingleLinkedList.insertWithIndex(2,7);moveSingleLinkedList.insertWithIndex(1,6);moveSingleLinkedList.print();System.out.println("开始头部移动:");moveSingleLinkedList.moveToHead(moveSingleLinkedList.head.next.next);moveSingleLinkedList.print();moveSingleLinkedList.moveToHead(moveSingleLinkedList.head);moveSingleLinkedList.print();System.out.println("开始尾部移动:");moveSingleLinkedList.moveToTail(moveSingleLinkedList.head);moveSingleLinkedList.print();moveSingleLinkedList.moveToTail(moveSingleLinkedList.head.next);moveSingleLinkedList.print();System.out.println("开始头部移动:");moveSingleLinkedList.moveToHead(moveSingleLinkedList.head.next.next.next.next);moveSingleLinkedList.print();System.out.println("开始尾部移动:");moveSingleLinkedList.moveToTail(moveSingleLinkedList.head.next.next.next.next);moveSingleLinkedList.print();}}
执行结果:
8=>6=>9=>7=>1打印链表节点结束。
开始头部移动:
打印链表节点数据:
9=>8=>6=>7=>1打印链表节点结束。
打印链表节点数据:
9=>8=>6=>7=>1打印链表节点结束。
开始尾部移动:
打印链表节点数据:
8=>6=>7=>1=>9打印链表节点结束。
打印链表节点数据:
8=>7=>1=>9=>6打印链表节点结束。
开始头部移动:
打印链表节点数据:
6=>8=>7=>1=>9打印链表节点结束。
开始尾部移动:
打印链表节点数据:
6=>8=>7=>1=>9打印链表节点结束。
说明:
移动到头部(Move to Head)
- 时间复杂度:O(n)
原因
:
需要遍历链表找到目标节点的前驱节点(最坏情况下遍历整个链表)。
例如,若目标节点是最后一个节点,需要遍历全部 n 个节点。
- 空间复杂度:O(1)
原因
:
仅使用常数级别的临时变量(如 prev),没有额外内存分配。
移动到尾部(Move to Tail)
- 时间复杂度:O(n)
原因
:
若目标节点是头节点:需要遍历链表找到新的尾节点(遍历全部 n 个节点)。
若目标节点是中间节点:需要遍历找到目标节点的前驱节点(最坏 O(n)),再遍历找到原尾节点(最坏 O(n))。
总计:两次遍历,但时间复杂度仍为 O(n)(系数可忽略)。
- 空间复杂度:O(1)
原因
:
同样只使用常数级别的临时变量。
注意:如果频繁操作尾部,可以通过以下优化降低时间复杂度:
维护尾指针:
在链表中增加一个tail指针,指向尾节点。
移动到尾部的时间复杂度可降为 O(1)(直接通过 tail 指针操作)。
但需要额外维护tail指针的更新逻辑(例如插入、删除节点时)。
2.4 总结
场景大类 | 操作方式 | 时间复杂度 | 空间复杂度 |
---|---|---|---|
单链表节点插入 | 头部插入 | O(1) | O(1) |
单链表节点插入 | 尾部插入 | O(n) | O(1) |
单链表节点插入 | 单链表指定位置插入 | O(n) | O(1) |
单链表节点删除 | 删除头部结点 | O(1) | O(1) |
单链表节点删除 | 删除尾部结点 | O(n) | O(1) |
单链表节点移动 | 移动到头部 | O(n) | O(1) |
单链表节点移动 | 移动到尾部 | O(n) | O(1) |
根据总结可知,时间复杂度为O(1)的情况有单链表节点的头部插入和头部节点删除
,其余场景的时间复杂度为O(n)。