链表8:链表反转之二:4道变形题
Posted 纵横千里,捭阖四方
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表8:链表反转之二:4道变形题相关的知识,希望对你有一定的参考价值。
链表反转有几个常见的变形题目:
-
一个是指定链表的某个区间,让你进行反转(NC21)。
-
将链表每K个一组进行反转,最后不够K个的就直接输出(LeetCode 25)。
-
LeetCode24,两两交换链表的结点,不就是将上面的K换成2吗?
-
有一个更无聊的题LeetCode143,重排链表,题意有点绕,我们后面再看怎么个无聊法。
我们一个个来看。
1.指定区间反转
NC21对题目的描述是:
也就是反转链表的一个区间,例如这里反转从第二到第四个元素:
输入:{1,2,3,4,5},2,4
返回值:{1,4,3,2,5}
这个题目是在普通反转的基础上先找到区间的位置再执行反转。当然还需要对参数、边界等进行判断。
基本过程是:
1.先判断m、n是否合法,不合法则直接返回head。需要计算list length,过程中同时确定m-1(即mPre)和n+1(nNext)的位置。
2.循环反转m-n部分,先把m.next=nNext确定,而mPre.next需要反转结束后,根据m是否为头作不同选择。
3.如果m为头,则直接换头,否则mPre.next = n。
public class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
//计算length,同时把mPre和nNext找到出来
ListNode tmp = head;
ListNode mPre = null;
ListNode nNext = null;
int len = 0;
for(; tmp!=null; tmp = tmp.next){
len = len+1;
if(len == m -1) mPre = tmp;
if(len == n + 1) nNext = tmp;
}
//判断1<=m <= n <= length
if(m > n || m < 1 || n > len) return head;
//先把m-n反转,再设置mPre nNext的连接
//三个辅助指针用于反转
ListNode node1 = (mPre == null)?head : mPre.next;//指向m
ListNode node2 = node1.next;
ListNode node3 = null;
node1.next = nNext;//m的next指向n的next
//开始反转
while(node2 != nNext){
node3 = node2.next;
node2.next = node1;
node1 = node2;
node2 = node3;
}//循环结束,node1指向n
//判断是否换头,根据m是否为head
if(mPre == null){
return node1;
}else{
mPre.next = node1;
return head;
}
}
}
这个题还可以通过建立虚拟结点等来做,写法又不一样,我们熟悉一种就好。
2.K个一组反转链表
先看题目要求:
LeetCode25:给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
如果面试时直接手写这个题,肯定够喝一壶的,但是经过前面两个题的铺垫,是不是感觉也没那么难了?
这个题的思路不复杂,但是想完全写对也真不容易,还是指针的指向问题容易晕。
这个题的大致过程是:
大致过程可以分解为
1、找到待翻转的k个节点,如果剩余数量小于 k ,不反转直接返回头结点。
2、对K个进行翻转。并返回翻转后的头结点。这里的问题是区间的界定,这是个左闭又开区间,本轮操作的尾结点其实就是下一轮操作的头结点。
3、对下一轮 k 个节点也进行翻转操作。
4、将上一轮翻转后的尾结点指向下一轮翻转后的头节点,即将每一轮翻转的k的节点连接起来。
在网上找到一张图,感觉画得挺好:
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null) {
return head;
}
ListNode tail = head;
for (int i = 0; i < k; i++) {
//剩余数量小于k的话,则不需要反转。
if (tail == null) {
return head;
}
tail = tail.next;
}
// 反转前 k 个元素
ListNode newHead = reverse(head, tail);
//下一轮的开始的地方就是tail
head.next = reverseKGroup(tail, k);
return newHead;
}
/*
左闭又开区间
*/
private ListNode reverse(ListNode head, ListNode tail) {
ListNode pre = null;
ListNode next = null;
while (head != tail) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
注意上面我们直接复用了反转的方法。
所以我们一直强调,这种基础的,关键的算法一定能做到闭着眼睛都能手写,面试的时候手到擒来,将重点放在题目模型的构造上,这会大大减小我们写代码的压力。
3.leetcode 24 两两交换链表中的节点
题目的要求是:
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
这个题可以将上面的K直接换成2。所以如果我们将反转写熟了,K个一组的练过,这个题毫无难度,但是两两交换本身没有K这个麻烦,我们可以基于相邻结点的特性重新设计和实现。
同样,本题有迭代和递归两种方式,不过递归比较难想清楚,我们只用迭代来解决。
为了便于实现,我们创建虚拟结点 dummyHead,令 dummyHead.next = head。令 temp 表示当前到达的节点,初始时 temp = dummyHead。每次需要交换 temp 后面的两个节点。
如果 temp 的后面没有节点或者只有一个节点,则没有更多的节点需要交换,因此结束交换。否则,获得 temp 后面的两个节点 node1 和 node2,通过更新节点的指针关系实现两两交换节点。
具体而言,交换之前的节点关系是 temp -> node1 -> node2,交换之后的节点关系要变成 temp -> node2 -> node1,因此需要进行如下操作。
temp.next = node2
node1.next = node2.next
node2.next = node1
完成上述操作之后,节点关系即变成 temp -> node2 -> node1。再令 temp = node1,对链表中的其余节点进行两两交换,直到全部节点都被两两交换。
两两交换链表中的节点之后,新的链表的头节点是 dummyHead.next,返回新的链表的头节点即可。
完整的代码是:
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode temp = dummyHead;
while (temp.next != null && temp.next.next != null) {
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1;
}
return dummyHead.next;
}
}
上面有个要求我们用红色标记的,是啥意思呢?
其实我们进行调整还有一种方法就是先将元素值保存到数组里,然后调整好之后再写回到链表,也就是只改链表的结点值,而不修改节点。这种方式降低了反转的难度,但是被毙掉了😄。
4.leetcode 143 重排链表
题目的要求是:
给定一个单链表 L:L0→L1→…→Ln-1→Ln ,
将其重新排列后变为:L0→Ln→L1→Ln-1→L2→Ln-2→…
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
1.给定链表 1->2->3->4, 重新排列为 1->4->2->3.
2.给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3.
你说这个题无聊不?
因为链表不支持下标访问,所以我们无法随机访问链表中任意位置的元素。比较容易想到的一个方法是,我们利用线性表存储该链表,然后利用线性表可以下标访问的特点,直接按顺序访问指定元素,重建该链表即可。但是如果面试官给你出这个题,一定是不喜欢看到你这么做 的,还是希望你用链表遍历+反转+合并来实现。因此这个题写清楚也非常不容易。
方法1:先保存到现象表,调整完之后再写回链表
class Solution {
public void reorderList(ListNode head) {
if (head == null) {
return;
}
List<ListNode> list = new ArrayList<ListNode>();
ListNode node = head;
while (node != null) {
list.add(node);
node = node.next;
}
int i = 0, j = list.size() - 1;
while (i < j) {
list.get(i).next = list.get(j);
i++;
if (i == j) {
break;
}
list.get(j).next = list.get(i);
j--;
}
list.get(i).next = null;
}
}
方法二:寻找链表中点 + 链表逆序 + 合并链表
目标链表即为将原链表的左半端和反转后的右半端合并后的结果
这样我们的任务即可划分为三步:
1.找到原链表的中点。可以使用快慢指针来找。
2.将原链表的右半端反转。
3.将原链表的两端合并。
废话不多说,直接看代码:
class Solution {
public void reorderList(ListNode head) {
if (head == null) {
return;
}
ListNode mid = middleNode(head);
ListNode l1 = head;
ListNode l2 = mid.next;
mid.next = null;
l2 = reverseList(l2);
mergeList(l1, l2);
}
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
public void mergeList(ListNode l1, ListNode l2) {
ListNode l1_tmp;
ListNode l2_tmp;
while (l1 != null && l2 != null) {
l1_tmp = l1.next;
l2_tmp = l2.next;
l1.next = l2;
l1 = l1_tmp;
l2.next = l1;
l2 = l2_tmp;
}
}
}
这里再次直接使用了链表的反转方法和合并两个链表的方法。如果平时已经将其练习得非常熟了,这个题可以说毫无压力,反之,可能就要竖着面试,横着出来了。
以上是关于链表8:链表反转之二:4道变形题的主要内容,如果未能解决你的问题,请参考以下文章