【算法练习Day4】 两两交换链表节点&&删除链表倒数第 N 个结点&&环形链表 II

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:练题
🎯长路漫漫浩浩,万事皆有期待

两两交换链表中的节点

24. 两两交换链表中的节点- 力扣(LeetCode)
在这里插入图片描述

该题是交换一条链表中两两相连的节点,注意不能直接交换节点数值,而是要控制指针完成节点的交换,这道题我一听题目感觉好像是两个链表的节点互相交换的题目,但实际看到题发现并不是,看到题目有点懵,不知道该怎么交换。

这道题的要点就是要找好,交换节点哪些节点该用临时指针保存,而哪些节点并不需要保存,一定要理清思路,才不会在交换节点时候将自己绕进去。

一般思路

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while(cur->next != nullptr && cur->next->next != nullptr) {
            ListNode* tmp = cur->next; // 记录临时节点
            ListNode* tmp1 = cur->next->next->next; // 记录临时节点

            cur->next = cur->next->next;    // 步骤一
            cur->next->next = tmp;          // 步骤二
            cur->next->next->next = tmp1;   // 步骤三

            cur = cur->next->next; // cur移动两位,准备下一轮交换
        }
        return dummyHead->next;
    }
};

这道题也采用了虚拟头节点的技巧,交换数据时,需要找到要交换数据的上一个节点,例如题目测试用例1中,1,2节点交换信息,而1节点的上一个虚拟头节点连接的是2号节点,然后2号节点链接一号节点完成两两交换后,1节点连接之前2号节点后面的链表部分,如果没有虚拟头节点作为辅助链接,那么在第一次交换,也就是头节点需要进行两两交换时,就要单独判断一次,增加代码量的同时,也更容易出错。

上述的讲解,就已经明确了我们需要两个临时变量来指向节点,一个用来指向待交换的第一个节点,另一个用来指向第二个待交换的节点的next节点,以防交换完之后找不到后面的链表,而存储第一个待交换的节点的目的是,前一个节点链接第二个待交换的节点,后第二个节点再连第一个,方便交换。

递归思路

先两两交换后面的节点,再交换前面的节点,链接上后面交换好了的节点,注意递归结束是空节点或者只剩一个节点
在这里插入图片描述

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if(head==nullptr||head->next==nullptr)
        {
            return head;
        }
        auto tmp=swapPairs(head->head->next);//先两两交换后面的节点,返回新的头节点
        auto ret=head->next;//先存一下最终返回的节点
        head->next->next=head;//交换前面的节点
        head->next=tmp;//链接上后面交换好了的节点
        return ret;//返回现在的头结点
    }
};

其他问题

● while (cur->next != nullptr && cur->next->next != nullptr) 这边为什么是&& 不是|| 一个是对于偶数个结点的判断 一个是奇数个结点 那不应该是||的关系吗?
奇数节点就不需要交换了,所以只有满足后面有偶数个节点的时候才会进入循环

● 循环条件,什么情况应该判断指针本身为空呢?
可以看这个遍历的指针最后需要走到哪里 需不需要对最后一个节点做操作

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

删除链表的倒数第 N 个结点

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
在这里插入图片描述

暴力求解

这道题最容易想出的方法是暴力求解,方法是先遍历一遍链表用计数器count数出有多少个链表节点,之后用count减去N,得到的就是正着数第count-N个节点就是我们要删除的节点,然后还是用之前删除链表的思路,先找到要删除节点的前一个节点的next指向要删除节点的next完成删除。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode*hummyhead=new ListNode(0);
        hummyhead->next=head;
        ListNode*cur=hummyhead;int count=0;
        while(cur->next){
            cur=cur->next;count++;
        }
        int n1=(count-n)>0?count-n:0;cur=hummyhead;
        while(n1--){
            cur=cur->next;
        }
        if(count)
        cur->next=cur->next->next;
        return hummyhead->next;
    }
};

此题仍然使用虚拟头节点的方法来写,这样当要删除的节点是头节点的时候,并不需要单独判断情况,节省代码量。

双指针法

而另一种相对省时间一点的方法是双指针法

具体思路为创建快慢指针,一开始均指向虚拟头节点,然后让快指针先走n步,然后快慢再同时往后遍历,直到快指针指向空,此时慢指针所指向的节点则为要删除的节点。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* slow = dummyHead;
        ListNode* fast = dummyHead;
        while(n-- && fast != NULL) {
            fast = fast->next;
        }
        fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
        while (fast != NULL) {
            fast = fast->next;
            slow = slow->next;
        }
        
         ListNode *tmp = slow->next;//  C++释放内存的逻辑
         slow->next = tmp->next;
         delete tmp;
        
        return dummyHead->next;
    }
};

思路写到这里已经很清晰了,那么为什么我们要将n++之后再走呢?原因也很简单,是为了让快指针fast指向要删除节点的上一个位置,如果不明白这个快慢指针的思路,可以自行在纸上以画图的形式模拟一下。

采用虚拟头结点不需要单独判断头结点是否为空,并且确实很有必要,不然很容易想漏情况,以下是不用虚拟头结点的代码:

class Solution {
public:
    ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
    if(pListHead==nullptr)
	{
		return nullptr;
	}

	ListNode* front=pListHead;
	ListNode* rear=pListHead;

	while (k>0&&front) {
	k--;
	front=front->next;
	}

	while (front) {
	front=front->next;
	rear=rear->next;
	}

	return k>0?nullptr:rear;
    }
};

其他问题

● 链表问题的debug:可以在循环中逻辑执行前打一次输出语句,执行后打一次输出节点值val, 符合预期在增加打输出node.next.val(如果你需要知道后面连接是啥),空针就代表断链了或者其他错误了, 然后这样慢慢增加找, 还有一种就是利用手动画图debug学链表的时候经常画图设计简单case边界case来调试,现在写链表的题都是可以脑子有链表图

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

环形链表 II

142. 环形链表 II - 力扣(LeetCode)

在这里插入图片描述

环形链表的基础上,增加了需要判断环的入口的问题,首先是判断链表是否有环,有环的判断是简单的,用双指针方法都从头开始,快指针一次走两个节点,慢指针一次走一个节点,如果相遇则为有环,相遇了之后在相遇点处,用一个指针来指向(slow指针直接向后走也可以)相遇点,然后在头节点再用一个指针指向,两个指针一起走,相遇处则为环的入口。

为什么快慢指针,一定会在有环时相遇呢?如果没有环,那么快指针一定比慢指针走得快所以,不可能相遇,但是如果有环,两指针同时在环内,那就是快指针追逐慢指针,由于快指针一次走两步,慢指针一次走一步,进环后每次运动都相当于两指针距离减少一个节点,相当于慢指针不动,快指针每次走一步,所以有环两节点一定会相遇!但是如果快指针一次走更多步,比如走三步,那么有可能进入环之后,两指针无法相遇,造成死循环。

讲完相遇再说说,第二个关键的步骤,我们假设从起点到换入口距离为x,从入口到相遇点位置距离为y,相遇点再到入口的后半段距离我们设为z,可得慢指针slow走过x+y,而快指针fast走过x+y+n*(y+z),这里的n是fast指针在环内走的圈数,y+z是一圈经过的距离,可得(x+y)2=x+y+n(y+z)化简得x=n*(y+z)-y,单独提出一圈也就是y+z来消去-y得x=(n-1)*(y+z)+z。当n为一圈时,得到x=z的关系。

正是由于这样我们才得到了上述的结论,如果fast指针在圈内转了不止一圈,实际上结果也是一样的因为无论它转了多少圈,最后开始相遇的那一圈,起点是一样的。

如果对以上推理有些疑问,可以参考这篇博客,里面有详细的分析:【链表OJ题(九)】环形链表延伸问题以及相关OJ题

公式推导

这个方法的难点在于公式推导的过程,只要推导出了公式,解题就变得十分简单
结论:一个指针从 相遇点 开始走,一个指针从 链表头 开始走,它们会在 环的入口点 相遇。”
接下来推导公式:

在这里插入图片描述
由于 fast 的速度是 slow 的 2 倍。

所以便可以得出这个式子:2 ( L + x ) = L + N * c + x,而这个式子又可以进行推导:

2 ( L + x ) = L + N * c + x
            ↓
      L + x = N * c
            ↓
      L = N * c - x
            ↓
      L = ( N - 1 ) * c +  c - x 

这里 公式已经推导 完成:L = ( N - 1 ) * c + c - x 。但是这个公式到底是什么意思?

意思是一个指针从起始点开始走,一个指针从相遇点开始走,它们会在环的入口点相遇

根据这个我们也就可以做出这道题目了。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast != NULL && fast->next != NULL) {
            slow = slow->next;
            fast = fast->next->next;
            // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
            if (slow == fast) {
                ListNode* index1 = fast;
                ListNode* index2 = head;
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index2; // 返回环的入口
            }
        }
        return NULL;
    }
};

相交链表

先利用 快慢指针 ,以 环形链表 的解法,找到 fast 和 slow 相交的点。然后将这个 交点 给为 meetnode 。作为两条新链表的尾。那么 meetnode->next 为某条新链表的头。然后 入环点 ,就可以看做是两条链表的交点。然后就是 相交链表 的做法
在这里插入图片描述

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* curA = headA;
        ListNode* curB = headB;
        int lenA = 0, lenB = 0;
        while (curA != NULL) { // 求链表A的长度
            lenA++;
            curA = curA->next;
        }
        while (curB != NULL) { // 求链表B的长度
            lenB++;
            curB = curB->next;
        }
        curA = headA;
        curB = headB;
        // 让curA为最长链表的头,lenA为其长度
        if (lenB > lenA) {
            swap (lenA, lenB);
            swap (curA, curB);
        }
        // 求长度差
        int gap = lenA - lenB;
        // 让curA和curB在同一起点上(末尾位置对齐)
        while (gap--) {
            curA = curA->next;
        }
        // 遍历curA 和 curB,遇到相同则直接返回
        while (curA != NULL) {
            if (curA == curB) {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return NULL;
    }
};

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

总结:

今天的三道题也算是复习回顾了,但对递归有了新的理解。接下来,我们继续进行算法练习·。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sherry的成长之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值