您的位置:首页 > 其它

每日一恋 LeetCode 92 & 206. Reverse Linked List (反转链表)

2018-08-05 19:47 513 查看

206 反转链表 题目描述

Reverse a singly linked list.

Example:

Input: 1->2->3->4->5->NULL
Output: 5->4->3->2->1->NULL

Follow up:

A linked list can be reversed either iteratively or recursively. Could you implement both?

分析:

题目默认应该是原地反转链表,并且要求写出递归和迭代两个版本。

前言:

递归与数学归纳法总是一起讨论,可见数学归纳法在递归的背后有多么重要。那么什么是数学归纳法呢?
先说结论:数学归纳法是编码的依据
数学归纳法用于证明断言对所有自然数成立:

  • 证明对于N=1成立
  • 证明N>1时:如果对于N-1成立,那么对于N成立

我们在写一个递归方法之前,要按照如下的规则进行书写:
1、严格定义递归函数作用,包括参数,返回值,Side-effect
2、先一般,后特殊
3、每次调用 必须 缩小问题规模
4、每次问题规模缩小成都必须为 1

递归版:

思路:先一般,每次只处理当前结点(要点3和4),当前结点之后的所有结点递归调用(要点2)。后特殊,这里有一个技巧,我们不要去想特殊情况是怎么样的,而是去看已经写好的函数体,一行一行去看,有哪些地方可能会出错,这些会出错的地方,就是我们的特殊情况。

只处理当前结点,将当前头结点的下一个结点的 next 域指向当前头结点,然后当前头结点的 next 域指向空。

public ListNode reverseList(ListNode head) {
...
head.next.next = head;
head.next = null;
...
}

再递归处理后续结点,因为后续结点的处理必定在当前结点之前进行处理(因为当前结点的 next 域会指向空,导致获得不到后续结点的引用),newHead 就是递归完成后新链表的头结点,

public ListNode reverseList(ListNode head) {
...
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;

return newHead;
}

最后处理特殊情况,仔细查看上面的代码,发现 head.next 有可能为空,如果 head.next 为空,那么 head.next.next 就会出错,因此 head.next 不能为空,这就是一个特殊条件,也就是进入该方法的前提是链表中至少有两个结点。
然后我们再看看 size == 0 或 size == 1 的情况,前者直接返回 null,后者直接返回当前传进来的结点即可。

最终的代码如下:

/**
* Reverse a linked list.
*
* Input: 1->2->3->4->5->NULL
* Output: 5->4->3->2->1->NULL
* @param head head the linked list to reverse.
* @return head of the reversed linked list.
*/
public ListNode reverseList(ListNode head) {

// size == 0 or size == 1
if ( head == null || head.next == null )
return head;

ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;

return newHead;
}

迭代版:

思路:定义循环不变式,用一个分隔线将链表分为左右两部分,分割线左边的定义为 newHead ,代表反转成功的链表;分割线右边的定义为 currentHead ,代表还没有反转的链表,这个定义在循环开始至结束都要保证成立。

根据循环不变式的定义,起始时,newHead 指向 null,currentHead 指向 head;结束时 newHead 指向最后一个元素,currentHead 指向 null。

在写循环代码时,循环的退出条件可以不急着写,先写循环体。在写循环体的过程中,所考虑的始终是中间状态(循环已经做了很多轮了,下一轮怎么做,而不是考虑第一轮怎么做)

/**
* Input: 1->2->3->4->5->NULL
* NULL|1->2->3->4->5->NULL
* NULL<-1|->2->3->4->5->NULL
* NULL<-1<-2|->3->4->5->NULL
* Output: 5->4->3->2->1->NULL
*
* @param head head the linked list to reverse.
* @return head of the reversed linked list.
*/
public ListNode reverseList(ListNode head) {

ListNode newHead = null; // 反转成功的链表
ListNode curHead = head; // 还没有反转的链表

while (curHead != null) {
// curHead.next 如果curHead为空,会出错
ListNode nextNode = curHead.next; // 保存下一个结点的引用
curHead.next = newHead; // 处理当前结点,将next指向已经反转成功的链表
newHead = curHead; // 更新已经反转成功的指针
curHead = nextNode; // 更新还没有反转的指针
}

return newHead;
}

时间复杂度:O(n)O(n)
空间复杂度:O(1)O(1)

92 反转链表 II 题目描述

反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

说明:
1 ≤ m ≤ n ≤ 链表长度。

示例:

输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
^  ^  ^
prev begin

思路:

反转后正常情况下,链表的前面一部分和后面一部分是没有反转的,为了中间反转的部分能够顺利连接,就需要保存前面未反转部分最后一个结点 prev 和后面未反转部分第一个结点 after。

1   ->2   ->3   ->4   ->5   ->NULL
^     ^     ^
prev begin  next
cur
1   ->3   ->2   ->4   ->5   ->NULL
^     ^     ^    ^
prev begin  cur  next

另外,题目中没有指明是否是原地反转,如果是非原地反转就比较简单了,先与206题一样反转中间的部分,然后再用保存的两个结点将其连接起来;如果是原地反转,就需要额外设置一个指向当前反转部分第一个结点的指针 begin。

具体步骤:因为每次原地反转就是将 prev.next 指针指向 cur.next(例子中1指向3),当前待反转的结点 cur.next 指向next.next(2指向4),然后将 next.next 更新为 cur(3指向2),最后更新已反转链表的第一个结点位置 begin 指针,指向 next 结点(原本指向 2,现在指向 3)。将链表拉直,就从示例 1 变到了 示例 2。。需要注意的是,这里的 prev 始终指向 begin,在写循环的时候要始终保持这个对应关系。

public ListNode reverseBetween(ListNode head, int m, int n) {

if (head == null || head.next == null)
return head;

ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;

ListNode prev = dummyHead; // 不反转链表的最后一个结点,初始时为虚拟头结点
ListNode cur = head; // 反转没开始时m位置的结点
ListNode begin = null; // 反转结束后m位置的结点,即原来n位置的结点
int i = 0;
while (cur != null) {
if (++i < m) { // cur 走到带反转结点位置
prev = cur;
cur = cur.next;
}
else if (i == m) { // 首次更新 begin 指针
begin = cur;
}
else if (i >= m && i <= n) { // 正式的反转操作,见上具体步骤
ListNode temp = cur.next;
prev.next = temp;
cur.next = temp.next;
temp.next = begin;
begin = prev.next;
}
else { // i > n 最后的部分不需要处理
break;
}
}
return dummyHead.next;
}

这是LeetCode上投票数超过41K的答案,思路其实是一样的。

public ListNode reverseBetween(ListNode head, int m, int n) {
if(head == null) return null;
ListNode dummy = new ListNode(0); // create a dummy node to mark the head of this list
dummy.next = head;
ListNode pre = dummy; // make a pointer pre as a marker for the node before reversing
for(int i = 0; i<m-1; i++) pre = pre.next;

ListNode start = pre.next; // a pointer to the beginning of a sub-list that will be reversed
ListNode then = start.next; // a pointer to a node that will be reversed

// 1 - 2 -3 - 4 - 5 ; m=2; n =4 ---> pre = 1, start = 2, then = 3
// dummy-> 1 -> 2 -> 3 -> 4 -> 5

for(int i=0; i<n-m; i++)
{
start.next = then.next;
then.next = pre.next;
pre.next = then;
then = start.next;
}

// first reversing : dummy->1 - 3 - 2 - 4 - 5; pre = 1, start = 2, then = 4
// second reversing: dummy->1 - 4 - 3 - 2 - 5; pre = 1, start = 2, then = 5 (finish)

return dummy.next;

}

如果文章里有说得不对的地方请前辈多多指正~ 希望与君共勉~

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: