链表8:链表反转之二:4道变形题

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表8:链表反转之二:4道变形题相关的知识,希望对你有一定的参考价值。

链表反转有几个常见的变形题目:

  1. 一个是指定链表的某个区间,让你进行反转(NC21)。

  2. 将链表每K个一组进行反转,最后不够K个的就直接输出(LeetCode 25)。

  3. LeetCode24,两两交换链表的结点,不就是将上面的K换成2吗?

  4. 有一个更无聊的题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 = node2node1.next = node2.nextnode2.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道变形题的主要内容,如果未能解决你的问题,请参考以下文章

给秋招加点料——Hot15道高频算法面试题!

给秋招加点料——Hot15道高频算法面试题!

看一遍就理解,图解单链表反转

看一遍就理解,图解单链表反转

排序奇升偶降链表-链表拆分反转归并-字节跳动高频题

链表11:链表中删除元素的8道题之二