代码随想录算法训练营第四天| Leetcode 24. 两两交换链表中的节点、19.删除链表的倒数第N个节点、160. 链表相交 、142.环形链表II

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

题目链接:Leetcode 24. 两两交换链表中的节点

题目描述:

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

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

思路:本题的重点两两交换,而由于交换这两个节点需要先找到第一个节点的前一个节点,而头节点又没有前一个节点,因此本题构建一个虚拟头节点是一个很好的方法。个人体会,对于任何抽象的模拟过程,手动模拟画图是最好的理解方式。

注:以上图片来源于《代码随想录》

以下是代码实现:

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* cur = dummyHead;
        //警惕这里的判断条件,连续两个->要注意第一个next不是空节点
        while (cur->next != nullptr && cur->next->next != nullptr) {
            //至于这里的写法大同小异,只要保证移动指针顺序正确即可
            ListNode* temp = cur->next;

            cur->next = temp->next;
            temp->next = cur->next->next;
            cur->next->next = temp;
            //不要忘了更新cur指针
            cur = cur->next->next;
        }
        return dummyHead->next;
    }
};

Leetcode 19.删除链表的倒数第N个节点

题目链接:Leetcode 19.删除链表的倒数第N个节点

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

思路:这道题最容易想到的方法就是先遍历一遍链表统计链表长度,然后循环n次即可。

第一种方法:利用链表长度找到删除节点

class Solution {
public:
    int getlen(ListNode* head) {
        int len = 0;
        while (head) {
            ++len;
            head = head->next;
        }
        return len;
    }
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0, head);
        int len = getlen(head);
        ListNode* cur = dummyHead;
        //注意节点序号从1开始(一定要看清楚每道题的起始序号)
        //这里用++i或者i++的目的是最后让cur指向被删除元素的前一个元素
        for (int i = 1; i < len - n + 1; i++) {
            cur = cur->next;
        }
        //删除节点基本操作
        cur->next = cur->next->next;
        ListNode* res = dummyHead->next;
        delete dummyHead;
        return res;
    }
};
  • 时间复杂度:O(L),其中 L 是链表的长度。

  • 空间复杂度:O(1)。

费劲心思求出链表长度之后,由于本题的进阶是寻找只需一次循环的方法,不禁联想到栈的先进后出的性质。尽管利用栈可以仅需一次循环,但是代价是增大了空间复杂度

第二种方法:利用栈的性质找到删除节点

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0, head);
        stack<ListNode*> st;
        ListNode* cur = dummyHead;
        while (cur) {
            st.push(cur);
            cur=cur->next;
        }
        for (int i = 0; i < n; i++) {
            st.pop();
        }
        ListNode* prev = st.top();
        prev->next = prev->next->next;
        ListNode* res = dummyHead->next;
        delete dummyHead;
        return res;
    }
};
  • 时间复杂度:O(L),其中 L 是链表的长度。

  • 空间复杂度:O(L),其中 L是链表的长度。主要为栈的开销。

那有没有一种方法既能用一次循环代替两次循环,而且空间复杂度还不会提高呢?

转念一想,上面黑体字不正是双指针法解决的问题吗?

第三种方法:双指针法

(1)定义fast指针和slow指针,初始值为虚拟头结点.

(2)fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点。

(3)fast和slow同时移动,直到fast指向末尾

(4)删除slow指向的下一个节点

现在有一个问题,为什么fast先走n+1步之后二者同时移动,slow恰好指向倒数第n个节点的前一个节点?(由于本题删除节点的需要,我们需要找到被删除节点的前一个结点)

换句话说,为什么fast先走n步之后二者同时移动,slow恰好指向倒数第n个节点?

说下我的理解:fast移动n步之后,fast与slow之间距离之差为n个节点,由于两个指针移动速度相同,在移动次数相同的情况下,二者的距离差是一定的,因此当fast指向NULL时,slow与fast的距离差仍然是n,而根据“倒数”这一定义,slow指向的地方这不恰恰是倒数第n个节点吗?

如果仍不太理解,可以看这里

代码如下:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0, head);
        ListNode* fast = dummyHead;
        ListNode* slow = dummyHead;
        while (n-- && fast != nullptr) {
            fast = fast->next;
        }
        //再让fast走一步
        fast = fast->next;
        // fast和slow一起走,二者之间距离差不变
        //因此fast指向NULL时slow指向倒数第n个节点的前一个节点
        while (fast != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }
        ListNode* temp = slow->next; //保存该节点便于释放内存
        slow->next = slow->next->next;
        delete temp;
        return dummyHead->next;
    }
};
  • 时间复杂度:O(L),其中 L 是链表的长度。
  • 空间复杂度: O(1)

Leetcode 160.链表相交

题目链接:Leetcode 160.链表相交

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

思路:

PS:这道题真的被leetcode大佬的题解折服了,以至于其他方法黯然失色(就是表达一下心情,没有说其他方法不好的意思)同样是双指针,但是思路非常巧妙写法非常简洁!!!

注:以下思路及证明过程来自于Leetcode题解区:

160. 相交链表 - 力扣(LeetCode)

(1)首先判断两个链表是否为空,如果为空,则一定没有交点。

(2)当二者均不为空时,创建两个指针pA,pB分别指向两个链表的头节点,然后将两个指针依次遍历两个链表的每个节点。具体步骤如下:

  • 每步操作需要同时更新指针 pA 和 pB。
  • 如果指针 pA 不为空,则将指针 pA 移到下一个节点;如果指针 pB 不为空,则将指针 pB移到下一个节点。
  • 如果指针 pA 为空,则将指针 pA移到链表 headB 的头节点;如果指针 pB 为空,则将指针 pB 移到链表 headA 的头节点。

  • 当指针 pA 和 pB 指向同一个节点或者都为空时,返回它们指向的节点或者 nullptr。

对于双指针方法的证明过程如下:

以下是代码:

class Solution {
public:
    ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
        if (headA == nullptr || headB == nullptr) {
            return nullptr;
        }
        ListNode *pA = headA, *pB = headB;
        while (pA != pB) {
            pA = pA == nullptr ? headB : pA->next;
            pB = pB == nullptr ? headA : pB->next;
        }
        return pA;
    }
};
  • 时间复杂度:时间复杂度:O(m+n)),其中 m 和 n 是分别是链表 headA 和 headB 的长度。两个指针同时遍历两个链表,每个指针遍历两个链表各一次。
  • 空间复杂度: O(1)。

Leetcode 142.环形链表II

题目链接:Leetcode 142.环形链表II

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

思路:本题通过数学推导出一个小结论,并根据双指针法实现。主要分为两个步骤:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

判断是否有环很简单,假设快指针一次移动两个节点慢指针一次移动一个节点,直到快指针移动到NULL。

(1)如果无环,则快慢指针不会相遇。

第一个很好理解,快指针移动的快,慢指针肯定追不上

(2)如果有环,则快慢指针会相遇,且会在环内相遇。

第二个需要大家思考一下,首先快慢指针在环里相遇,为什么?由于快指针速度快,会先一步进入环,一直绕环旋转而不会出环,直到慢指针进入环。那为什么一定会相遇呢,不会出现快指针越过慢指针的情况吗?答案是不会。因为二者速度之差仅为1个节点,换句话说,快指针在慢指针进入环内以1个节点的速度接近慢指针,因此一定会相遇。

判断是否有环之后,通过一定的数学推导可以巧妙的实现第二个问题(至于怎么想到的我也不知道,也许是某位大佬灵光一动就推出来了)。具体的推导过程看这里

代码实现:

class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast != nullptr && fast->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
            // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
            if (fast == slow) {
                ListNode* index1 = fast;
                ListNode* index2 = head;
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index2;// 返回环的入口
            }
        }
        return nullptr;
    }
};
  • 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
  • 空间复杂度: O(1)

总结:作为初学者,学习前人的经验是必要的,今天的第三题和第四题就是很好的例子,一开始我只能想出最简单的方法,但是通过理解其他人的方法,我也感觉到有很大的长进。

最后,如果文章有错误,请在评论区或私信指出,让我们共同进步!

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第二十二天的算法训练营主要涵盖了Leetcode题目的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组找到长度最小的子数组,

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值