剑指Offer_#18_删除链表的节点
Contents
题目
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
注意:此题对比原题有改动
示例 1:
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
题目保证链表中节点的值互不相同
若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点
思路分析
Leetcode的此题和书本上原题不符,原题中输入参数的待删除节点不是int类型,而是指针类型,所以可以通过用待删除节点的后一节点覆盖待删除节点,来达到O(1)
的时间复杂度。Leetcode上的题目,只能通过从前往后遍历来实现,复杂度是O(n)
。
需要考虑的几个边界条件
- 输入为
null
,也应该返回null
- 删除的是第一个节点,返回
head.next
- 删除的是最后一个节点
在提交代码之前,应该自己编写以上的几个测试用例,先自己测试通过,再提交。
《剑指Offer》原题思路
- 原题的意思是,删除节点
val
,这里传入的参数val
是ListNode的指针,或者说是ListNode对象。- 区别在于,这样就可以直接访问到待删除节点的后一节点了,可以用下面的方法。
- 设要删除节点为
j
,前后节点为i
,k
,算法思路是:
1.修改节点j
的val
为后一节点k
的val
2.然后将j
节点的next
指向k
节点的next
(也就是跳过k
节点)。
这样做的效果与直接删除j
节点一样。 - 需要考虑的特殊条件
如果待删除的节点就是最后一个节点,那么已经没有下一节点了,上面的方法行不通,所以只能遍历。
解答1:从前往后遍历节点
编写代码需要注意的一点
为了删除当前节点cur
,需修改的时上一节点pre
的next
指针。所以必须在访问到上一个节点pre
时就进行判断,然后修改pre
节点的指针。
这导致了第一个节点head
在循环当中无法被访问和判断,解决这一问题有两个方法:
- 在
head
节点之前增加一个伪节点dummyHead
,再返回结果时返回dummyHead.next
,这样可在循环中处理head
节点。dummyHead
之所以被叫做伪节点,是因为这个节点的作用只是提供了一个指向head
的指针,而它的val
属性没有任何意义,最后返回时也不需要这个节点
- 进入循环代码之前,单独对
head
节点进行判断和处理。
代码中两种方法都写了,可以看出,两种写法代码量是一样的,所以用哪种方式都可以。
class Solution {
public ListNode deleteNode(ListNode head, int val) {
//增加一个伪节点,则循环语句可以覆盖head节点,不需为head节点单独进行判断
//此时,返回值为dummyHead.next
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
//如果没有伪节点,那么需要单独对head进行判断
//此时,返回值为head
// if(head == null) return null;
// if(head.val == val) return head.next;
ListNode cur = dummyHead;
while(cur != null){//遍历查找数值是val的节点
if(cur.next != null && cur.next.val == val){
cur.next = cur.next.next;//跳过要删除的节点
break;
}
cur = cur.next;//向后遍历
}
//return head;
return dummyHead.next;
}
}
复杂度分析
时间复杂度O(n)
空间复杂度O(1)
解答2:剑指Offer原题解法
按照书上的原题意写了如下代码,只可在本地IDE测试。
public class Solution {
public static ListNode deleteNode(ListNode head, ListNode val){
if(head == null || val == null) return null;
if(val.next != null){//待删除节点不是尾节点
val.val = val.next.val;//用下一节点的值覆盖待删除节点
val.next = val.next.next;//跳过下一节点
}else{//待删除节点是尾节点,则从前向后遍历
if(head == val) return null;//链表只有一个节点,则返回空链表
ListNode cur = head;
while(cur != null){//ERROR:这里不可以用cur.next,可能出现NullPointerException
if(cur.next != null && cur.next == val){//如果cur.next是待删除节点
cur.next = cur.next.next;//cur连接到cur.next.nex,相当于跳过了待删除节点
}
cur = cur.next;//向后遍历
}
}
return head;
}
}
复杂度分析
时间复杂度
输入不是尾节点时,时间复杂度为O(1)
输入是尾节点时,时间复杂度为O(n),
平均时间复杂度:(1*O(n)+(n-1)*O(1))/n=O(1)
,依然是O(1),比解法1好
空间复杂度
O(1)
另附测试代码
测试代码包含ListNode类和Main类。
ListNode类中实现了两个工具方法:
LinkedList
:输入数组,生成对应的链表PrintLinkedList
:输入链表,打印出对应的数组
import java.util.ArrayList;
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
public static ListNode LinkedList(int[] numArray){//输入数组,构造一个链表
ListNode dummyHead = new ListNode(0);
ListNode pre = dummyHead;
for(int i = 0;i <= numArray.length - 1;i++){
ListNode cur = new ListNode(numArray[i]);//构造当前节点
pre.next = cur;
pre = cur;
}
return dummyHead.next;
}
public static void PrintLinkedList(ListNode head){//输入链表头节点,打印对应的数组
if(head == null) System.out.println("[]");
else{
ListNode cur = head;
ArrayList<Integer> res = new ArrayList<>();
while(cur != null){
res.add(cur.val);
cur = cur.next;
}
System.out.println(res);
}
}
}
Main类用于测试
public class Main {
public static void main(String[] args){
ListNode head = ListNode.LinkedList(new int[]{1,2,3,4,5});
//这样写是不对的,因为这个val和head不在一个对象当中,所以在删除节点时,无法进行比较
//ListNode val = ListNode.LinkedList(new int[]{5});
//正确写法:val应该是从head对象获取到的
ListNode val = head.next;
//打印
System.out.println("输入:head,val");
ListNode.PrintLinkedList(head);
ListNode.PrintLinkedList(val);
ListNode deleted = Solution.deleteNode(head,val);
//打印
System.out.println("输出:deleted");
ListNode.PrintLinkedList(deleted);
}
}
使用方法
在IDE中建立三个类,名为Solution
,Main
,ListNode
。将上面的代码粘贴到对应类的java文件中。
自行修改main方法中的输入值head,val,然后运行Main类当中的main方法即可进行测试。