Leetcoder Day4|链表part2及总结

语言:Java/C++ 

目录

24. 两两交换链表中的节点 

🏁解题思路:

迭代法

递归

1​​​​​​9.删除链表的倒数第N个节点 

​编辑

面试题 02.07. 链表相交 

142.环形链表II  

链表总结


24. 两两交换链表中的节点 

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例:

输入:head = [1,2,3,4]
输出:[2,1,4,3]

🏁解题思路:

看到这题,我的第一反应是,如果奇数个元素,最后一个节点怎么办,按照题目的意思,最后的元素就保留即可不用进行交换。实际上对于代码而言不需要做额外的区分。

这道题有两种思路,一旦遇到这种有很多重复操作的情况,通常情况都是可以使用递归的,但是递归的思路比较抽象,就先从常规的迭代方法来解决这个问题。卡哥在随想录文字版中给的思路其实很明白了,但是代码没有leetcode官方给出的清晰,因此采取官方的代码进行梳理。

迭代法

因为涉及到头节点的转换,照旧设置一个虚拟头节点dummyHead。拿前两个元素举例:

  • 交换之前的节点关系为:dummyHead -> node1 -> node2
  • 交换之后的节点关系为: dummyHead -> node2 -> node1

因此需要进行的操作如下:

cur->next = node2
node1->next = node2->next
node2->next = node1

完成上述操作之后,节点关系即变成 cur -> node2 -> node1。再令 cur = node1,对链表中的其余节点进行两两交换,直到全部节点都被两两交换。循环结束的条件就是cur后面没有或者只有一个节点,即下一个节点和下下个节点均不为空。

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyHead= new ListNode(0);
        dummyHead->next=head;
        ListNode* cur=dummyHead;
        while(cur->next!=nullptr && cur->next->next!=nullptr){
            ListNode* node1=cur->next;
            ListNode* node2=cur->next->next;

            cur->next=node2;
            node1->next=node2->next;
            node2->next=node1;

            cur=node1;
        }
        ListNode* temp=dummyHead->next;
        delete dummyHead;
        return temp;

    }
};
  • 时间复杂度:O(n),其中n是链表的节点数量。需要对每个节点进行更新指针的操作。

  • 空间复杂度:O(1)

递归

第二个思路就是递归了,卡哥没有讲这个思路。因此参考了leetcode题解中画手大鹏的图解。

昨天的翻转链表中已经用到了一次递归思想。递归本质就是不断重复相同的事情,看到一句评论说的很好,要把递归拆解成,“递”和”归“ 两部分,根据业务判断,从头到尾每次递给下一层该是什么参数,从尾到头归来需要什么样的放返回值。因此在写递归方法时,我们需要关注三点:

  1. 返回值:本题的返回值就是新的链表的头节点 newHead;
  2. 调用单元做了什么:进行交换工作,即将 2 指向 11 指向下一层的递归函数,最后返回节点 2;
  3. 终止条件:head为空指针或head->next为空指针。

要理解的一点是,传入调用单元的函数是准备交换的下一对节点的第一个节点,也就是1,2,3,4中,已经进行完1和2的交换,接下来调用swapPairs()函数时,里面传入的参数应该是3,起到将3和4交换的作用。
 

Java:(为了加强Java和C++语法熟练程度,每道题基本上都分别用两个语言编写)

class Solution {
    public ListNode swapPairs(ListNode head) {
        //终止条件
        if(head==null||head.next==null){
            return head;
        }
        //实现第一次交换
        ListNode newHead=head.next;// 新的头节点为当前头节点的下一个节点
        //调用单元
        //将第三个节点传入用了交换第三和第四个节点
        //并且交换后的前头节点的下一个节点为后面的子链表
        ListNode three=head.next.next;
        head.next=swapPairs(three);
        newHead.next=head;
        //返回值
        return newHead;
    }
}

时间复杂度:O(n),其中n是链表的节点数量。需要对每个节点进行更新指针的操作。

空间复杂度:O(n),其中n是链表的节点数量。空间复杂度主要取决于递归调用的栈空间。

其实很多递归并不是最优解法,可以看到时间复杂度和迭代算法是一样的,甚至空间复杂度更高,只是看起来相对简洁而已,但是思路往往比较抽象,所以做题时不用非得死磕递归,能AC是王道。


1​​​​​​9.删除链表的倒数第N个节点 

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:能尝试使用一趟扫描实现吗?

输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 示例 2:

输入:head = [1], n = 1 输出:[] 示例 3:

输入:head = [1,2], n = 1 输出:[1]

这道题的进阶要求是一趟实现,其实跟之前数组单元中删除第n个元素的思路类似,还是采用双指针的思想。

这里需要注意“倒数”二字,遇到这种有长度或时间差的情况,一般可以设置快慢两个指针,让fast指针先走n步,然后slow指针和fast指针一同移动,直到fast指向链表末尾,删掉slow所指向的节点就可以了。想到了之前考研时遇到的相交和环形问题,都是类似的思路。

同样因为考虑到涉及头节点的情况,设置虚拟头节点,fast和slow两个指针初始位置都指向虚拟头节点。因此,这时删除的情况便是,当fast为空指针时进行删除操作。这里又有一个细节:如果slow指向的是待删除节点,不好进行删除操作,因此可以让slow指向待删除节点的前一个节点,方便做删除操作,所以fast和slow之间的距离差应该为n+1,即fast多走n+1步。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyHead=new ListNode(0);
        ListNode fast=dummyHead;
        ListNode slow=dummyHead;
        dummyHead.next=head;
        while(n-- > 0 && fast!=null){
            fast=fast.next;
        }
        fast=fast.next;  //fast多走一步
        while(fast!=null){
            fast=fast.next;
            slow=slow.next;
        }
        slow.next=slow.next.next;
        return dummyHead.next;

    }
}

⚠️ 这里在第一次编写的时候遇到了一个典型的语法错误,在 Java 中,&& 运算符需要两个操作数都是布尔类型。在代码中,n-- 是一个整数,而不是布尔值,因此导致了编译错误。因此需要将 n-- 与零进行比较,以产生布尔结果。



面试题 02.07. 链表相交 

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

题目数据保证整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

看到这个题,想到上一题里我有提到过,如果遇到有长度差,或者相交的情况,可以首先考虑能不能用双指针的思路来解决。之前也遇到过类似的题,比如有两个很像的单词,找出从第几个字母开始后面都是一样的等等。本质上都是求两个链表交点节点的指针。

我们主要关注的是两个链表都不为空的一般情况,这时要么两个链表长度相等,否则一定会有一个长链表和一个短链表,因此先求出两个链表的长度,接着计算长度差,让两个链表的指针指向同一起点,即把它们的末尾对齐如下图所示:
 

然后同时向后移动curA和curB,如果遇到curA == curB,则找到交点。这个题其实和上一个找倒数第n个节点很类似有木有。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode curA=headA;
        ListNode curB=headB;
        int lenA=0, lenB=0;
        //获取两个链表的长度
        while(curA!=null){
            lenA++;
            curA=curA.next;
        }
        while(curB!=null){
            lenB++;
            curB=curB.next;
        }
        curA=headA;
        curB=headB;
        //找出长的链表使其成为curA
        if(lenA<lenB){
            ListNode tmp=curA;
            int tmpLen=lenA;
            //  进行交换
            lenA=lenB;
            lenB=tmpLen;
            curA=curB;
            curB=tmp;
        }
        int gap=lenA-lenB;
        while(gap-->0){
            curA=curA.next;
        }
        while(curA!=null){
            if(curA==curB){
                return curA;
            }
            else{
                curA=curA.next;
                curB=curB.next;
            }
        }
        return null;

        
    }
}

时间复杂度: O(n)

空间复杂度: O(n)

除了上面写法,还在题解中看到了之前提到Krahets大佬的解法,分享在这里以供参考:

设「第一个公共节点」为 node ,「链表 headA」的节点数量为 a,「链表 headB」的节点数量为 b ,「两链表的公共尾部」的节点数量为 c ,则有:

头节点 headA 到 node 前,共有a−c 个节点;
头节点 headB 到 node 前,共有b−c 个节点;

考虑构建两个节点指针 A​ , B 分别指向两链表头节点 headA , headB ,做如下操作:

  • 指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为:a+(b−c)
  • 指针 B 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为:b+(a−c)

如下式所示,此时指针 A , B 重合,并有两种情况:

a+(b−c)=b+(a−c)

  1. 若两链表公共尾部 (即c>0 ) :指针 A , B 同时指向「第一个公共节点」node 。
  2. 若两链表公共尾部 (即c=0 ) :指针 A , B 同时指向 null

因此返回 A 即可。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode A = headA, B = headB;
        while (A != B) {
            A = A != null ? A.next : headB;
            B = B != null ? B.next : headA;
        }
        return A;
    }
}


142.环形链表II  

题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

说明:不允许修改给定的链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

又是一道在做移除倒数第n个节点时提到的情况——环形链表,还是用双指针去解决。如果是第一次遇到这个问题的小伙伴真就容易卡在这里了。再次强调诸如:寻找距离尾部第 K 个节点、寻找环入口、寻找公共尾部入口等,都可以用双指针。

首先梳理一下这道题需要我们解决什么问题:

1. 是否有环?

这道题其实是141.环形链表的升级版,在判断有环的基础上还要确定环的入口。

首先,是否有环这个问题,环意味如何设置一个指针不断遍历链表,到了某一点后,会不断地重复访问到这一点以及后面的节点。因此一种比较直接的思路是设置一个哈希表,遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。

如果用双指针来解决,涉及到Floyd 判圈算法龟兔赛跑算法):

假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。

当一个链表有环时,让快指针当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。因此分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果fast 指针走过链表末端,说明链表无环,此时直接返回 null;如果 fast 和slow指针在途中相遇 ,说明这个链表有环。

2. 若有环,入口在哪里?

假设从头结点到环形入口节点的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点之间的节点数为y。 从相遇节点再到环形入口节点节点数为z。 如图所示:

那么相遇时:

  • slow指针走过的节点数为: x+y
  • fast指针走过的节点数:x+y+n(y+z),n为fast在环内走了n圈才遇到slow

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x+y)*2 = x+y+n(y+z)

将公式进行整理可以得到:(x+y)=n(y+z)

而我们要求的是x的值,即头节点到入口节点的长度,因此:x = (n - 1) (y + z) + z

当 n为1的时候,公式就化解为 x=z,这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是环形入口的节点。因此这里设置两个index指针,index1从第一次相遇的节点出发,index2从头节点出发,等index1==index2时,即为环形入口的节点。对于n>1的情况,效果和n=1是一样的,只不过index1多转了n-1圈而已。


class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast=head;
        ListNode* slow=head;
        while(fast!=NULL && fast->next!=NULL && fast->next->next!=NULL){
            slow=slow->next;
            fast=fast->next;
            fast=fast->next;
            if(slow==fast){
                ListNode* idx1=head;
                ListNode* idx2=fast;
                while(idx1!=idx2){
                    idx1=idx1->next;
                    idx2=idx2->next;
                }
                return idx2;
            }
        }
        return NULL;
        
    }
};

⚠️这里面要注意,如果只写while(fast->next!=NULL && fast->next->next!=NULL)会报错,因为编译器不知道你当前的节点是否为空,因此还要加上对于当前节点的判断:


链表总结

双指针技术本质上是依赖速度起始位置不同的两个指针对于链表的遍历来实现对问题的快速解决。在这里已经引用自在飞花的总结,已经很全面了。

无法高效获取长度,无法根据偏移快速访问元素,是链表的两个劣势。然而面试的时候经常碰见诸如获取倒数第k个元素,获取中间位置的元素,判断链表是否存在环,判断环的长度等和长度与位置有关的问题。这些问题都可以通过灵活运用双指针来解决。

Tips:双指针并不是固定的公式,而是一种思维方式~

(1)先来看"倒数第k个元素的问题"。设有两个指针 p 和 q,初始时均指向头结点。首先,先让 p 沿着 next 移动 k 次。此时,p 指向第 k+1个结点,q 指向头节点,两个指针的距离为 k 。然后,同时移动 p 和 q,直到 p 指向空,此时 q 即指向倒数第 k 个结点。可以参考下图来理解:


class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode *p = head, *q = head; //初始化
        while(k--) {   //将 p指针移动 k 次
            p = p->next;
        }
        while(p != nullptr) {//同时移动,直到 p == nullptr
            p = p->next;
            q = q->next;
        }
        return q;
    }
};

(2)获取中间元素的问题。设有两个指针 fast 和 slow,初始时指向头节点。每次移动时,fast向后走两次,slow向后走一次,直到 fast 无法向后走两次。这使得在每轮移动之后。fast 和 slow 的距离就会增加一。设链表有 n 个元素,那么最多移动 n/2 轮。当 n 为奇数时,slow 恰好指向中间结点,当 n 为 偶数时,slow 恰好指向中间两个结点的靠前一个(可以考虑下如何使其指向后一个结点呢?)。

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode *p = head, *q = head;
        while(q != nullptr && q->next != nullptr) {
            p = p->next;
            q = q->next->next;
        }
        return p;
    } 
};

(3)是否存在环的问题。如果将尾结点的 next 指针指向其他任意一个结点,那么链表就存在了一个环。

上一部分中,总结快慢指针的特性 —— 每轮移动之后两者的距离会加一。下面会继续用该特性解决环的问题。
当一个链表有环时,快慢指针都会陷入环中进行无限次移动,然后变成了追及问题。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的。当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。如果一个链表存在环,那么快慢指针必然会相遇

class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode *slow = head;
        ListNode *fast = head;
        while(fast != nullptr) {
            fast = fast->next;
            if(fast != nullptr) {
                fast = fast->next;
            }
            if(fast == slow) {
                return true;
            }
            slow = slow->next;
        }
        return nullptr;
    }
};

最后一个问题,如果存在环,如何判断环的入口呢?方法是,从快慢指针相遇的点设置一个指针,从头节点设置一个指针,直到这两个指针相遇,所在节点即为入口。两次相遇间的移动次数即为环的长度。

  • 29
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值