Leetcode 刷题日记
2021.2.3
题目链接:
https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/
问题描述:
反转单向链表
解答一:
栈
代码:
class Solution {
public ListNode reverseList(ListNode head) {
Stack<ListNode> stack = new Stack();
while(head != null){
stack.push(head);
head = head.next;
}
if(stack.empty()) return null;
else{head = stack.pop();
ListNode cursor = head;
while(!stack.empty()){
cursor.next = stack.pop();
cursor = cursor.next;
}
cursor.next = null;
return head;
}
}
}`
分析:
时间复杂度:O(n)
空间复杂度:O(n)
运行结果:
评注:
这是我首先想到的方法,这个方法利用了栈的特性。实际上,在处理倒序问题时,栈都是一个可靠的方法。
解答二:
双指针(迭代)
代码:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode last = null;
ListNode cursor = head;
ListNode next = head == null ? null : cursor.next;
if(head == null) return null;
else{
while(cursor != null){
cursor.next = last;
last = cursor;
cursor = next;
next = next == null || next.next == null ? null : next.next;
}
return last;
}
}
}
分析:
时间复杂度:O(n)
空间复杂度:O(1)
运行结果:
评注:
要想实现单向链表的倒序,有两个难点:一是无法获得任意给定节点的直接前驱;二是如果改变了一个节点的后继,那么原后继的引用将可能丢失。这时使用双指针就是必要的。通过双指针来获取足够多的引用,这是非常朴素的想法。
这个方法在实现时实际有很多细节需要注意:
1.空指针的判断:要判断一个引用对象是否为空,使用==是唯一的办法。如果调用equals(),就会报错,因为要调用对象的方法,必须首先保证对象非空。
2.循环的使用:选择while循环还是do while循环两者的区别在于循环开始的时候。前者实现判断是否满足循环条件,然后才进入循环体;而后者是先执行一次循环体,然后再判断是否进入下一个循环体。前者至少执行零次循环体,而后者至少执行一次。但是在循环结束时,其退出程序都是相同的:只要不满足循环条件,就退出循环。
3.边界情况的把握:笔者在最初写的时候,由于没有把握好边界情况,导致反转前最后一个节点的指针域没有指向其前驱,这实际上是在写循环条件时没有注意边界情况导致的。
解答三:
递归
代码:(以下代码转自https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/solution/fan-zhuan-lian-biao-by-leetcode-solution-jvs5/)
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
System.out.println("it is the end of the linkedList:"+head.val);
//if the head is a null pointer or the head is the last node
return head;
//this is exactly the head in the reversed linkedList
}
ListNode newHead = reverseList(head.next);
//pre-recursion:the newHead should be the last node in the original linkedList
System.out.printf("this value:%d\n",newHead.val);
System.out.println("next node of the head:"+head.next.val);
head.next.next = head;
head.next = null;
return newHead;
}
分析:
时间复杂度:O(n)
空间复杂度:O(n)
运行结果:
评注:
关于递归:
1.递归中为什么主调函数能储存自己的参数?
在内存中,方法的调用是通过栈来实现的。先调用的方法先被压入栈中,主调函数进行递归调用后,并不会立即被弹出,而是在被调函数返回后才会被弹出。这样就可以保证主调函数的参数不会被覆盖。
2.为什么调用方法要通过栈而不是其他数据结构?
①操作简单。一个标准的栈只有两个操作:push()和pop()他们的时间复杂度都是O(1),没有过多的开销,执行效率高。
②功能特殊。队列的操作也是常数复杂度,但队列是无法完成方法调用的,原因在于主调函数必须在所有被调函数都返回之后才能返回,而队列的特点导致主调函数总是先于被调函数出队,这是不能满足需求的。
3.哪些情况下应该使用递归?
从函数调用的实现原理可以看出,在进行递归调用时,主调函数的参数可以被保存。在这种需要保存参数的情况下,递归的使用是合理的。笔者在第一次使用递归做该题的时候就困惑与如何保存原链表中某一元素前驱和后继的引用。事实上,这一工作是使用递归的必然结果。此题较难用递归实现,所以这一解法能加深对递归的理解。