问题描述
题目:删除链表的倒数第n个节点;
描述:给定一个链表,删除链表中倒数第n个节点,返回头节点;
Example:
输入:1->2->3->4->5->null;2
输出:1->2->3->5->null
输入:1->null;1
输出:null;
解法1(本人的憨憨解法)
这题最先想到的解法是,先通过快慢节点,算出该链表的长度lenth,然后再循环一次,到length-n(下标从0开始,所以是length-n)的位置,删掉该节点();
public class Solution {
/**
* @param head: The first node of linked list.
* @param n: An integer
* @return: The head of linked list.
*/
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head == null ){
return null;
}
ListNode fast = head.next;
ListNode slow = head;
int step = 1;
int length = 0;
while(fast != null && fast.next!= null){
fast = fast.next.next;
slow = slow.next;
++step;
}
if(fast == null){//length分奇偶
length = step*2-1;
}else if(fast.next == null){
length = step*2;
}
System.out.println("length="+length );
if(length == n){//删了头节点
head= head.next;
return head;
}
//倒数第n个等于正数第length+1-n个;下标为length+1-n-1 = length -n;
ListNode preNode = head;
ListNode target = head.next;
int i =1;
while(preNode != null){
if(i == length-n){
preNode.next = target.next ;
return head;
}
target = target.next;
preNode = preNode.next;
i++;
}
return head;
}
}
这种做法有几点不太好:
- 首先就是时间复杂度,假设链表长度为m,快慢节点需要O(m/2),第二次循环,最差是O(m),最好是O(1).(太不稳定了啊是不是,话说如果删除靠前的节点,实现较好时间复杂度的情况,那还用倒数干嘛??)
- 算长度。而且长度分奇偶…可能算长度还有别的方法,但是当我写下if-else if的时候…
- 下标计算。这里很容易乱掉,前面说的被删除的下标的length+1-n-1,但是实际上,我们要找的根本不是被删的那个节点,而是被删除节点的前继节点,因为单向链表删除节点的方法:preNode.next = delNode.next=preNode.next.next;可以说跟被删除节点没有压根没有被用到。所以要么只定义一个preNode节点(长度就变成了length-n-1),要么再定义双指针(preNode,delNode);
- 变量太多了。这个应该只是我有的问题,就是从计算长度,到正式删除节点,我定义了两次双指针,共计4个指针,再加上step,length,几乎满屏都是自定义变量(憨得让人窒息)。
为了本憨憨的生命安全,所以我进化了,就有了.
解法2(本人不那么憨憨的解法)
需要搞定的几个问题:
1.时间复杂度。这个题目的要求是最好时间复杂度为O(n);即要么只有一次循环,要么两次以上循环加在一起确定等于length;
2.计算长度。如果不需要计算长度,那么之前说的2.3问题都不是问题;
3.这是后来意识到的问题,假如删除的就是头节点应该怎么办,因为我已经不算长度了,那原先写的length==n时相当于删除头节点,就不能用了。
先上代码:
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode tempNode = new ListNode(0);
tempNode.next= head;//head移动的时候,tempNode的值也会随之变化吗?
for(int i=0;i<n;i++){
if(head == null){
return null;
}
head = head.next;
}
ListNode preNode = tempNode;//为什么等于tempNode而不是head
while(head!=null){//为什么不加head.next != null
head = head.next;
preNode = preNode.next;
}
preNode.next = preNode.next.next;
return tempNode.next;//为什么返回的不是head。
}
解释一下:
-
时间复杂度,稳定的O(n)。首先被删除节点的下标一定是length-n,一般想的肯定是从下标0到length-n,刚好删除;但是说起来,length-n也只是一段距离而已,只要在做删除的过程中,经过的是length-n距离,那整个循环从哪里开始都是一样的。所以,写法就成了,先让某个节点跑个n距离,然后第二次循环,被删除节点的前继节点从头开始,到先跑n个距离的节点值为null,剩余这length-n,刚好。
-
定义的还是双指针。如果只有一个指针的话,肯定只要作为被删除节点的前继节点,那如果删除的是头节点,去哪找头节点的前继节点哦。而且链表中,做双节点还是挺常见的方法,比如找链表中点之类的,本题解法一计算长度就是在利用双指针先将到链表中点的长度计算出来。
-
为什么两个指针都相当于指向了头节点的前继,而不是head;
第一:tempNode。也就是我在代码块中写的第四个问题“为什么返回的是tempNode.next而不是haed”。
一个原因当然是head在整个过程中最后已经指向了null(见解释第一条),另一个原因就是,他解决了不计算长度,被删除节点即头节点的问题。tempNode是我们自定义的节点,循环又是从head开始,那后面怎么动都跟tempNode没关系。
并且,代码块中的第一个问题:head移动,tempNode的值会变吗,tempNode.next的值呢。首先tempNode当然不会,而head指针的移动,只是head所指向的位置发生变化,他没有改变值,他一走了之后,tempNode.next指向的还是整个链表的头节点。而头节点发生变化也只是在删除操作(preNode.next = preNode.next.next;)之后才可能发生,总之跟head没啥关系。
第二:preNode。preNode是作为被删除节点的前继节点而定义的,如果指向head,在第一次循环时,他就跟着head跑了(tempNode.next没跑是因为tempNode没动)。至于他为什么定义在head之前,以及代码块中第三个问题“while循环判断条件为什么不加head.next != null”,见图:
可见第二次循环之后,preNoded的下标正好是head==null,length-n-1,按照单向链表删除的方法,到这里是对的。
结束
到这里基本就结束啦,第一次写这么长,可能有些疏漏,欢迎指正。
另外算法这种东西呢,个人认为是没有最好只有更好(所以大家还有更好的方法,欢迎一起交流啊),而且贵精不贵多(虽然我也还没做过多少,也不知道怎么好意思说这句话),一道题做完了,不丢开看下一题,而是想着怎么效率更高,代码量更少,才是学习算法的正确姿势啊(个人意见…而已)。